#[cfg(feature = "rust")]
use bon::Builder;
#[cfg(feature = "node")]
use napi_derive::napi;
#[cfg(feature = "python")]
use pyo3::{pyclass, pymethods};
#[cfg(feature = "python")]
use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods};
use serde::{Deserialize, Deserializer, Serialize};
fn deserialize_as_optional_json_string<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
let value = Option::<serde_json::Value>::deserialize(deserializer)?;
match value {
None => Ok(None),
Some(v) => serde_json::to_string(&v)
.map(Some)
.map_err(serde::de::Error::custom),
}
}
#[cfg_attr(feature = "node", napi(string_enum))]
#[cfg_attr(not(feature = "node"), derive(Clone))]
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum WebhookTemplateId {
EvmWalletFilter,
EvmContractEvents,
EvmAbiFilter,
SolanaWalletFilter,
BitcoinWalletFilter,
XrplWalletFilter,
HyperliquidWalletEventsFilter,
StellarWalletTransactionsSourceAccountFilter,
}
impl WebhookTemplateId {
pub fn as_str(&self) -> &'static str {
match self {
WebhookTemplateId::EvmWalletFilter => "evmWalletFilter",
WebhookTemplateId::EvmContractEvents => "evmContractEvents",
WebhookTemplateId::EvmAbiFilter => "evmAbiFilter",
WebhookTemplateId::SolanaWalletFilter => "solanaWalletFilter",
WebhookTemplateId::BitcoinWalletFilter => "bitcoinWalletFilter",
WebhookTemplateId::XrplWalletFilter => "xrplWalletFilter",
WebhookTemplateId::HyperliquidWalletEventsFilter => "hyperliquidWalletEventsFilter",
WebhookTemplateId::StellarWalletTransactionsSourceAccountFilter => {
"stellarWalletTransactionsSourceAccountFilter"
}
}
}
}
#[cfg_attr(feature = "node", napi(string_enum))]
#[cfg_attr(not(feature = "node"), derive(Clone))]
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum WebhookStartFrom {
Last,
Latest,
}
#[cfg_attr(feature = "rust", derive(Builder))]
#[cfg_attr(feature = "python", gen_stub_pyclass)]
#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
#[cfg_attr(feature = "node", napi(object))]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvmWalletFilterTemplate {
pub wallets: Vec<String>,
}
#[cfg(feature = "python")]
#[gen_stub_pymethods]
#[pymethods]
impl EvmWalletFilterTemplate {
#[new]
pub fn new(wallets: Vec<String>) -> Self {
Self { wallets }
}
}
#[cfg_attr(feature = "rust", derive(Builder))]
#[cfg_attr(feature = "python", gen_stub_pyclass)]
#[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 {
pub contracts: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub event_hashes: Option<Vec<String>>,
}
#[cfg(feature = "python")]
#[gen_stub_pymethods]
#[pymethods]
impl EvmContractEventsTemplate {
#[new]
#[pyo3(signature = (contracts, event_hashes=None))]
pub fn new(contracts: Vec<String>, event_hashes: Option<Vec<String>>) -> Self {
Self {
contracts,
event_hashes,
}
}
}
#[cfg_attr(feature = "rust", derive(Builder))]
#[cfg_attr(feature = "python", gen_stub_pyclass)]
#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
#[cfg_attr(feature = "node", napi(object))]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvmAbiFilterTemplate {
pub abi: String,
pub contracts: Vec<String>,
}
#[cfg(feature = "python")]
#[gen_stub_pymethods]
#[pymethods]
impl EvmAbiFilterTemplate {
#[new]
pub fn new(abi: String, contracts: Vec<String>) -> Self {
Self { abi, contracts }
}
}
#[cfg_attr(feature = "rust", derive(Builder))]
#[cfg_attr(feature = "python", gen_stub_pyclass)]
#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
#[cfg_attr(feature = "node", napi(object))]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SolanaWalletFilterTemplate {
pub accounts: Vec<String>,
}
#[cfg(feature = "python")]
#[gen_stub_pymethods]
#[pymethods]
impl SolanaWalletFilterTemplate {
#[new]
pub fn new(accounts: Vec<String>) -> Self {
Self { accounts }
}
}
#[cfg_attr(feature = "rust", derive(Builder))]
#[cfg_attr(feature = "python", gen_stub_pyclass)]
#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
#[cfg_attr(feature = "node", napi(object))]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BitcoinWalletFilterTemplate {
pub wallets: Vec<String>,
}
#[cfg(feature = "python")]
#[gen_stub_pymethods]
#[pymethods]
impl BitcoinWalletFilterTemplate {
#[new]
pub fn new(wallets: Vec<String>) -> Self {
Self { wallets }
}
}
#[cfg_attr(feature = "rust", derive(Builder))]
#[cfg_attr(feature = "python", gen_stub_pyclass)]
#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
#[cfg_attr(feature = "node", napi(object))]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct XrplWalletFilterTemplate {
pub wallets: Vec<String>,
}
#[cfg(feature = "python")]
#[gen_stub_pymethods]
#[pymethods]
impl XrplWalletFilterTemplate {
#[new]
pub fn new(wallets: Vec<String>) -> Self {
Self { wallets }
}
}
#[cfg_attr(feature = "rust", derive(Builder))]
#[cfg_attr(feature = "python", gen_stub_pyclass)]
#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
#[cfg_attr(feature = "node", napi(object))]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HyperliquidWalletEventsFilterTemplate {
pub wallets: Vec<String>,
}
#[cfg(feature = "python")]
#[gen_stub_pymethods]
#[pymethods]
impl HyperliquidWalletEventsFilterTemplate {
#[new]
pub fn new(wallets: Vec<String>) -> Self {
Self { wallets }
}
}
#[cfg_attr(feature = "rust", derive(Builder))]
#[cfg_attr(feature = "python", gen_stub_pyclass)]
#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
#[cfg_attr(feature = "node", napi(object))]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StellarWalletTransactionsFilterTemplate {
pub wallets: Vec<String>,
}
#[cfg(feature = "python")]
#[gen_stub_pymethods]
#[pymethods]
impl StellarWalletTransactionsFilterTemplate {
#[new]
pub fn new(wallets: Vec<String>) -> Self {
Self { wallets }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "templateId", content = "templateArgs", rename_all = "camelCase")]
pub enum TemplateArgs {
EvmWalletFilter(EvmWalletFilterTemplate),
EvmContractEvents(EvmContractEventsTemplate),
EvmAbiFilter(EvmAbiFilterTemplate),
SolanaWalletFilter(SolanaWalletFilterTemplate),
BitcoinWalletFilter(BitcoinWalletFilterTemplate),
XrplWalletFilter(XrplWalletFilterTemplate),
HyperliquidWalletEventsFilter(HyperliquidWalletEventsFilterTemplate),
StellarWalletTransactionsSourceAccountFilter(StellarWalletTransactionsFilterTemplate),
}
impl TemplateArgs {
pub fn tag(&self) -> WebhookTemplateId {
match self {
Self::EvmWalletFilter(_) => WebhookTemplateId::EvmWalletFilter,
Self::EvmContractEvents(_) => WebhookTemplateId::EvmContractEvents,
Self::EvmAbiFilter(_) => WebhookTemplateId::EvmAbiFilter,
Self::SolanaWalletFilter(_) => WebhookTemplateId::SolanaWalletFilter,
Self::BitcoinWalletFilter(_) => WebhookTemplateId::BitcoinWalletFilter,
Self::XrplWalletFilter(_) => WebhookTemplateId::XrplWalletFilter,
Self::HyperliquidWalletEventsFilter(_) => {
WebhookTemplateId::HyperliquidWalletEventsFilter
}
Self::StellarWalletTransactionsSourceAccountFilter(_) => {
WebhookTemplateId::StellarWalletTransactionsSourceAccountFilter
}
}
}
}
#[cfg_attr(feature = "python", gen_stub_pyclass)]
#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
#[cfg_attr(feature = "node", napi(object))]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebhookDestinationAttributes {
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub security_token: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub compression: Option<String>,
}
#[cfg(feature = "python")]
#[gen_stub_pymethods]
#[pymethods]
impl WebhookDestinationAttributes {
#[new]
#[pyo3(signature = (url, security_token=None, compression=None))]
pub fn new(url: String, security_token: Option<String>, compression: Option<String>) -> Self {
Self {
url,
security_token,
compression,
}
}
}
#[cfg_attr(feature = "python", gen_stub_pyclass)]
#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
#[cfg_attr(feature = "node", napi(object))]
#[cfg_attr(not(feature = "node"), derive(Clone))]
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct GetWebhooksParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub offset: Option<i64>,
}
#[cfg(feature = "python")]
#[gen_stub_pymethods]
#[pymethods]
impl GetWebhooksParams {
#[new]
#[pyo3(signature = (limit=None, offset=None))]
pub fn new(limit: Option<i64>, offset: Option<i64>) -> Self {
Self { limit, offset }
}
}
#[cfg_attr(feature = "python", gen_stub_pyclass)]
#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
#[cfg_attr(feature = "node", napi(object))]
#[cfg_attr(not(feature = "node"), derive(Clone))]
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct UpdateWebhookParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub notification_email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub destination_attributes: Option<WebhookDestinationAttributes>,
}
#[cfg(feature = "python")]
#[gen_stub_pymethods]
#[pymethods]
impl UpdateWebhookParams {
#[new]
#[pyo3(signature = (name=None, notification_email=None, destination_attributes=None))]
pub fn new(
name: Option<String>,
notification_email: Option<String>,
destination_attributes: Option<WebhookDestinationAttributes>,
) -> Self {
Self {
name,
notification_email,
destination_attributes,
}
}
}
#[cfg_attr(feature = "node", napi(object))]
#[cfg_attr(not(feature = "node"), derive(Clone))]
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ActivateWebhookParams {
pub start_from: WebhookStartFrom,
}
#[cfg_attr(feature = "rust", derive(Builder))]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateWebhookFromTemplateParams {
pub name: String,
pub network: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub notification_email: Option<String>,
pub destination_attributes: WebhookDestinationAttributes,
#[serde(flatten)]
pub template_args: TemplateArgs,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateWebhookTemplateParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub notification_email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub destination_attributes: Option<WebhookDestinationAttributes>,
#[serde(flatten)]
pub template_args: TemplateArgs,
}
#[cfg_attr(feature = "python", gen_stub_pyclass)]
#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
#[cfg_attr(feature = "node", napi(object))]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Webhook {
pub id: String,
pub name: String,
pub status: String,
pub network: String,
pub created_at: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub updated_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub template_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub notification_email: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_as_optional_json_string"
)]
pub destination_attributes: Option<String>,
}
#[cfg_attr(feature = "python", gen_stub_pyclass)]
#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
#[cfg_attr(feature = "node", napi(object))]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebhookPageInfo {
pub limit: i64,
pub offset: i64,
pub total: i64,
}
#[cfg_attr(feature = "python", gen_stub_pyclass)]
#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
#[cfg_attr(feature = "node", napi(object))]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListWebhooksResponse {
pub data: Vec<Webhook>,
#[serde(rename = "pageInfo")]
pub page_info: WebhookPageInfo,
}
#[cfg_attr(feature = "python", gen_stub_pyclass)]
#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
#[cfg_attr(feature = "node", napi(object))]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebhookEnabledCountResponse {
pub total: i64,
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod template_args_tests {
use super::*;
#[test]
fn evm_wallet_filter_roundtrip() {
let args = TemplateArgs::EvmWalletFilter(EvmWalletFilterTemplate {
wallets: vec!["0xabc".to_string()],
});
let json = serde_json::to_string(&args).unwrap();
assert!(json.contains(r#""templateId":"evmWalletFilter""#));
assert!(json.contains(r#""wallets":["0xabc"]"#));
let parsed: TemplateArgs = serde_json::from_str(&json).unwrap();
assert!(matches!(parsed, TemplateArgs::EvmWalletFilter(_)));
assert!(matches!(parsed.tag(), WebhookTemplateId::EvmWalletFilter));
}
#[test]
fn evm_contract_events_roundtrip() {
let args = TemplateArgs::EvmContractEvents(EvmContractEventsTemplate {
contracts: vec!["0xdef".to_string()],
event_hashes: Some(vec!["0x1234".to_string()]),
});
let json = serde_json::to_string(&args).unwrap();
assert!(json.contains(r#""templateId":"evmContractEvents""#));
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(_)));
}
#[test]
fn evm_abi_filter_roundtrip() {
let args = TemplateArgs::EvmAbiFilter(EvmAbiFilterTemplate {
abi: "[]".to_string(),
contracts: vec!["0xdef".to_string()],
});
let json = serde_json::to_string(&args).unwrap();
assert!(json.contains(r#""templateId":"evmAbiFilter""#));
let parsed: TemplateArgs = serde_json::from_str(&json).unwrap();
assert!(matches!(parsed, TemplateArgs::EvmAbiFilter(_)));
}
#[test]
fn solana_wallet_filter_roundtrip() {
let args = TemplateArgs::SolanaWalletFilter(SolanaWalletFilterTemplate {
accounts: vec!["acc".to_string()],
});
let json = serde_json::to_string(&args).unwrap();
assert!(json.contains(r#""templateId":"solanaWalletFilter""#));
let parsed: TemplateArgs = serde_json::from_str(&json).unwrap();
assert!(matches!(parsed, TemplateArgs::SolanaWalletFilter(_)));
}
#[test]
fn bitcoin_wallet_filter_roundtrip() {
let args = TemplateArgs::BitcoinWalletFilter(BitcoinWalletFilterTemplate {
wallets: vec!["bc1".to_string()],
});
let json = serde_json::to_string(&args).unwrap();
assert!(json.contains(r#""templateId":"bitcoinWalletFilter""#));
let parsed: TemplateArgs = serde_json::from_str(&json).unwrap();
assert!(matches!(parsed, TemplateArgs::BitcoinWalletFilter(_)));
}
#[test]
fn xrpl_wallet_filter_roundtrip() {
let args = TemplateArgs::XrplWalletFilter(XrplWalletFilterTemplate {
wallets: vec!["r1".to_string()],
});
let json = serde_json::to_string(&args).unwrap();
assert!(json.contains(r#""templateId":"xrplWalletFilter""#));
let parsed: TemplateArgs = serde_json::from_str(&json).unwrap();
assert!(matches!(parsed, TemplateArgs::XrplWalletFilter(_)));
}
#[test]
fn hyperliquid_wallet_events_filter_roundtrip() {
let args =
TemplateArgs::HyperliquidWalletEventsFilter(HyperliquidWalletEventsFilterTemplate {
wallets: vec!["0xhl".to_string()],
});
let json = serde_json::to_string(&args).unwrap();
assert!(json.contains(r#""templateId":"hyperliquidWalletEventsFilter""#));
let parsed: TemplateArgs = serde_json::from_str(&json).unwrap();
assert!(matches!(
parsed,
TemplateArgs::HyperliquidWalletEventsFilter(_)
));
}
#[test]
fn stellar_wallet_transactions_filter_roundtrip() {
let args = TemplateArgs::StellarWalletTransactionsSourceAccountFilter(
StellarWalletTransactionsFilterTemplate {
wallets: vec!["G...".to_string()],
},
);
let json = serde_json::to_string(&args).unwrap();
assert!(json.contains(r#""templateId":"stellarWalletTransactionsSourceAccountFilter""#));
let parsed: TemplateArgs = serde_json::from_str(&json).unwrap();
assert!(matches!(
parsed,
TemplateArgs::StellarWalletTransactionsSourceAccountFilter(_)
));
}
#[test]
fn create_params_flattens_template_args() {
let params = CreateWebhookFromTemplateParams {
name: "n".to_string(),
network: "ethereum-mainnet".to_string(),
notification_email: None,
destination_attributes: WebhookDestinationAttributes {
url: "https://x".to_string(),
security_token: None,
compression: None,
},
template_args: TemplateArgs::EvmWalletFilter(EvmWalletFilterTemplate {
wallets: vec!["0xabc".to_string()],
}),
};
let json = serde_json::to_value(¶ms).unwrap();
let obj = json.as_object().unwrap();
assert_eq!(
obj.get("templateId").and_then(|v| v.as_str()),
Some("evmWalletFilter")
);
assert!(obj.get("templateArgs").unwrap().is_object());
assert_eq!(obj["templateArgs"]["wallets"][0].as_str(), Some("0xabc"));
}
}