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