From 10f875fb06e600106cba9d62d867da6eda474ae5 Mon Sep 17 00:00:00 2001 From: John Mitsch Date: Wed, 27 May 2026 10:50:11 -0400 Subject: [PATCH] fix(webhooks): send camelCase eventHashes in evmContractEvents template (DX-5346) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Webhooks API expects `eventHashes` (camelCase) in the `evmContractEvents` templateArgs. `EvmContractEventsTemplate` had no serde rename, so the field was serialized as `event_hashes` and the API returned `500: Expected array for arg "eventHashes"` whether the field was supplied or not — making the template effectively unusable. --- crates/core/src/webhooks/mod.rs | 39 +++++++++++++++++++++++++++++ crates/core/src/webhooks/webhook.rs | 4 +++ 2 files changed, 43 insertions(+) diff --git a/crates/core/src/webhooks/mod.rs b/crates/core/src/webhooks/mod.rs index 0bbd919..225fb79 100644 --- a/crates/core/src/webhooks/mod.rs +++ b/crates/core/src/webhooks/mod.rs @@ -740,6 +740,45 @@ mod tests { .unwrap(); } + // Wire-inspection regression: the API expects `eventHashes` (camelCase) + // and returns 500 if it sees `event_hashes`. Confirm the field reaches the + // wire under the camelCase key. + #[tokio::test] + async fn create_webhook_from_template_wire_body_uses_camelcase_event_hashes() { + use wiremock::matchers::body_partial_json; + let server = MockServer::start().await; + Mock::given(method("POST")) + .and(path_regex("/webhooks/template/evmContractEvents")) + .and(body_partial_json(serde_json::json!({ + "templateArgs": { + "eventHashes": ["0xabcd"], + } + }))) + .respond_with(ResponseTemplate::new(201).set_body_json(webhook_response_json())) + .mount(&server) + .await; + let sdk = make_sdk(format!("{}/", server.uri())); + let template_args = TemplateArgs::EvmContractEvents(EvmContractEventsTemplate { + contracts: vec!["0xa0b8".to_string()], + event_hashes: Some(vec!["0xabcd".to_string()]), + }); + let params = CreateWebhookFromTemplateParams { + name: "test-webhook".to_string(), + network: "ethereum-mainnet".to_string(), + notification_email: None, + destination_attributes: WebhookDestinationAttributes { + url: "https://example.com/hook".to_string(), + security_token: None, + compression: None, + }, + template_args, + }; + sdk.webhooks + .create_webhook_from_template(¶ms) + .await + .unwrap(); + } + #[tokio::test] async fn update_webhook_template_api_error() { let server = MockServer::start().await; diff --git a/crates/core/src/webhooks/webhook.rs b/crates/core/src/webhooks/webhook.rs index 42eac0f..89cad36 100644 --- a/crates/core/src/webhooks/webhook.rs +++ b/crates/core/src/webhooks/webhook.rs @@ -99,6 +99,7 @@ impl EvmWalletFilterTemplate { #[cfg_attr(feature = "python", pyclass(get_all, set_all))] #[cfg_attr(feature = "node", napi(object))] #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct EvmContractEventsTemplate { /// Contract addresses to watch for events. pub contracts: Vec, @@ -547,6 +548,9 @@ mod template_args_tests { }); let json = serde_json::to_string(&args).unwrap(); assert!(json.contains(r#""templateId":"evmContractEvents""#)); + // API expects camelCase `eventHashes` — snake_case is silently rejected with a 500. + assert!(json.contains(r#""eventHashes":["0x1234"]"#)); + assert!(!json.contains("event_hashes")); let parsed: TemplateArgs = serde_json::from_str(&json).unwrap(); assert!(matches!(parsed, TemplateArgs::EvmContractEvents(_))); }