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#[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#[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 Last,
67 Latest,
69}
70
71#[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 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#[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 pub contracts: Vec<String>,
106 #[serde(skip_serializing_if = "Option::is_none")]
108 pub event_hashes: Option<Vec<String>>,
109}
110
111#[cfg(feature = "python")]
112#[gen_stub_pymethods]
113#[pymethods]
114impl EvmContractEventsTemplate {
115 #[new]
116 #[pyo3(signature = (contracts, event_hashes=None))]
117 pub fn new(contracts: Vec<String>, event_hashes: Option<Vec<String>>) -> Self {
118 Self {
119 contracts,
120 event_hashes,
121 }
122 }
123}
124
125#[cfg_attr(feature = "rust", derive(Builder))]
128#[cfg_attr(feature = "python", gen_stub_pyclass)]
129#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
130#[cfg_attr(feature = "node", napi(object))]
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct EvmAbiFilterTemplate {
133 pub abi: String,
135 pub contracts: Vec<String>,
137}
138
139#[cfg(feature = "python")]
140#[gen_stub_pymethods]
141#[pymethods]
142impl EvmAbiFilterTemplate {
143 #[new]
144 pub fn new(abi: String, contracts: Vec<String>) -> Self {
145 Self { abi, contracts }
146 }
147}
148
149#[cfg_attr(feature = "rust", derive(Builder))]
152#[cfg_attr(feature = "python", gen_stub_pyclass)]
153#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
154#[cfg_attr(feature = "node", napi(object))]
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct SolanaWalletFilterTemplate {
157 pub accounts: Vec<String>,
159}
160
161#[cfg(feature = "python")]
162#[gen_stub_pymethods]
163#[pymethods]
164impl SolanaWalletFilterTemplate {
165 #[new]
166 pub fn new(accounts: Vec<String>) -> Self {
167 Self { accounts }
168 }
169}
170
171#[cfg_attr(feature = "rust", derive(Builder))]
173#[cfg_attr(feature = "python", gen_stub_pyclass)]
174#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
175#[cfg_attr(feature = "node", napi(object))]
176#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct BitcoinWalletFilterTemplate {
178 pub wallets: Vec<String>,
180}
181
182#[cfg(feature = "python")]
183#[gen_stub_pymethods]
184#[pymethods]
185impl BitcoinWalletFilterTemplate {
186 #[new]
187 pub fn new(wallets: Vec<String>) -> Self {
188 Self { wallets }
189 }
190}
191
192#[cfg_attr(feature = "rust", derive(Builder))]
194#[cfg_attr(feature = "python", gen_stub_pyclass)]
195#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
196#[cfg_attr(feature = "node", napi(object))]
197#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct XrplWalletFilterTemplate {
199 pub wallets: Vec<String>,
201}
202
203#[cfg(feature = "python")]
204#[gen_stub_pymethods]
205#[pymethods]
206impl XrplWalletFilterTemplate {
207 #[new]
208 pub fn new(wallets: Vec<String>) -> Self {
209 Self { wallets }
210 }
211}
212
213#[cfg_attr(feature = "rust", derive(Builder))]
215#[cfg_attr(feature = "python", gen_stub_pyclass)]
216#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
217#[cfg_attr(feature = "node", napi(object))]
218#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct HyperliquidWalletEventsFilterTemplate {
220 pub wallets: Vec<String>,
222}
223
224#[cfg(feature = "python")]
225#[gen_stub_pymethods]
226#[pymethods]
227impl HyperliquidWalletEventsFilterTemplate {
228 #[new]
229 pub fn new(wallets: Vec<String>) -> Self {
230 Self { wallets }
231 }
232}
233
234#[cfg_attr(feature = "rust", derive(Builder))]
237#[cfg_attr(feature = "python", gen_stub_pyclass)]
238#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
239#[cfg_attr(feature = "node", napi(object))]
240#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct StellarWalletTransactionsFilterTemplate {
242 pub wallets: Vec<String>,
244}
245
246#[cfg(feature = "python")]
247#[gen_stub_pymethods]
248#[pymethods]
249impl StellarWalletTransactionsFilterTemplate {
250 #[new]
251 pub fn new(wallets: Vec<String>) -> Self {
252 Self { wallets }
253 }
254}
255
256#[derive(Debug, Clone, Serialize, Deserialize)]
267#[serde(tag = "templateId", content = "templateArgs", rename_all = "camelCase")]
268pub enum TemplateArgs {
269 EvmWalletFilter(EvmWalletFilterTemplate),
271 EvmContractEvents(EvmContractEventsTemplate),
273 EvmAbiFilter(EvmAbiFilterTemplate),
275 SolanaWalletFilter(SolanaWalletFilterTemplate),
277 BitcoinWalletFilter(BitcoinWalletFilterTemplate),
279 XrplWalletFilter(XrplWalletFilterTemplate),
281 HyperliquidWalletEventsFilter(HyperliquidWalletEventsFilterTemplate),
283 StellarWalletTransactionsSourceAccountFilter(StellarWalletTransactionsFilterTemplate),
285}
286
287impl TemplateArgs {
288 pub fn tag(&self) -> WebhookTemplateId {
289 match self {
290 Self::EvmWalletFilter(_) => WebhookTemplateId::EvmWalletFilter,
291 Self::EvmContractEvents(_) => WebhookTemplateId::EvmContractEvents,
292 Self::EvmAbiFilter(_) => WebhookTemplateId::EvmAbiFilter,
293 Self::SolanaWalletFilter(_) => WebhookTemplateId::SolanaWalletFilter,
294 Self::BitcoinWalletFilter(_) => WebhookTemplateId::BitcoinWalletFilter,
295 Self::XrplWalletFilter(_) => WebhookTemplateId::XrplWalletFilter,
296 Self::HyperliquidWalletEventsFilter(_) => {
297 WebhookTemplateId::HyperliquidWalletEventsFilter
298 }
299 Self::StellarWalletTransactionsSourceAccountFilter(_) => {
300 WebhookTemplateId::StellarWalletTransactionsSourceAccountFilter
301 }
302 }
303 }
304}
305
306#[cfg_attr(feature = "python", gen_stub_pyclass)]
310#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
311#[cfg_attr(feature = "node", napi(object))]
312#[derive(Debug, Clone, Serialize, Deserialize)]
313pub struct WebhookDestinationAttributes {
314 pub url: String,
316 #[serde(skip_serializing_if = "Option::is_none")]
318 pub security_token: Option<String>,
319 #[serde(skip_serializing_if = "Option::is_none")]
321 pub compression: Option<String>,
322}
323
324#[cfg(feature = "python")]
325#[gen_stub_pymethods]
326#[pymethods]
327impl WebhookDestinationAttributes {
328 #[new]
329 #[pyo3(signature = (url, security_token=None, compression=None))]
330 pub fn new(url: String, security_token: Option<String>, compression: Option<String>) -> Self {
331 Self {
332 url,
333 security_token,
334 compression,
335 }
336 }
337}
338
339#[cfg_attr(feature = "python", gen_stub_pyclass)]
343#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
344#[cfg_attr(feature = "node", napi(object))]
345#[cfg_attr(not(feature = "node"), derive(Clone))]
346#[derive(Debug, Default, Serialize, Deserialize)]
347pub struct GetWebhooksParams {
348 #[serde(skip_serializing_if = "Option::is_none")]
350 pub limit: Option<i64>,
351 #[serde(skip_serializing_if = "Option::is_none")]
353 pub offset: Option<i64>,
354}
355
356#[cfg(feature = "python")]
357#[gen_stub_pymethods]
358#[pymethods]
359impl GetWebhooksParams {
360 #[new]
361 #[pyo3(signature = (limit=None, offset=None))]
362 pub fn new(limit: Option<i64>, offset: Option<i64>) -> Self {
363 Self { limit, offset }
364 }
365}
366
367#[cfg_attr(feature = "python", gen_stub_pyclass)]
370#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
371#[cfg_attr(feature = "node", napi(object))]
372#[cfg_attr(not(feature = "node"), derive(Clone))]
373#[derive(Debug, Default, Serialize, Deserialize)]
374pub struct UpdateWebhookParams {
375 #[serde(skip_serializing_if = "Option::is_none")]
377 pub name: Option<String>,
378 #[serde(skip_serializing_if = "Option::is_none")]
380 pub notification_email: Option<String>,
381 #[serde(skip_serializing_if = "Option::is_none")]
383 pub destination_attributes: Option<WebhookDestinationAttributes>,
384}
385
386#[cfg(feature = "python")]
387#[gen_stub_pymethods]
388#[pymethods]
389impl UpdateWebhookParams {
390 #[new]
391 #[pyo3(signature = (name=None, notification_email=None, destination_attributes=None))]
392 pub fn new(
393 name: Option<String>,
394 notification_email: Option<String>,
395 destination_attributes: Option<WebhookDestinationAttributes>,
396 ) -> Self {
397 Self {
398 name,
399 notification_email,
400 destination_attributes,
401 }
402 }
403}
404
405#[cfg_attr(feature = "node", napi(object))]
407#[cfg_attr(not(feature = "node"), derive(Clone))]
408#[derive(Debug, Serialize, Deserialize)]
409#[serde(rename_all = "camelCase")]
410pub struct ActivateWebhookParams {
411 pub start_from: WebhookStartFrom,
413}
414
415#[cfg_attr(feature = "rust", derive(Builder))]
417#[derive(Debug, Clone, Serialize, Deserialize)]
418pub struct CreateWebhookFromTemplateParams {
419 pub name: String,
421 pub network: String,
423 #[serde(skip_serializing_if = "Option::is_none")]
425 pub notification_email: Option<String>,
426 pub destination_attributes: WebhookDestinationAttributes,
428 #[serde(flatten)]
431 pub template_args: TemplateArgs,
432}
433
434#[derive(Debug, Clone, Serialize, Deserialize)]
436pub struct UpdateWebhookTemplateParams {
437 #[serde(skip_serializing_if = "Option::is_none")]
439 pub name: Option<String>,
440 #[serde(skip_serializing_if = "Option::is_none")]
442 pub notification_email: Option<String>,
443 #[serde(skip_serializing_if = "Option::is_none")]
445 pub destination_attributes: Option<WebhookDestinationAttributes>,
446 #[serde(flatten)]
449 pub template_args: TemplateArgs,
450}
451
452#[cfg_attr(feature = "python", gen_stub_pyclass)]
456#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
457#[cfg_attr(feature = "node", napi(object))]
458#[derive(Debug, Clone, Serialize, Deserialize)]
459pub struct Webhook {
460 pub id: String,
462 pub name: String,
464 pub status: String,
466 pub network: String,
468 pub created_at: String,
470 #[serde(default, skip_serializing_if = "Option::is_none")]
472 pub updated_at: Option<String>,
473 #[serde(skip_serializing_if = "Option::is_none")]
475 pub template_id: Option<String>,
476 #[serde(skip_serializing_if = "Option::is_none")]
478 pub notification_email: Option<String>,
479 #[serde(
481 default,
482 skip_serializing_if = "Option::is_none",
483 deserialize_with = "deserialize_as_optional_json_string"
484 )]
485 pub destination_attributes: Option<String>,
486}
487
488#[cfg_attr(feature = "python", gen_stub_pyclass)]
490#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
491#[cfg_attr(feature = "node", napi(object))]
492#[derive(Debug, Clone, Serialize, Deserialize)]
493pub struct WebhookPageInfo {
494 pub limit: i64,
496 pub offset: i64,
498 pub total: i64,
500}
501
502#[cfg_attr(feature = "python", gen_stub_pyclass)]
504#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
505#[cfg_attr(feature = "node", napi(object))]
506#[derive(Debug, Clone, Serialize, Deserialize)]
507pub struct ListWebhooksResponse {
508 pub data: Vec<Webhook>,
510 #[serde(rename = "pageInfo")]
512 pub page_info: WebhookPageInfo,
513}
514
515#[cfg_attr(feature = "python", gen_stub_pyclass)]
517#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
518#[cfg_attr(feature = "node", napi(object))]
519#[derive(Debug, Clone, Serialize, Deserialize)]
520pub struct WebhookEnabledCountResponse {
521 pub total: i64,
523}
524
525#[cfg(test)]
526#[allow(clippy::unwrap_used)]
527mod template_args_tests {
528 use super::*;
529
530 #[test]
531 fn evm_wallet_filter_roundtrip() {
532 let args = TemplateArgs::EvmWalletFilter(EvmWalletFilterTemplate {
533 wallets: vec!["0xabc".to_string()],
534 });
535 let json = serde_json::to_string(&args).unwrap();
536 assert!(json.contains(r#""templateId":"evmWalletFilter""#));
537 assert!(json.contains(r#""wallets":["0xabc"]"#));
538 let parsed: TemplateArgs = serde_json::from_str(&json).unwrap();
539 assert!(matches!(parsed, TemplateArgs::EvmWalletFilter(_)));
540 assert!(matches!(parsed.tag(), WebhookTemplateId::EvmWalletFilter));
541 }
542
543 #[test]
544 fn evm_contract_events_roundtrip() {
545 let args = TemplateArgs::EvmContractEvents(EvmContractEventsTemplate {
546 contracts: vec!["0xdef".to_string()],
547 event_hashes: Some(vec!["0x1234".to_string()]),
548 });
549 let json = serde_json::to_string(&args).unwrap();
550 assert!(json.contains(r#""templateId":"evmContractEvents""#));
551 assert!(json.contains(r#""eventHashes":["0x1234"]"#));
553 assert!(!json.contains("event_hashes"));
554 let parsed: TemplateArgs = serde_json::from_str(&json).unwrap();
555 assert!(matches!(parsed, TemplateArgs::EvmContractEvents(_)));
556 }
557
558 #[test]
559 fn evm_abi_filter_roundtrip() {
560 let args = TemplateArgs::EvmAbiFilter(EvmAbiFilterTemplate {
561 abi: "[]".to_string(),
562 contracts: vec!["0xdef".to_string()],
563 });
564 let json = serde_json::to_string(&args).unwrap();
565 assert!(json.contains(r#""templateId":"evmAbiFilter""#));
566 let parsed: TemplateArgs = serde_json::from_str(&json).unwrap();
567 assert!(matches!(parsed, TemplateArgs::EvmAbiFilter(_)));
568 }
569
570 #[test]
571 fn solana_wallet_filter_roundtrip() {
572 let args = TemplateArgs::SolanaWalletFilter(SolanaWalletFilterTemplate {
573 accounts: vec!["acc".to_string()],
574 });
575 let json = serde_json::to_string(&args).unwrap();
576 assert!(json.contains(r#""templateId":"solanaWalletFilter""#));
577 let parsed: TemplateArgs = serde_json::from_str(&json).unwrap();
578 assert!(matches!(parsed, TemplateArgs::SolanaWalletFilter(_)));
579 }
580
581 #[test]
582 fn bitcoin_wallet_filter_roundtrip() {
583 let args = TemplateArgs::BitcoinWalletFilter(BitcoinWalletFilterTemplate {
584 wallets: vec!["bc1".to_string()],
585 });
586 let json = serde_json::to_string(&args).unwrap();
587 assert!(json.contains(r#""templateId":"bitcoinWalletFilter""#));
588 let parsed: TemplateArgs = serde_json::from_str(&json).unwrap();
589 assert!(matches!(parsed, TemplateArgs::BitcoinWalletFilter(_)));
590 }
591
592 #[test]
593 fn xrpl_wallet_filter_roundtrip() {
594 let args = TemplateArgs::XrplWalletFilter(XrplWalletFilterTemplate {
595 wallets: vec!["r1".to_string()],
596 });
597 let json = serde_json::to_string(&args).unwrap();
598 assert!(json.contains(r#""templateId":"xrplWalletFilter""#));
599 let parsed: TemplateArgs = serde_json::from_str(&json).unwrap();
600 assert!(matches!(parsed, TemplateArgs::XrplWalletFilter(_)));
601 }
602
603 #[test]
604 fn hyperliquid_wallet_events_filter_roundtrip() {
605 let args =
606 TemplateArgs::HyperliquidWalletEventsFilter(HyperliquidWalletEventsFilterTemplate {
607 wallets: vec!["0xhl".to_string()],
608 });
609 let json = serde_json::to_string(&args).unwrap();
610 assert!(json.contains(r#""templateId":"hyperliquidWalletEventsFilter""#));
611 let parsed: TemplateArgs = serde_json::from_str(&json).unwrap();
612 assert!(matches!(
613 parsed,
614 TemplateArgs::HyperliquidWalletEventsFilter(_)
615 ));
616 }
617
618 #[test]
619 fn stellar_wallet_transactions_filter_roundtrip() {
620 let args = TemplateArgs::StellarWalletTransactionsSourceAccountFilter(
621 StellarWalletTransactionsFilterTemplate {
622 wallets: vec!["G...".to_string()],
623 },
624 );
625 let json = serde_json::to_string(&args).unwrap();
626 assert!(json.contains(r#""templateId":"stellarWalletTransactionsSourceAccountFilter""#));
627 let parsed: TemplateArgs = serde_json::from_str(&json).unwrap();
628 assert!(matches!(
629 parsed,
630 TemplateArgs::StellarWalletTransactionsSourceAccountFilter(_)
631 ));
632 }
633
634 #[test]
635 fn create_params_flattens_template_args() {
636 let params = CreateWebhookFromTemplateParams {
637 name: "n".to_string(),
638 network: "ethereum-mainnet".to_string(),
639 notification_email: None,
640 destination_attributes: WebhookDestinationAttributes {
641 url: "https://x".to_string(),
642 security_token: None,
643 compression: None,
644 },
645 template_args: TemplateArgs::EvmWalletFilter(EvmWalletFilterTemplate {
646 wallets: vec!["0xabc".to_string()],
647 }),
648 };
649 let json = serde_json::to_value(¶ms).unwrap();
650 let obj = json.as_object().unwrap();
651 assert_eq!(
652 obj.get("templateId").and_then(|v| v.as_str()),
653 Some("evmWalletFilter")
654 );
655 assert!(obj.get("templateArgs").unwrap().is_object());
656 assert_eq!(obj["templateArgs"]["wallets"][0].as_str(), Some("0xabc"));
657 }
658}