Skip to main content

quicknode_sdk/webhooks/
mod.rs

1pub mod webhook;
2
3pub use webhook::{
4    ActivateWebhookParams, BitcoinWalletFilterTemplate, CreateWebhookFromTemplateParams,
5    EvmAbiFilterTemplate, EvmContractEventsTemplate, EvmWalletFilterTemplate, GetWebhooksParams,
6    HyperliquidWalletEventsFilterTemplate, ListWebhooksResponse, SolanaWalletFilterTemplate,
7    StellarWalletTransactionsFilterTemplate, TemplateArgs, UpdateWebhookParams,
8    UpdateWebhookTemplateParams, Webhook, WebhookDestinationAttributes,
9    WebhookEnabledCountResponse, WebhookStartFrom, WebhookTemplateId, XrplWalletFilterTemplate,
10};
11
12use crate::{config::WebhooksConfig, errors::SdkError, SdkConfig};
13
14const WEBHOOKS_BASE_URL: &str = "https://api.quicknode.com/webhooks/rest/v1/";
15
16pub(crate) struct ResolvedWebhooksConfig {
17    pub(crate) base_url: reqwest::Url,
18}
19
20impl ResolvedWebhooksConfig {
21    pub(crate) fn from_config(config: Option<&WebhooksConfig>) -> Result<Self, SdkError> {
22        let url_str = config
23            .and_then(|s| s.base_url.as_deref())
24            .unwrap_or(WEBHOOKS_BASE_URL);
25        let mut base_url = reqwest::Url::parse(url_str)?;
26        if !base_url.path().ends_with('/') {
27            base_url.set_path(&format!("{}/", base_url.path()));
28        }
29        Ok(Self { base_url })
30    }
31}
32
33// ── Client ─────────────────────────────────────────────────────────────────
34
35/// Client for the QuickNode Webhooks REST API. Create webhooks from filter
36/// templates, manage their lifecycle, and update their destinations.
37#[derive(Debug, Clone)]
38pub struct WebhooksApiClient {
39    config: SdkConfig,
40}
41
42impl WebhooksApiClient {
43    pub fn new(config: SdkConfig) -> Self {
44        Self { config }
45    }
46
47    /// Returns a paginated list of webhooks on the account. Each entry includes
48    /// the webhook's identifier, creation timestamp, name, network, notification
49    /// email, destination configuration (URL, security token, compression),
50    /// current status, and any associated template. The response also includes
51    /// a `pageInfo` object with the applied limit, offset, and total count.
52    pub async fn list_webhooks(
53        &self,
54        params: &GetWebhooksParams,
55    ) -> Result<ListWebhooksResponse, SdkError> {
56        let mut url = self.config.webhooks().base_url.join("webhooks")?;
57        {
58            let mut pairs = url.query_pairs_mut();
59            if let Some(v) = params.limit {
60                pairs.append_pair("limit", &v.to_string());
61            }
62            if let Some(v) = params.offset {
63                pairs.append_pair("offset", &v.to_string());
64            }
65        }
66        let resp = self
67            .config
68            .http_client()
69            .get(url)
70            .send()
71            .await
72            .map_err(SdkError::Http)?;
73        let status = resp.status();
74        let body = resp.text().await.map_err(SdkError::Http)?;
75        if !status.is_success() {
76            return Err(SdkError::Api { status, body });
77        }
78        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
79    }
80
81    /// Removes every webhook on the account. Destructive and takes no
82    /// parameters.
83    pub async fn delete_all_webhooks(&self) -> Result<(), SdkError> {
84        let url = self.config.webhooks().base_url.join("webhooks")?;
85        let resp = self
86            .config
87            .http_client()
88            .delete(url)
89            .send()
90            .await
91            .map_err(SdkError::Http)?;
92        let status = resp.status();
93        if !status.is_success() {
94            let body = resp.text().await.map_err(SdkError::Http)?;
95            return Err(SdkError::Api { status, body });
96        }
97        Ok(())
98    }
99
100    /// Fetches a single webhook's full configuration and status by ID. Returns
101    /// creation timestamp, name, network, notification email, destination
102    /// configuration (URL, security token, compression), the sequence number
103    /// of the last successfully delivered block, the current status, and the
104    /// associated template with its arguments.
105    pub async fn get_webhook(&self, id: &str) -> Result<Webhook, SdkError> {
106        let url = self
107            .config
108            .webhooks()
109            .base_url
110            .join(&format!("webhooks/{id}"))?;
111        let resp = self
112            .config
113            .http_client()
114            .get(url)
115            .send()
116            .await
117            .map_err(SdkError::Http)?;
118        let status = resp.status();
119        let body = resp.text().await.map_err(SdkError::Http)?;
120        if !status.is_success() {
121            return Err(SdkError::Api { status, body });
122        }
123        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
124    }
125
126    /// Modifies an existing webhook's configuration. Supports updating the
127    /// webhook's name, notification email, and destination attributes (URL,
128    /// security token, and compression — `none` or `gzip`). All fields are
129    /// optional, so partial updates are supported; if the security token is
130    /// omitted on update, one is generated automatically. Returns the
131    /// webhook's full updated configuration.
132    pub async fn update_webhook(
133        &self,
134        id: &str,
135        params: &UpdateWebhookParams,
136    ) -> Result<Webhook, SdkError> {
137        let url = self
138            .config
139            .webhooks()
140            .base_url
141            .join(&format!("webhooks/{id}"))?;
142        let resp = self
143            .config
144            .http_client()
145            .patch(url)
146            .json(params)
147            .send()
148            .await
149            .map_err(SdkError::Http)?;
150        let status = resp.status();
151        let body = resp.text().await.map_err(SdkError::Http)?;
152        if !status.is_success() {
153            return Err(SdkError::Api { status, body });
154        }
155        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
156    }
157
158    /// Permanently removes a single webhook by ID.
159    pub async fn delete_webhook(&self, id: &str) -> Result<(), SdkError> {
160        let url = self
161            .config
162            .webhooks()
163            .base_url
164            .join(&format!("webhooks/{id}"))?;
165        let resp = self
166            .config
167            .http_client()
168            .delete(url)
169            .send()
170            .await
171            .map_err(SdkError::Http)?;
172        let status = resp.status();
173        if !status.is_success() {
174            let body = resp.text().await.map_err(SdkError::Http)?;
175            return Err(SdkError::Api { status, body });
176        }
177        Ok(())
178    }
179
180    /// Pauses a webhook by ID so it stops delivering events until reactivated.
181    pub async fn pause_webhook(&self, id: &str) -> Result<(), SdkError> {
182        let url = self
183            .config
184            .webhooks()
185            .base_url
186            .join(&format!("webhooks/{id}/pause"))?;
187        let resp = self
188            .config
189            .http_client()
190            .post(url)
191            .send()
192            .await
193            .map_err(SdkError::Http)?;
194        let status = resp.status();
195        if !status.is_success() {
196            let body = resp.text().await.map_err(SdkError::Http)?;
197            return Err(SdkError::Api { status, body });
198        }
199        Ok(())
200    }
201
202    /// Activates a previously created or paused webhook so it begins (or
203    /// resumes) delivering events. `start_from` determines where processing
204    /// resumes: `Latest` begins from the newest available block; other values
205    /// replay from an earlier point.
206    pub async fn activate_webhook(
207        &self,
208        id: &str,
209        params: &ActivateWebhookParams,
210    ) -> Result<(), SdkError> {
211        let url = self
212            .config
213            .webhooks()
214            .base_url
215            .join(&format!("webhooks/{id}/activate"))?;
216        let resp = self
217            .config
218            .http_client()
219            .post(url)
220            .json(params)
221            .send()
222            .await
223            .map_err(SdkError::Http)?;
224        let status = resp.status();
225        if !status.is_success() {
226            let body = resp.text().await.map_err(SdkError::Http)?;
227            return Err(SdkError::Api { status, body });
228        }
229        Ok(())
230    }
231
232    /// Returns the total number of enabled webhooks currently configured on
233    /// the account.
234    pub async fn get_enabled_count(&self) -> Result<WebhookEnabledCountResponse, SdkError> {
235        let url = self
236            .config
237            .webhooks()
238            .base_url
239            .join("webhooks/enabled_count")?;
240        let resp = self
241            .config
242            .http_client()
243            .get(url)
244            .send()
245            .await
246            .map_err(SdkError::Http)?;
247        let status = resp.status();
248        let body = resp.text().await.map_err(SdkError::Http)?;
249        if !status.is_success() {
250            return Err(SdkError::Api { status, body });
251        }
252        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
253    }
254
255    /// Creates a new webhook from a predefined filter template. Requires a
256    /// descriptive name, a target blockchain network, and destination
257    /// attributes (URL, optional security token — auto-generated when omitted,
258    /// and optional compression — `gzip` or `none`). `template_args` carries
259    /// template-specific configuration such as wallet addresses or contract
260    /// filters. An optional `notification_email` receives alerts if the
261    /// webhook terminates.
262    pub async fn create_webhook_from_template(
263        &self,
264        params: &CreateWebhookFromTemplateParams,
265    ) -> Result<Webhook, SdkError> {
266        let template_id = params.template_args.template_id.as_str();
267        let url = self
268            .config
269            .webhooks()
270            .base_url
271            .join(&format!("webhooks/template/{template_id}"))?;
272
273        #[allow(clippy::needless_borrows_for_generic_args)]
274        let mut body =
275            serde_json::to_value(&params).map_err(|e| SdkError::Config(e.to_string()))?;
276        let obj = body.as_object_mut().ok_or_else(|| {
277            SdkError::Config("failed to serialize request body as JSON object".into())
278        })?;
279        obj.insert(
280            "templateArgs".to_string(),
281            serde_json::from_str(&params.template_args.value)
282                .map_err(|e| SdkError::Config(e.to_string()))?,
283        );
284
285        let resp = self
286            .config
287            .http_client()
288            .post(url)
289            .json(&body)
290            .send()
291            .await
292            .map_err(SdkError::Http)?;
293        let status = resp.status();
294        let body = resp.text().await.map_err(SdkError::Http)?;
295        if !status.is_success() {
296            return Err(SdkError::Api { status, body });
297        }
298        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
299    }
300
301    /// Updates an existing template-backed webhook, modifying its template
302    /// arguments and optionally its name, notification email, and destination
303    /// attributes (URL, security token, compression — `none` or `gzip`).
304    /// All optional fields support partial updates; a security token is
305    /// generated automatically if not provided. Templates cover EVM chains,
306    /// Solana, Bitcoin, XRPL, Hyperliquid, and Stellar.
307    pub async fn update_webhook_template(
308        &self,
309        webhook_id: &str,
310        params: &UpdateWebhookTemplateParams,
311    ) -> Result<Webhook, SdkError> {
312        let template_id = params.template_args.template_id.as_str();
313        let url = self
314            .config
315            .webhooks()
316            .base_url
317            .join(&format!("webhooks/{webhook_id}/template/{template_id}"))?;
318
319        #[allow(clippy::needless_borrows_for_generic_args)]
320        let mut body =
321            serde_json::to_value(&params).map_err(|e| SdkError::Config(e.to_string()))?;
322        let obj = body.as_object_mut().ok_or_else(|| {
323            SdkError::Config("failed to serialize request body as JSON object".into())
324        })?;
325        obj.insert(
326            "templateArgs".to_string(),
327            serde_json::from_str(&params.template_args.value)
328                .map_err(|e| SdkError::Config(e.to_string()))?,
329        );
330
331        let resp = self
332            .config
333            .http_client()
334            .patch(url)
335            .json(&body)
336            .send()
337            .await
338            .map_err(SdkError::Http)?;
339        let status = resp.status();
340        let body = resp.text().await.map_err(SdkError::Http)?;
341        if !status.is_success() {
342            return Err(SdkError::Api { status, body });
343        }
344        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
345    }
346}
347
348// ── Tests ──────────────────────────────────────────────────────────────────
349
350#[cfg(test)]
351#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
352mod tests {
353    use super::*;
354    use crate::{QuickNodeSdk, SdkFullConfig, WebhooksConfig};
355    use wiremock::matchers::{method, path, path_regex};
356    use wiremock::{Mock, MockServer, ResponseTemplate};
357
358    fn make_sdk(base_url: String) -> QuickNodeSdk {
359        QuickNodeSdk::new(&SdkFullConfig {
360            api_key: "test-key".to_string(),
361            http: None,
362            admin: None,
363            streams: None,
364            webhooks: Some(WebhooksConfig {
365                base_url: Some(base_url),
366            }),
367            kvstore: None,
368        })
369        .unwrap()
370    }
371
372    fn webhook_response_json() -> serde_json::Value {
373        serde_json::json!({
374            "id": "wh-1234-5678",
375            "name": "test-webhook",
376            "status": "active",
377            "network": "ethereum-mainnet",
378            "created_at": "2026-03-19T12:00:00Z",
379            "updated_at": "2026-03-19T12:00:00Z"
380        })
381    }
382
383    #[tokio::test]
384    async fn list_webhooks_success() {
385        let server = MockServer::start().await;
386        let response = serde_json::json!({
387            "data": [webhook_response_json()]
388        });
389        Mock::given(method("GET"))
390            .and(path("/webhooks"))
391            .respond_with(ResponseTemplate::new(200).set_body_json(response))
392            .mount(&server)
393            .await;
394        let sdk = make_sdk(format!("{}/", server.uri()));
395        let resp = sdk
396            .webhooks
397            .list_webhooks(&GetWebhooksParams::default())
398            .await
399            .unwrap();
400        assert_eq!(resp.data.len(), 1);
401        assert_eq!(resp.data[0].id, "wh-1234-5678");
402    }
403
404    #[tokio::test]
405    async fn list_webhooks_api_error() {
406        let server = MockServer::start().await;
407        Mock::given(method("GET"))
408            .and(path("/webhooks"))
409            .respond_with(ResponseTemplate::new(400).set_body_string("Bad Request"))
410            .mount(&server)
411            .await;
412        let sdk = make_sdk(format!("{}/", server.uri()));
413        let err = sdk
414            .webhooks
415            .list_webhooks(&GetWebhooksParams::default())
416            .await
417            .unwrap_err();
418        assert!(matches!(err, SdkError::Api { .. }));
419    }
420
421    #[tokio::test]
422    async fn list_webhooks_server_error() {
423        let server = MockServer::start().await;
424        Mock::given(method("GET"))
425            .and(path("/webhooks"))
426            .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
427            .mount(&server)
428            .await;
429        let sdk = make_sdk(format!("{}/", server.uri()));
430        let err = sdk
431            .webhooks
432            .list_webhooks(&GetWebhooksParams::default())
433            .await
434            .unwrap_err();
435        assert!(matches!(err, SdkError::Api { .. }));
436    }
437
438    #[tokio::test]
439    async fn get_webhook_success() {
440        let server = MockServer::start().await;
441        Mock::given(method("GET"))
442            .and(path("/webhooks/test-id"))
443            .respond_with(ResponseTemplate::new(200).set_body_json(webhook_response_json()))
444            .mount(&server)
445            .await;
446        let sdk = make_sdk(format!("{}/", server.uri()));
447        let resp = sdk.webhooks.get_webhook("test-id").await.unwrap();
448        assert_eq!(resp.id, "wh-1234-5678");
449    }
450
451    #[tokio::test]
452    async fn get_webhook_api_error() {
453        let server = MockServer::start().await;
454        Mock::given(method("GET"))
455            .and(path("/webhooks/test-id"))
456            .respond_with(ResponseTemplate::new(404).set_body_string("Not Found"))
457            .mount(&server)
458            .await;
459        let sdk = make_sdk(format!("{}/", server.uri()));
460        let err = sdk.webhooks.get_webhook("test-id").await.unwrap_err();
461        assert!(matches!(err, SdkError::Api { .. }));
462    }
463
464    #[tokio::test]
465    async fn update_webhook_success() {
466        let server = MockServer::start().await;
467        let mut updated = webhook_response_json();
468        updated["name"] = serde_json::json!("updated-name");
469        Mock::given(method("PATCH"))
470            .and(path("/webhooks/test-id"))
471            .respond_with(ResponseTemplate::new(200).set_body_json(updated))
472            .mount(&server)
473            .await;
474        let sdk = make_sdk(format!("{}/", server.uri()));
475        let params = UpdateWebhookParams {
476            name: Some("updated-name".to_string()),
477            ..Default::default()
478        };
479        let resp = sdk
480            .webhooks
481            .update_webhook("test-id", &params)
482            .await
483            .unwrap();
484        assert_eq!(resp.name, "updated-name");
485    }
486
487    #[tokio::test]
488    async fn update_webhook_api_error() {
489        let server = MockServer::start().await;
490        Mock::given(method("PATCH"))
491            .and(path("/webhooks/test-id"))
492            .respond_with(ResponseTemplate::new(400).set_body_string("Bad Request"))
493            .mount(&server)
494            .await;
495        let sdk = make_sdk(format!("{}/", server.uri()));
496        let params = UpdateWebhookParams::default();
497        let err = sdk
498            .webhooks
499            .update_webhook("test-id", &params)
500            .await
501            .unwrap_err();
502        assert!(matches!(err, SdkError::Api { .. }));
503    }
504
505    #[tokio::test]
506    async fn delete_webhook_success() {
507        let server = MockServer::start().await;
508        Mock::given(method("DELETE"))
509            .and(path("/webhooks/test-id"))
510            .respond_with(ResponseTemplate::new(200))
511            .mount(&server)
512            .await;
513        let sdk = make_sdk(format!("{}/", server.uri()));
514        sdk.webhooks.delete_webhook("test-id").await.unwrap();
515    }
516
517    #[tokio::test]
518    async fn delete_webhook_api_error() {
519        let server = MockServer::start().await;
520        Mock::given(method("DELETE"))
521            .and(path("/webhooks/test-id"))
522            .respond_with(ResponseTemplate::new(404).set_body_string("Not Found"))
523            .mount(&server)
524            .await;
525        let sdk = make_sdk(format!("{}/", server.uri()));
526        let err = sdk.webhooks.delete_webhook("test-id").await.unwrap_err();
527        assert!(matches!(err, SdkError::Api { .. }));
528    }
529
530    #[tokio::test]
531    async fn delete_all_webhooks_success() {
532        let server = MockServer::start().await;
533        Mock::given(method("DELETE"))
534            .and(path("/webhooks"))
535            .respond_with(ResponseTemplate::new(204))
536            .mount(&server)
537            .await;
538        let sdk = make_sdk(format!("{}/", server.uri()));
539        sdk.webhooks.delete_all_webhooks().await.unwrap();
540    }
541
542    #[tokio::test]
543    async fn delete_all_webhooks_api_error() {
544        let server = MockServer::start().await;
545        Mock::given(method("DELETE"))
546            .and(path("/webhooks"))
547            .respond_with(ResponseTemplate::new(400).set_body_string("Bad Request"))
548            .mount(&server)
549            .await;
550        let sdk = make_sdk(format!("{}/", server.uri()));
551        let err = sdk.webhooks.delete_all_webhooks().await.unwrap_err();
552        assert!(matches!(err, SdkError::Api { .. }));
553    }
554
555    #[tokio::test]
556    async fn pause_webhook_success() {
557        let server = MockServer::start().await;
558        Mock::given(method("POST"))
559            .and(path("/webhooks/test-id/pause"))
560            .respond_with(ResponseTemplate::new(201))
561            .mount(&server)
562            .await;
563        let sdk = make_sdk(format!("{}/", server.uri()));
564        sdk.webhooks.pause_webhook("test-id").await.unwrap();
565    }
566
567    #[tokio::test]
568    async fn pause_webhook_api_error() {
569        let server = MockServer::start().await;
570        Mock::given(method("POST"))
571            .and(path("/webhooks/test-id/pause"))
572            .respond_with(ResponseTemplate::new(404).set_body_string("Not Found"))
573            .mount(&server)
574            .await;
575        let sdk = make_sdk(format!("{}/", server.uri()));
576        let err = sdk.webhooks.pause_webhook("test-id").await.unwrap_err();
577        assert!(matches!(err, SdkError::Api { .. }));
578    }
579
580    #[tokio::test]
581    async fn activate_webhook_success() {
582        let server = MockServer::start().await;
583        Mock::given(method("POST"))
584            .and(path("/webhooks/test-id/activate"))
585            .respond_with(ResponseTemplate::new(201))
586            .mount(&server)
587            .await;
588        let sdk = make_sdk(format!("{}/", server.uri()));
589        let params = ActivateWebhookParams {
590            start_from: WebhookStartFrom::Latest,
591        };
592        sdk.webhooks
593            .activate_webhook("test-id", &params)
594            .await
595            .unwrap();
596    }
597
598    #[tokio::test]
599    async fn activate_webhook_api_error() {
600        let server = MockServer::start().await;
601        Mock::given(method("POST"))
602            .and(path("/webhooks/test-id/activate"))
603            .respond_with(ResponseTemplate::new(400).set_body_string("Bad Request"))
604            .mount(&server)
605            .await;
606        let sdk = make_sdk(format!("{}/", server.uri()));
607        let params = ActivateWebhookParams {
608            start_from: WebhookStartFrom::Latest,
609        };
610        let err = sdk
611            .webhooks
612            .activate_webhook("test-id", &params)
613            .await
614            .unwrap_err();
615        assert!(matches!(err, SdkError::Api { .. }));
616    }
617
618    #[tokio::test]
619    async fn get_enabled_count_success() {
620        let server = MockServer::start().await;
621        Mock::given(method("GET"))
622            .and(path("/webhooks/enabled_count"))
623            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({"total": 5})))
624            .mount(&server)
625            .await;
626        let sdk = make_sdk(format!("{}/", server.uri()));
627        let resp = sdk.webhooks.get_enabled_count().await.unwrap();
628        assert_eq!(resp.total, 5);
629    }
630
631    #[tokio::test]
632    async fn get_enabled_count_api_error() {
633        let server = MockServer::start().await;
634        Mock::given(method("GET"))
635            .and(path("/webhooks/enabled_count"))
636            .respond_with(ResponseTemplate::new(400).set_body_string("Bad Request"))
637            .mount(&server)
638            .await;
639        let sdk = make_sdk(format!("{}/", server.uri()));
640        let err = sdk.webhooks.get_enabled_count().await.unwrap_err();
641        assert!(matches!(err, SdkError::Api { .. }));
642    }
643
644    #[tokio::test]
645    async fn create_webhook_from_template_success() {
646        let server = MockServer::start().await;
647        Mock::given(method("POST"))
648            .and(path_regex("/webhooks/template/evmWalletFilter"))
649            .respond_with(ResponseTemplate::new(201).set_body_json(webhook_response_json()))
650            .mount(&server)
651            .await;
652        let sdk = make_sdk(format!("{}/", server.uri()));
653        let template_args = TemplateArgs::evm_wallet_filter(&EvmWalletFilterTemplate {
654            wallets: vec!["0xabc".to_string()],
655        })
656        .unwrap();
657        let params = CreateWebhookFromTemplateParams {
658            name: "test-webhook".to_string(),
659            network: "ethereum-mainnet".to_string(),
660            notification_email: None,
661            destination_attributes: WebhookDestinationAttributes {
662                url: "https://example.com/hook".to_string(),
663                security_token: None,
664                compression: None,
665            },
666            template_args,
667        };
668        let resp = sdk
669            .webhooks
670            .create_webhook_from_template(&params)
671            .await
672            .unwrap();
673        assert_eq!(resp.id, "wh-1234-5678");
674    }
675
676    #[tokio::test]
677    async fn create_webhook_from_template_api_error() {
678        let server = MockServer::start().await;
679        Mock::given(method("POST"))
680            .and(path_regex("/webhooks/template/evmWalletFilter"))
681            .respond_with(ResponseTemplate::new(400).set_body_string("Bad Request"))
682            .mount(&server)
683            .await;
684        let sdk = make_sdk(format!("{}/", server.uri()));
685        let template_args = TemplateArgs::evm_wallet_filter(&EvmWalletFilterTemplate {
686            wallets: vec!["0xabc".to_string()],
687        })
688        .unwrap();
689        let params = CreateWebhookFromTemplateParams {
690            name: "test-webhook".to_string(),
691            network: "ethereum-mainnet".to_string(),
692            notification_email: None,
693            destination_attributes: WebhookDestinationAttributes {
694                url: "https://example.com/hook".to_string(),
695                security_token: None,
696                compression: None,
697            },
698            template_args,
699        };
700        let err = sdk
701            .webhooks
702            .create_webhook_from_template(&params)
703            .await
704            .unwrap_err();
705        assert!(matches!(err, SdkError::Api { .. }));
706    }
707
708    #[tokio::test]
709    async fn update_webhook_template_success() {
710        let server = MockServer::start().await;
711        Mock::given(method("PATCH"))
712            .and(path_regex("/webhooks/test-id/template/evmWalletFilter"))
713            .respond_with(ResponseTemplate::new(200).set_body_json(webhook_response_json()))
714            .mount(&server)
715            .await;
716        let sdk = make_sdk(format!("{}/", server.uri()));
717        let template_args = TemplateArgs::evm_wallet_filter(&EvmWalletFilterTemplate {
718            wallets: vec!["0xabc".to_string()],
719        })
720        .unwrap();
721        let params = UpdateWebhookTemplateParams {
722            name: None,
723            notification_email: None,
724            destination_attributes: None,
725            template_args,
726        };
727        let resp = sdk
728            .webhooks
729            .update_webhook_template("test-id", &params)
730            .await
731            .unwrap();
732        assert_eq!(resp.id, "wh-1234-5678");
733    }
734
735    #[tokio::test]
736    async fn update_webhook_template_api_error() {
737        let server = MockServer::start().await;
738        Mock::given(method("PATCH"))
739            .and(path_regex("/webhooks/test-id/template/evmWalletFilter"))
740            .respond_with(ResponseTemplate::new(400).set_body_string("Bad Request"))
741            .mount(&server)
742            .await;
743        let sdk = make_sdk(format!("{}/", server.uri()));
744        let template_args = TemplateArgs::evm_wallet_filter(&EvmWalletFilterTemplate {
745            wallets: vec!["0xabc".to_string()],
746        })
747        .unwrap();
748        let params = UpdateWebhookTemplateParams {
749            name: None,
750            notification_email: None,
751            destination_attributes: None,
752            template_args,
753        };
754        let err = sdk
755            .webhooks
756            .update_webhook_template("test-id", &params)
757            .await
758            .unwrap_err();
759        assert!(matches!(err, SdkError::Api { .. }));
760    }
761}