Skip to main content

quicknode_sdk/webhooks/
webhook.rs

1#[cfg(feature = "rust")]
2use bon::Builder;
3#[cfg(feature = "node")]
4use napi_derive::napi;
5#[cfg(feature = "python")]
6use pyo3::{pyclass, pymethods};
7#[cfg(feature = "python")]
8use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods};
9use serde::{Deserialize, Deserializer, Serialize};
10
11fn deserialize_as_optional_json_string<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
12where
13    D: Deserializer<'de>,
14{
15    let value = Option::<serde_json::Value>::deserialize(deserializer)?;
16    match value {
17        None => Ok(None),
18        Some(v) => serde_json::to_string(&v)
19            .map(Some)
20            .map_err(serde::de::Error::custom),
21    }
22}
23
24// ── Enums ──────────────────────────────────────────────────────────────────
25
26/// Identifier of a predefined webhook filter template.
27#[cfg_attr(feature = "node", napi(string_enum))]
28#[cfg_attr(not(feature = "node"), derive(Clone))]
29#[derive(Debug, Serialize, Deserialize)]
30#[serde(rename_all = "camelCase")]
31pub enum WebhookTemplateId {
32    EvmWalletFilter,
33    EvmContractEvents,
34    EvmAbiFilter,
35    SolanaWalletFilter,
36    BitcoinWalletFilter,
37    XrplWalletFilter,
38    HyperliquidWalletEventsFilter,
39    StellarWalletTransactionsSourceAccountFilter,
40}
41
42impl WebhookTemplateId {
43    pub fn as_str(&self) -> &'static str {
44        match self {
45            WebhookTemplateId::EvmWalletFilter => "evmWalletFilter",
46            WebhookTemplateId::EvmContractEvents => "evmContractEvents",
47            WebhookTemplateId::EvmAbiFilter => "evmAbiFilter",
48            WebhookTemplateId::SolanaWalletFilter => "solanaWalletFilter",
49            WebhookTemplateId::BitcoinWalletFilter => "bitcoinWalletFilter",
50            WebhookTemplateId::XrplWalletFilter => "xrplWalletFilter",
51            WebhookTemplateId::HyperliquidWalletEventsFilter => "hyperliquidWalletEventsFilter",
52            WebhookTemplateId::StellarWalletTransactionsSourceAccountFilter => {
53                "stellarWalletTransactionsSourceAccountFilter"
54            }
55        }
56    }
57}
58
59/// Position a webhook begins (or resumes) delivering from when activated.
60#[cfg_attr(feature = "node", napi(string_enum))]
61#[cfg_attr(not(feature = "node"), derive(Clone))]
62#[derive(Debug, Serialize, Deserialize)]
63#[serde(rename_all = "lowercase")]
64pub enum WebhookStartFrom {
65    /// Resume from the last-delivered block.
66    Last,
67    /// Start from the newest available block.
68    Latest,
69}
70
71// ── Template Arg Structs ───────────────────────────────────────────────────
72
73/// Template arguments for an EVM wallet filter: matches activity for a list of
74/// wallet addresses.
75#[cfg_attr(feature = "rust", derive(Builder))]
76#[cfg_attr(feature = "python", gen_stub_pyclass)]
77#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
78#[cfg_attr(feature = "node", napi(object))]
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct EvmWalletFilterTemplate {
81    /// Wallet addresses to match against.
82    pub wallets: Vec<String>,
83}
84
85#[cfg(feature = "python")]
86#[gen_stub_pymethods]
87#[pymethods]
88impl EvmWalletFilterTemplate {
89    #[new]
90    pub fn new(wallets: Vec<String>) -> Self {
91        Self { wallets }
92    }
93}
94
95/// Template arguments for filtering EVM contract events, scoped to a specific
96/// set of event topic hashes.
97#[cfg_attr(feature = "rust", derive(Builder))]
98#[cfg_attr(feature = "python", gen_stub_pyclass)]
99#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
100#[cfg_attr(feature = "node", napi(object))]
101#[derive(Debug, Clone, Serialize, Deserialize)]
102#[serde(rename_all = "camelCase")]
103pub struct EvmContractEventsTemplate {
104    /// Contract addresses to watch for events.
105    pub contracts: Vec<String>,
106    /// Event topic hashes to restrict the filter to specific events.
107    pub event_hashes: Vec<String>,
108}
109
110#[cfg(feature = "python")]
111#[gen_stub_pymethods]
112#[pymethods]
113impl EvmContractEventsTemplate {
114    #[new]
115    pub fn new(contracts: Vec<String>, event_hashes: Vec<String>) -> Self {
116        Self {
117            contracts,
118            event_hashes,
119        }
120    }
121}
122
123/// Template arguments for an EVM ABI filter: decodes and filters events for a
124/// set of contracts using a provided ABI.
125#[cfg_attr(feature = "rust", derive(Builder))]
126#[cfg_attr(feature = "python", gen_stub_pyclass)]
127#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
128#[cfg_attr(feature = "node", napi(object))]
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct EvmAbiFilterTemplate {
131    /// JSON-encoded contract ABI used to decode event data.
132    pub abi: String,
133    /// Contract addresses to watch for events.
134    pub contracts: Vec<String>,
135}
136
137#[cfg(feature = "python")]
138#[gen_stub_pymethods]
139#[pymethods]
140impl EvmAbiFilterTemplate {
141    #[new]
142    pub fn new(abi: String, contracts: Vec<String>) -> Self {
143        Self { abi, contracts }
144    }
145}
146
147/// Template arguments for a Solana wallet filter: matches activity for a list
148/// of Solana account addresses.
149#[cfg_attr(feature = "rust", derive(Builder))]
150#[cfg_attr(feature = "python", gen_stub_pyclass)]
151#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
152#[cfg_attr(feature = "node", napi(object))]
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct SolanaWalletFilterTemplate {
155    /// Solana account addresses to match against.
156    pub accounts: Vec<String>,
157}
158
159#[cfg(feature = "python")]
160#[gen_stub_pymethods]
161#[pymethods]
162impl SolanaWalletFilterTemplate {
163    #[new]
164    pub fn new(accounts: Vec<String>) -> Self {
165        Self { accounts }
166    }
167}
168
169/// Template arguments for a Bitcoin wallet filter.
170#[cfg_attr(feature = "rust", derive(Builder))]
171#[cfg_attr(feature = "python", gen_stub_pyclass)]
172#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
173#[cfg_attr(feature = "node", napi(object))]
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct BitcoinWalletFilterTemplate {
176    /// Bitcoin wallet addresses to match against.
177    pub wallets: Vec<String>,
178}
179
180#[cfg(feature = "python")]
181#[gen_stub_pymethods]
182#[pymethods]
183impl BitcoinWalletFilterTemplate {
184    #[new]
185    pub fn new(wallets: Vec<String>) -> Self {
186        Self { wallets }
187    }
188}
189
190/// Template arguments for an XRPL wallet filter.
191#[cfg_attr(feature = "rust", derive(Builder))]
192#[cfg_attr(feature = "python", gen_stub_pyclass)]
193#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
194#[cfg_attr(feature = "node", napi(object))]
195#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct XrplWalletFilterTemplate {
197    /// XRPL wallet addresses to match against.
198    pub wallets: Vec<String>,
199}
200
201#[cfg(feature = "python")]
202#[gen_stub_pymethods]
203#[pymethods]
204impl XrplWalletFilterTemplate {
205    #[new]
206    pub fn new(wallets: Vec<String>) -> Self {
207        Self { wallets }
208    }
209}
210
211/// Template arguments for a Hyperliquid wallet-events filter.
212#[cfg_attr(feature = "rust", derive(Builder))]
213#[cfg_attr(feature = "python", gen_stub_pyclass)]
214#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
215#[cfg_attr(feature = "node", napi(object))]
216#[derive(Debug, Clone, Serialize, Deserialize)]
217pub struct HyperliquidWalletEventsFilterTemplate {
218    /// Hyperliquid wallet addresses to match against.
219    pub wallets: Vec<String>,
220}
221
222#[cfg(feature = "python")]
223#[gen_stub_pymethods]
224#[pymethods]
225impl HyperliquidWalletEventsFilterTemplate {
226    #[new]
227    pub fn new(wallets: Vec<String>) -> Self {
228        Self { wallets }
229    }
230}
231
232/// Template arguments for a Stellar wallet-transactions filter, matching
233/// transactions where the given wallets are the source account.
234#[cfg_attr(feature = "rust", derive(Builder))]
235#[cfg_attr(feature = "python", gen_stub_pyclass)]
236#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
237#[cfg_attr(feature = "node", napi(object))]
238#[derive(Debug, Clone, Serialize, Deserialize)]
239pub struct StellarWalletTransactionsFilterTemplate {
240    /// Stellar wallet addresses to match against.
241    pub wallets: Vec<String>,
242}
243
244#[cfg(feature = "python")]
245#[gen_stub_pymethods]
246#[pymethods]
247impl StellarWalletTransactionsFilterTemplate {
248    #[new]
249    pub fn new(wallets: Vec<String>) -> Self {
250        Self { wallets }
251    }
252}
253
254// ── ByList Template Arg Structs ────────────────────────────────────────────
255//
256// Every template supports two input shapes: inline values (e.g. `wallets:
257// [...]`) or a reference to a pre-created list (`walletsListName: "..."`).
258// The two shapes share the same `templateId` and the same URL path; the
259// server disambiguates by which field is present. The SDK models each shape
260// as its own struct so callers can't accidentally mix them.
261
262/// ByList form of `EvmWalletFilterTemplate` — references a pre-created
263/// wallets list by name instead of inlining the addresses.
264#[cfg_attr(feature = "rust", derive(Builder))]
265#[cfg_attr(feature = "python", gen_stub_pyclass)]
266#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
267#[cfg_attr(feature = "node", napi(object))]
268#[derive(Debug, Clone, Serialize, Deserialize)]
269#[serde(rename_all = "camelCase")]
270pub struct EvmWalletFilterByListTemplate {
271    /// Name of the pre-created wallets list.
272    pub wallets_list_name: String,
273}
274
275#[cfg(feature = "python")]
276#[gen_stub_pymethods]
277#[pymethods]
278impl EvmWalletFilterByListTemplate {
279    #[new]
280    pub fn new(wallets_list_name: String) -> Self {
281        Self { wallets_list_name }
282    }
283}
284
285/// ByList form of `EvmContractEventsTemplate` — references pre-created
286/// contract and (optionally) event-hash lists by name. Omitting
287/// `event_hashes_list_name` matches all events from the listed contracts.
288#[cfg_attr(feature = "rust", derive(Builder))]
289#[cfg_attr(feature = "python", gen_stub_pyclass)]
290#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
291#[cfg_attr(feature = "node", napi(object))]
292#[derive(Debug, Clone, Serialize, Deserialize)]
293#[serde(rename_all = "camelCase")]
294pub struct EvmContractEventsByListTemplate {
295    /// Name of the pre-created contracts list.
296    pub contracts_list_name: String,
297    /// Optional name of a pre-created event-hashes list; when omitted, all
298    /// events from the listed contracts match.
299    #[serde(skip_serializing_if = "Option::is_none")]
300    pub event_hashes_list_name: Option<String>,
301}
302
303#[cfg(feature = "python")]
304#[gen_stub_pymethods]
305#[pymethods]
306impl EvmContractEventsByListTemplate {
307    #[new]
308    #[pyo3(signature = (contracts_list_name, event_hashes_list_name=None))]
309    pub fn new(contracts_list_name: String, event_hashes_list_name: Option<String>) -> Self {
310        Self {
311            contracts_list_name,
312            event_hashes_list_name,
313        }
314    }
315}
316
317/// ByList form of `EvmAbiFilterTemplate` — carries the ABI inline (the only
318/// non-list shape this template has) and optionally references a pre-created
319/// contracts list. Note the wire key is `abiJson`, distinct from the inline
320/// variant's `abi`.
321#[cfg_attr(feature = "rust", derive(Builder))]
322#[cfg_attr(feature = "python", gen_stub_pyclass)]
323#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
324#[cfg_attr(feature = "node", napi(object))]
325#[derive(Debug, Clone, Serialize, Deserialize)]
326#[serde(rename_all = "camelCase")]
327pub struct EvmAbiFilterByListTemplate {
328    /// JSON-encoded contract ABI used to decode event data.
329    pub abi_json: String,
330    /// Optional name of a pre-created contracts list; when omitted, the ABI
331    /// is applied to all contracts.
332    #[serde(skip_serializing_if = "Option::is_none")]
333    pub contracts_list_name: Option<String>,
334}
335
336#[cfg(feature = "python")]
337#[gen_stub_pymethods]
338#[pymethods]
339impl EvmAbiFilterByListTemplate {
340    #[new]
341    #[pyo3(signature = (abi_json, contracts_list_name=None))]
342    pub fn new(abi_json: String, contracts_list_name: Option<String>) -> Self {
343        Self {
344            abi_json,
345            contracts_list_name,
346        }
347    }
348}
349
350/// ByList form of `SolanaWalletFilterTemplate`.
351#[cfg_attr(feature = "rust", derive(Builder))]
352#[cfg_attr(feature = "python", gen_stub_pyclass)]
353#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
354#[cfg_attr(feature = "node", napi(object))]
355#[derive(Debug, Clone, Serialize, Deserialize)]
356#[serde(rename_all = "camelCase")]
357pub struct SolanaWalletFilterByListTemplate {
358    /// Name of the pre-created accounts list.
359    pub accounts_list_name: String,
360}
361
362#[cfg(feature = "python")]
363#[gen_stub_pymethods]
364#[pymethods]
365impl SolanaWalletFilterByListTemplate {
366    #[new]
367    pub fn new(accounts_list_name: String) -> Self {
368        Self { accounts_list_name }
369    }
370}
371
372/// ByList form of `BitcoinWalletFilterTemplate`.
373#[cfg_attr(feature = "rust", derive(Builder))]
374#[cfg_attr(feature = "python", gen_stub_pyclass)]
375#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
376#[cfg_attr(feature = "node", napi(object))]
377#[derive(Debug, Clone, Serialize, Deserialize)]
378#[serde(rename_all = "camelCase")]
379pub struct BitcoinWalletFilterByListTemplate {
380    /// Name of the pre-created wallets list.
381    pub wallets_list_name: String,
382}
383
384#[cfg(feature = "python")]
385#[gen_stub_pymethods]
386#[pymethods]
387impl BitcoinWalletFilterByListTemplate {
388    #[new]
389    pub fn new(wallets_list_name: String) -> Self {
390        Self { wallets_list_name }
391    }
392}
393
394/// ByList form of `XrplWalletFilterTemplate`.
395#[cfg_attr(feature = "rust", derive(Builder))]
396#[cfg_attr(feature = "python", gen_stub_pyclass)]
397#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
398#[cfg_attr(feature = "node", napi(object))]
399#[derive(Debug, Clone, Serialize, Deserialize)]
400#[serde(rename_all = "camelCase")]
401pub struct XrplWalletFilterByListTemplate {
402    /// Name of the pre-created wallets list.
403    pub wallets_list_name: String,
404}
405
406#[cfg(feature = "python")]
407#[gen_stub_pymethods]
408#[pymethods]
409impl XrplWalletFilterByListTemplate {
410    #[new]
411    pub fn new(wallets_list_name: String) -> Self {
412        Self { wallets_list_name }
413    }
414}
415
416/// ByList form of `HyperliquidWalletEventsFilterTemplate`.
417#[cfg_attr(feature = "rust", derive(Builder))]
418#[cfg_attr(feature = "python", gen_stub_pyclass)]
419#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
420#[cfg_attr(feature = "node", napi(object))]
421#[derive(Debug, Clone, Serialize, Deserialize)]
422#[serde(rename_all = "camelCase")]
423pub struct HyperliquidWalletEventsFilterByListTemplate {
424    /// Name of the pre-created wallets list.
425    pub wallets_list_name: String,
426}
427
428#[cfg(feature = "python")]
429#[gen_stub_pymethods]
430#[pymethods]
431impl HyperliquidWalletEventsFilterByListTemplate {
432    #[new]
433    pub fn new(wallets_list_name: String) -> Self {
434        Self { wallets_list_name }
435    }
436}
437
438/// ByList form of `StellarWalletTransactionsFilterTemplate`.
439#[cfg_attr(feature = "rust", derive(Builder))]
440#[cfg_attr(feature = "python", gen_stub_pyclass)]
441#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
442#[cfg_attr(feature = "node", napi(object))]
443#[derive(Debug, Clone, Serialize, Deserialize)]
444#[serde(rename_all = "camelCase")]
445pub struct StellarWalletTransactionsFilterByListTemplate {
446    /// Name of the pre-created wallets list.
447    pub wallets_list_name: String,
448}
449
450#[cfg(feature = "python")]
451#[gen_stub_pymethods]
452#[pymethods]
453impl StellarWalletTransactionsFilterByListTemplate {
454    #[new]
455    pub fn new(wallets_list_name: String) -> Self {
456        Self { wallets_list_name }
457    }
458}
459
460// ── Per-template Inline-or-ByList enums ────────────────────────────────────
461//
462// `#[serde(untagged)]` dispatches on field shape — inline and ByList structs
463// have disjoint field names so deserialization is unambiguous.
464
465/// `EvmWalletFilter` template arguments in either inline or by-list form.
466#[derive(Debug, Clone, Serialize, Deserialize)]
467#[serde(untagged)]
468pub enum EvmWalletFilterInput {
469    Inline(EvmWalletFilterTemplate),
470    ByList(EvmWalletFilterByListTemplate),
471}
472
473/// `EvmContractEvents` template arguments in either inline or by-list form.
474#[derive(Debug, Clone, Serialize, Deserialize)]
475#[serde(untagged)]
476pub enum EvmContractEventsInput {
477    Inline(EvmContractEventsTemplate),
478    ByList(EvmContractEventsByListTemplate),
479}
480
481/// `EvmAbiFilter` template arguments in either inline or by-list form.
482#[derive(Debug, Clone, Serialize, Deserialize)]
483#[serde(untagged)]
484pub enum EvmAbiFilterInput {
485    Inline(EvmAbiFilterTemplate),
486    ByList(EvmAbiFilterByListTemplate),
487}
488
489/// `SolanaWalletFilter` template arguments in either inline or by-list form.
490#[derive(Debug, Clone, Serialize, Deserialize)]
491#[serde(untagged)]
492pub enum SolanaWalletFilterInput {
493    Inline(SolanaWalletFilterTemplate),
494    ByList(SolanaWalletFilterByListTemplate),
495}
496
497/// `BitcoinWalletFilter` template arguments in either inline or by-list form.
498#[derive(Debug, Clone, Serialize, Deserialize)]
499#[serde(untagged)]
500pub enum BitcoinWalletFilterInput {
501    Inline(BitcoinWalletFilterTemplate),
502    ByList(BitcoinWalletFilterByListTemplate),
503}
504
505/// `XrplWalletFilter` template arguments in either inline or by-list form.
506#[derive(Debug, Clone, Serialize, Deserialize)]
507#[serde(untagged)]
508pub enum XrplWalletFilterInput {
509    Inline(XrplWalletFilterTemplate),
510    ByList(XrplWalletFilterByListTemplate),
511}
512
513/// `HyperliquidWalletEventsFilter` template arguments in either inline or by-list form.
514#[derive(Debug, Clone, Serialize, Deserialize)]
515#[serde(untagged)]
516pub enum HyperliquidWalletEventsFilterInput {
517    Inline(HyperliquidWalletEventsFilterTemplate),
518    ByList(HyperliquidWalletEventsFilterByListTemplate),
519}
520
521/// `StellarWalletTransactionsSourceAccountFilter` template arguments in
522/// either inline or by-list form.
523#[derive(Debug, Clone, Serialize, Deserialize)]
524#[serde(untagged)]
525pub enum StellarWalletTransactionsFilterInput {
526    Inline(StellarWalletTransactionsFilterTemplate),
527    ByList(StellarWalletTransactionsFilterByListTemplate),
528}
529
530// ── Template Args ──────────────────────────────────────────────────────────
531
532/// Template identifier paired with its arguments. Exactly one variant selects
533/// which filter is applied; each variant's inner enum picks between inline
534/// values and a list reference. Consumed by `create_webhook_from_template`
535/// and `update_webhook_template`.
536// Pure-Rust discriminated union; no #[pyclass] / #[napi(object)] because PyO3
537// and napi-rs cannot represent enum-with-data. Each language binding crate
538// wraps this type for its own FFI surface.
539// The serde tag/content pair matches the API wire format when flattened into
540// a request struct.
541#[derive(Debug, Clone, Serialize, Deserialize)]
542#[serde(tag = "templateId", content = "templateArgs", rename_all = "camelCase")]
543pub enum TemplateArgs {
544    /// EVM wallet filter.
545    EvmWalletFilter(EvmWalletFilterInput),
546    /// EVM contract events filter.
547    EvmContractEvents(EvmContractEventsInput),
548    /// EVM ABI filter.
549    EvmAbiFilter(EvmAbiFilterInput),
550    /// Solana wallet filter.
551    SolanaWalletFilter(SolanaWalletFilterInput),
552    /// Bitcoin wallet filter.
553    BitcoinWalletFilter(BitcoinWalletFilterInput),
554    /// XRPL wallet filter.
555    XrplWalletFilter(XrplWalletFilterInput),
556    /// Hyperliquid wallet-events filter.
557    HyperliquidWalletEventsFilter(HyperliquidWalletEventsFilterInput),
558    /// Stellar wallet-transactions filter (source-account match).
559    StellarWalletTransactionsSourceAccountFilter(StellarWalletTransactionsFilterInput),
560}
561
562impl TemplateArgs {
563    pub fn tag(&self) -> WebhookTemplateId {
564        match self {
565            Self::EvmWalletFilter(_) => WebhookTemplateId::EvmWalletFilter,
566            Self::EvmContractEvents(_) => WebhookTemplateId::EvmContractEvents,
567            Self::EvmAbiFilter(_) => WebhookTemplateId::EvmAbiFilter,
568            Self::SolanaWalletFilter(_) => WebhookTemplateId::SolanaWalletFilter,
569            Self::BitcoinWalletFilter(_) => WebhookTemplateId::BitcoinWalletFilter,
570            Self::XrplWalletFilter(_) => WebhookTemplateId::XrplWalletFilter,
571            Self::HyperliquidWalletEventsFilter(_) => {
572                WebhookTemplateId::HyperliquidWalletEventsFilter
573            }
574            Self::StellarWalletTransactionsSourceAccountFilter(_) => {
575                WebhookTemplateId::StellarWalletTransactionsSourceAccountFilter
576            }
577        }
578    }
579}
580
581// ── Webhook Destination Attributes ─────────────────────────────────────────
582
583/// Destination configuration for a webhook.
584#[cfg_attr(feature = "python", gen_stub_pyclass)]
585#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
586#[cfg_attr(feature = "node", napi(object))]
587#[derive(Debug, Clone, Serialize, Deserialize)]
588pub struct WebhookDestinationAttributes {
589    /// Target URL that receives webhook payloads.
590    pub url: String,
591    /// Optional token sent with each payload so the receiver can verify authenticity; generated automatically when omitted.
592    #[serde(skip_serializing_if = "Option::is_none")]
593    pub security_token: Option<String>,
594    /// Payload compression (`gzip` or `none`).
595    pub compression: String,
596}
597
598#[cfg(feature = "python")]
599#[gen_stub_pymethods]
600#[pymethods]
601impl WebhookDestinationAttributes {
602    #[new]
603    #[pyo3(signature = (url, compression, security_token=None))]
604    pub fn new(url: String, compression: String, security_token: Option<String>) -> Self {
605        Self {
606            url,
607            security_token,
608            compression,
609        }
610    }
611}
612
613// ── Request Types ──────────────────────────────────────────────────────────
614
615/// Parameters for `list_webhooks`.
616#[cfg_attr(feature = "python", gen_stub_pyclass)]
617#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
618#[cfg_attr(feature = "node", napi(object))]
619#[cfg_attr(not(feature = "node"), derive(Clone))]
620#[derive(Debug, Default, Serialize, Deserialize)]
621pub struct GetWebhooksParams {
622    /// Maximum number of webhooks returned.
623    #[serde(skip_serializing_if = "Option::is_none")]
624    pub limit: Option<i64>,
625    /// Starting index into the result set.
626    #[serde(skip_serializing_if = "Option::is_none")]
627    pub offset: Option<i64>,
628}
629
630#[cfg(feature = "python")]
631#[gen_stub_pymethods]
632#[pymethods]
633impl GetWebhooksParams {
634    #[new]
635    #[pyo3(signature = (limit=None, offset=None))]
636    pub fn new(limit: Option<i64>, offset: Option<i64>) -> Self {
637        Self { limit, offset }
638    }
639}
640
641/// Parameters for `update_webhook`. All fields are optional; only set fields
642/// are modified.
643#[cfg_attr(feature = "python", gen_stub_pyclass)]
644#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
645#[cfg_attr(feature = "node", napi(object))]
646#[cfg_attr(not(feature = "node"), derive(Clone))]
647#[derive(Debug, Default, Serialize, Deserialize)]
648pub struct UpdateWebhookParams {
649    /// New human-readable name.
650    #[serde(skip_serializing_if = "Option::is_none")]
651    pub name: Option<String>,
652    /// New notification email.
653    #[serde(skip_serializing_if = "Option::is_none")]
654    pub notification_email: Option<String>,
655    /// New destination configuration.
656    #[serde(skip_serializing_if = "Option::is_none")]
657    pub destination_attributes: Option<WebhookDestinationAttributes>,
658}
659
660#[cfg(feature = "python")]
661#[gen_stub_pymethods]
662#[pymethods]
663impl UpdateWebhookParams {
664    #[new]
665    #[pyo3(signature = (name=None, notification_email=None, destination_attributes=None))]
666    pub fn new(
667        name: Option<String>,
668        notification_email: Option<String>,
669        destination_attributes: Option<WebhookDestinationAttributes>,
670    ) -> Self {
671        Self {
672            name,
673            notification_email,
674            destination_attributes,
675        }
676    }
677}
678
679/// Parameters for `activate_webhook`.
680#[cfg_attr(feature = "node", napi(object))]
681#[cfg_attr(not(feature = "node"), derive(Clone))]
682#[derive(Debug, Serialize, Deserialize)]
683#[serde(rename_all = "camelCase")]
684pub struct ActivateWebhookParams {
685    /// Position to begin (or resume) delivery from.
686    pub start_from: WebhookStartFrom,
687}
688
689/// Parameters for `create_webhook_from_template`.
690#[cfg_attr(feature = "rust", derive(Builder))]
691#[derive(Debug, Clone, Serialize, Deserialize)]
692pub struct CreateWebhookFromTemplateParams {
693    /// Human-readable label for the webhook.
694    pub name: String,
695    /// Blockchain network to watch (e.g. `ethereum-mainnet`).
696    pub network: String,
697    /// Optional email that receives alerts if the webhook terminates.
698    #[serde(skip_serializing_if = "Option::is_none")]
699    pub notification_email: Option<String>,
700    /// Destination configuration for delivered payloads.
701    pub destination_attributes: WebhookDestinationAttributes,
702    /// Filter template identifier and its arguments.
703    // Flattening the enum's tag/content produces { templateId, templateArgs }.
704    #[serde(flatten)]
705    pub template_args: TemplateArgs,
706}
707
708/// Parameters for `update_webhook_template`.
709#[derive(Debug, Clone, Serialize, Deserialize)]
710pub struct UpdateWebhookTemplateParams {
711    /// New human-readable name.
712    #[serde(skip_serializing_if = "Option::is_none")]
713    pub name: Option<String>,
714    /// New notification email.
715    #[serde(skip_serializing_if = "Option::is_none")]
716    pub notification_email: Option<String>,
717    /// New destination configuration.
718    #[serde(skip_serializing_if = "Option::is_none")]
719    pub destination_attributes: Option<WebhookDestinationAttributes>,
720    /// New template identifier and arguments.
721    // Flattening the enum's tag/content produces { templateId, templateArgs }.
722    #[serde(flatten)]
723    pub template_args: TemplateArgs,
724}
725
726// ── Response Types ─────────────────────────────────────────────────────────
727
728/// A webhook's full configuration and current state.
729#[cfg_attr(feature = "python", gen_stub_pyclass)]
730#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
731#[cfg_attr(feature = "node", napi(object))]
732#[derive(Debug, Clone, Serialize, Deserialize)]
733pub struct Webhook {
734    /// Unique webhook identifier.
735    pub id: String,
736    /// Human-readable webhook name.
737    pub name: String,
738    /// Current operational state (e.g. `active`, `paused`).
739    pub status: String,
740    /// Blockchain network the webhook is watching.
741    pub network: String,
742    /// Timestamp when the webhook was created.
743    pub created_at: String,
744    /// Timestamp of the most recent modification.
745    #[serde(default, skip_serializing_if = "Option::is_none")]
746    pub updated_at: Option<String>,
747    /// Template identifier used to create the webhook, if any.
748    #[serde(skip_serializing_if = "Option::is_none")]
749    pub template_id: Option<String>,
750    /// Email address notified of webhook terminations or failures.
751    #[serde(skip_serializing_if = "Option::is_none")]
752    pub notification_email: Option<String>,
753    /// Destination-specific configuration as a JSON string.
754    #[serde(
755        default,
756        skip_serializing_if = "Option::is_none",
757        deserialize_with = "deserialize_as_optional_json_string"
758    )]
759    pub destination_attributes: Option<String>,
760}
761
762/// Pagination metadata returned alongside a paginated webhooks list.
763#[cfg_attr(feature = "python", gen_stub_pyclass)]
764#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
765#[cfg_attr(feature = "node", napi(object))]
766#[derive(Debug, Clone, Serialize, Deserialize)]
767pub struct WebhookPageInfo {
768    /// Page size used for this response.
769    pub limit: i64,
770    /// Starting index of this page within the full result set.
771    pub offset: i64,
772    /// Total number of webhooks matching the query across all pages.
773    pub total: i64,
774}
775
776/// Response from `list_webhooks`.
777#[cfg_attr(feature = "python", gen_stub_pyclass)]
778#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
779#[cfg_attr(feature = "node", napi(object))]
780#[derive(Debug, Clone, Serialize, Deserialize)]
781pub struct ListWebhooksResponse {
782    /// Webhooks on the current page.
783    pub data: Vec<Webhook>,
784    /// Pagination metadata for the response.
785    #[serde(rename = "pageInfo")]
786    pub page_info: WebhookPageInfo,
787}
788
789/// Response from `get_enabled_count` for webhooks.
790#[cfg_attr(feature = "python", gen_stub_pyclass)]
791#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
792#[cfg_attr(feature = "node", napi(object))]
793#[derive(Debug, Clone, Serialize, Deserialize)]
794pub struct WebhookEnabledCountResponse {
795    /// Total count of enabled webhooks on the account.
796    pub total: i64,
797}
798
799#[cfg(test)]
800#[allow(clippy::unwrap_used)]
801mod template_args_tests {
802    use super::*;
803
804    #[test]
805    fn evm_wallet_filter_roundtrip() {
806        let args =
807            TemplateArgs::EvmWalletFilter(EvmWalletFilterInput::Inline(EvmWalletFilterTemplate {
808                wallets: vec!["0xabc".to_string()],
809            }));
810        let json = serde_json::to_string(&args).unwrap();
811        assert!(json.contains(r#""templateId":"evmWalletFilter""#));
812        assert!(json.contains(r#""wallets":["0xabc"]"#));
813        let parsed: TemplateArgs = serde_json::from_str(&json).unwrap();
814        assert!(matches!(
815            parsed,
816            TemplateArgs::EvmWalletFilter(EvmWalletFilterInput::Inline(_))
817        ));
818        assert!(matches!(parsed.tag(), WebhookTemplateId::EvmWalletFilter));
819    }
820
821    #[test]
822    fn evm_wallet_filter_by_list_roundtrip() {
823        let args = TemplateArgs::EvmWalletFilter(EvmWalletFilterInput::ByList(
824            EvmWalletFilterByListTemplate {
825                wallets_list_name: "my_list".to_string(),
826            },
827        ));
828        let json = serde_json::to_string(&args).unwrap();
829        assert!(json.contains(r#""templateId":"evmWalletFilter""#));
830        assert!(json.contains(r#""walletsListName":"my_list""#));
831        assert!(!json.contains(r#""wallets":"#));
832        let parsed: TemplateArgs = serde_json::from_str(&json).unwrap();
833        assert!(matches!(
834            parsed,
835            TemplateArgs::EvmWalletFilter(EvmWalletFilterInput::ByList(_))
836        ));
837    }
838
839    #[test]
840    fn evm_contract_events_roundtrip() {
841        let args = TemplateArgs::EvmContractEvents(EvmContractEventsInput::Inline(
842            EvmContractEventsTemplate {
843                contracts: vec!["0xdef".to_string()],
844                event_hashes: vec!["0x1234".to_string()],
845            },
846        ));
847        let json = serde_json::to_string(&args).unwrap();
848        assert!(json.contains(r#""templateId":"evmContractEvents""#));
849        // API expects camelCase `eventHashes` — snake_case is silently rejected with a 500.
850        assert!(json.contains(r#""eventHashes":["0x1234"]"#));
851        assert!(!json.contains("event_hashes"));
852        let parsed: TemplateArgs = serde_json::from_str(&json).unwrap();
853        assert!(matches!(
854            parsed,
855            TemplateArgs::EvmContractEvents(EvmContractEventsInput::Inline(_))
856        ));
857    }
858
859    #[test]
860    fn evm_contract_events_by_list_roundtrip() {
861        // The ByList variant lets event_hashes_list_name be omitted entirely.
862        let args = TemplateArgs::EvmContractEvents(EvmContractEventsInput::ByList(
863            EvmContractEventsByListTemplate {
864                contracts_list_name: "my_contracts".to_string(),
865                event_hashes_list_name: None,
866            },
867        ));
868        let json = serde_json::to_string(&args).unwrap();
869        assert!(json.contains(r#""contractsListName":"my_contracts""#));
870        assert!(!json.contains("eventHashesListName"));
871        let parsed: TemplateArgs = serde_json::from_str(&json).unwrap();
872        assert!(matches!(
873            parsed,
874            TemplateArgs::EvmContractEvents(EvmContractEventsInput::ByList(_))
875        ));
876    }
877
878    #[test]
879    fn evm_abi_filter_roundtrip() {
880        let args = TemplateArgs::EvmAbiFilter(EvmAbiFilterInput::Inline(EvmAbiFilterTemplate {
881            abi: "[]".to_string(),
882            contracts: vec!["0xdef".to_string()],
883        }));
884        let json = serde_json::to_string(&args).unwrap();
885        assert!(json.contains(r#""templateId":"evmAbiFilter""#));
886        let parsed: TemplateArgs = serde_json::from_str(&json).unwrap();
887        assert!(matches!(parsed, TemplateArgs::EvmAbiFilter(_)));
888    }
889
890    #[test]
891    fn evm_abi_filter_by_list_roundtrip() {
892        // The ByList variant uses `abiJson` (distinct from inline's `abi`) and
893        // an optional `contractsListName`.
894        let args =
895            TemplateArgs::EvmAbiFilter(EvmAbiFilterInput::ByList(EvmAbiFilterByListTemplate {
896                abi_json: "[]".to_string(),
897                contracts_list_name: Some("my_contracts".to_string()),
898            }));
899        let json = serde_json::to_string(&args).unwrap();
900        assert!(json.contains(r#""abiJson":"[]""#));
901        assert!(json.contains(r#""contractsListName":"my_contracts""#));
902        assert!(!json.contains(r#""abi":"#));
903        let parsed: TemplateArgs = serde_json::from_str(&json).unwrap();
904        assert!(matches!(
905            parsed,
906            TemplateArgs::EvmAbiFilter(EvmAbiFilterInput::ByList(_))
907        ));
908    }
909
910    #[test]
911    fn solana_wallet_filter_roundtrip() {
912        let args = TemplateArgs::SolanaWalletFilter(SolanaWalletFilterInput::Inline(
913            SolanaWalletFilterTemplate {
914                accounts: vec!["acc".to_string()],
915            },
916        ));
917        let json = serde_json::to_string(&args).unwrap();
918        assert!(json.contains(r#""templateId":"solanaWalletFilter""#));
919        let parsed: TemplateArgs = serde_json::from_str(&json).unwrap();
920        assert!(matches!(parsed, TemplateArgs::SolanaWalletFilter(_)));
921    }
922
923    #[test]
924    fn solana_wallet_filter_by_list_roundtrip() {
925        let args = TemplateArgs::SolanaWalletFilter(SolanaWalletFilterInput::ByList(
926            SolanaWalletFilterByListTemplate {
927                accounts_list_name: "my_accounts".to_string(),
928            },
929        ));
930        let json = serde_json::to_string(&args).unwrap();
931        assert!(json.contains(r#""accountsListName":"my_accounts""#));
932        assert!(!json.contains(r#""accounts":"#));
933    }
934
935    #[test]
936    fn bitcoin_wallet_filter_roundtrip() {
937        let args = TemplateArgs::BitcoinWalletFilter(BitcoinWalletFilterInput::Inline(
938            BitcoinWalletFilterTemplate {
939                wallets: vec!["bc1".to_string()],
940            },
941        ));
942        let json = serde_json::to_string(&args).unwrap();
943        assert!(json.contains(r#""templateId":"bitcoinWalletFilter""#));
944        let parsed: TemplateArgs = serde_json::from_str(&json).unwrap();
945        assert!(matches!(parsed, TemplateArgs::BitcoinWalletFilter(_)));
946    }
947
948    #[test]
949    fn bitcoin_wallet_filter_by_list_roundtrip() {
950        let args = TemplateArgs::BitcoinWalletFilter(BitcoinWalletFilterInput::ByList(
951            BitcoinWalletFilterByListTemplate {
952                wallets_list_name: "my_btc_list".to_string(),
953            },
954        ));
955        let json = serde_json::to_string(&args).unwrap();
956        assert!(json.contains(r#""walletsListName":"my_btc_list""#));
957    }
958
959    #[test]
960    fn xrpl_wallet_filter_roundtrip() {
961        let args = TemplateArgs::XrplWalletFilter(XrplWalletFilterInput::Inline(
962            XrplWalletFilterTemplate {
963                wallets: vec!["r1".to_string()],
964            },
965        ));
966        let json = serde_json::to_string(&args).unwrap();
967        assert!(json.contains(r#""templateId":"xrplWalletFilter""#));
968        let parsed: TemplateArgs = serde_json::from_str(&json).unwrap();
969        assert!(matches!(parsed, TemplateArgs::XrplWalletFilter(_)));
970    }
971
972    #[test]
973    fn xrpl_wallet_filter_by_list_roundtrip() {
974        let args = TemplateArgs::XrplWalletFilter(XrplWalletFilterInput::ByList(
975            XrplWalletFilterByListTemplate {
976                wallets_list_name: "my_xrpl_list".to_string(),
977            },
978        ));
979        let json = serde_json::to_string(&args).unwrap();
980        assert!(json.contains(r#""walletsListName":"my_xrpl_list""#));
981    }
982
983    #[test]
984    fn hyperliquid_wallet_events_filter_roundtrip() {
985        let args = TemplateArgs::HyperliquidWalletEventsFilter(
986            HyperliquidWalletEventsFilterInput::Inline(HyperliquidWalletEventsFilterTemplate {
987                wallets: vec!["0xhl".to_string()],
988            }),
989        );
990        let json = serde_json::to_string(&args).unwrap();
991        assert!(json.contains(r#""templateId":"hyperliquidWalletEventsFilter""#));
992        let parsed: TemplateArgs = serde_json::from_str(&json).unwrap();
993        assert!(matches!(
994            parsed,
995            TemplateArgs::HyperliquidWalletEventsFilter(_)
996        ));
997    }
998
999    #[test]
1000    fn hyperliquid_wallet_events_filter_by_list_roundtrip() {
1001        let args = TemplateArgs::HyperliquidWalletEventsFilter(
1002            HyperliquidWalletEventsFilterInput::ByList(
1003                HyperliquidWalletEventsFilterByListTemplate {
1004                    wallets_list_name: "my_hl_list".to_string(),
1005                },
1006            ),
1007        );
1008        let json = serde_json::to_string(&args).unwrap();
1009        assert!(json.contains(r#""walletsListName":"my_hl_list""#));
1010    }
1011
1012    #[test]
1013    fn stellar_wallet_transactions_filter_roundtrip() {
1014        let args = TemplateArgs::StellarWalletTransactionsSourceAccountFilter(
1015            StellarWalletTransactionsFilterInput::Inline(StellarWalletTransactionsFilterTemplate {
1016                wallets: vec!["G...".to_string()],
1017            }),
1018        );
1019        let json = serde_json::to_string(&args).unwrap();
1020        assert!(json.contains(r#""templateId":"stellarWalletTransactionsSourceAccountFilter""#));
1021        let parsed: TemplateArgs = serde_json::from_str(&json).unwrap();
1022        assert!(matches!(
1023            parsed,
1024            TemplateArgs::StellarWalletTransactionsSourceAccountFilter(_)
1025        ));
1026    }
1027
1028    #[test]
1029    fn stellar_wallet_transactions_filter_by_list_roundtrip() {
1030        let args = TemplateArgs::StellarWalletTransactionsSourceAccountFilter(
1031            StellarWalletTransactionsFilterInput::ByList(
1032                StellarWalletTransactionsFilterByListTemplate {
1033                    wallets_list_name: "my_stellar_list".to_string(),
1034                },
1035            ),
1036        );
1037        let json = serde_json::to_string(&args).unwrap();
1038        assert!(json.contains(r#""walletsListName":"my_stellar_list""#));
1039    }
1040
1041    #[test]
1042    fn create_params_flattens_template_args() {
1043        let params = CreateWebhookFromTemplateParams {
1044            name: "n".to_string(),
1045            network: "ethereum-mainnet".to_string(),
1046            notification_email: None,
1047            destination_attributes: WebhookDestinationAttributes {
1048                url: "https://x".to_string(),
1049                security_token: None,
1050                compression: "none".to_string(),
1051            },
1052            template_args: TemplateArgs::EvmWalletFilter(EvmWalletFilterInput::Inline(
1053                EvmWalletFilterTemplate {
1054                    wallets: vec!["0xabc".to_string()],
1055                },
1056            )),
1057        };
1058        let json = serde_json::to_value(&params).unwrap();
1059        let obj = json.as_object().unwrap();
1060        assert_eq!(
1061            obj.get("templateId").and_then(|v| v.as_str()),
1062            Some("evmWalletFilter")
1063        );
1064        assert!(obj.get("templateArgs").unwrap().is_object());
1065        assert_eq!(obj["templateArgs"]["wallets"][0].as_str(), Some("0xabc"));
1066    }
1067}