diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/components/create-webhook-config-modal.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/components/create-webhook-config-modal.tsx
index fd8c4cf304d..e226274a4ec 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/components/create-webhook-config-modal.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/components/create-webhook-config-modal.tsx
@@ -1,3 +1,4 @@
+import type { ThirdwebClient } from "thirdweb";
import type { Topic } from "@/api/webhook-configs";
import { WebhookConfigModal } from "./webhook-config-modal";
@@ -7,15 +8,19 @@ interface CreateWebhookConfigModalProps {
teamSlug: string;
projectSlug: string;
topics: Topic[];
+ client?: ThirdwebClient;
+ supportedChainIds?: Array;
}
export function CreateWebhookConfigModal(props: CreateWebhookConfigModalProps) {
return (
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/components/edit-webhook-config-modal.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/components/edit-webhook-config-modal.tsx
index 7c6457dbd03..96932c1610c 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/components/edit-webhook-config-modal.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/components/edit-webhook-config-modal.tsx
@@ -1,3 +1,4 @@
+import type { ThirdwebClient } from "thirdweb";
import type { Topic, WebhookConfig } from "@/api/webhook-configs";
import { WebhookConfigModal } from "./webhook-config-modal";
@@ -8,15 +9,19 @@ interface EditWebhookConfigModalProps {
projectSlug: string;
topics: Topic[];
webhookConfig: WebhookConfig;
+ client?: ThirdwebClient;
+ supportedChainIds?: Array;
}
export function EditWebhookConfigModal(props: EditWebhookConfigModalProps) {
return (
;
+ client?: ThirdwebClient;
+ supportedChainIds?: Array;
}
export function WebhooksOverview({
@@ -23,6 +26,8 @@ export function WebhooksOverview({
webhookConfigs,
topics,
metricsMap,
+ client,
+ supportedChainIds,
}: WebhooksOverviewProps) {
// Feature is enabled (matches server component behavior)
const isFeatureEnabled = true;
@@ -35,9 +40,11 @@ export function WebhooksOverview({
// Show full webhook functionality
return (
void;
+ client?: ThirdwebClient;
+ supportedChainIds?: Array;
}
export function TopicSelectorModal(props: TopicSelectorModalProps) {
@@ -45,6 +72,81 @@ export function TopicSelectorModal(props: TopicSelectorModalProps) {
},
);
+ // Separate forms for event and transaction filters
+ const eventFilterForm = useForm({
+ defaultValues: {
+ abi: "",
+ addresses: "",
+ chainIds: [] as string[],
+ eventTypes: [] as string[],
+ filterType: "event" as const,
+ fromAddresses: "",
+ inputAbi: [] as any[],
+ name: "",
+ secret: "",
+ sigHash: "",
+ sigHashAbi: "",
+ toAddresses: "",
+ webhookUrl: "",
+ },
+ resolver: zodResolver(webhookFormSchema),
+ });
+
+ const transactionFilterForm = useForm({
+ defaultValues: {
+ abi: "",
+ addresses: "",
+ chainIds: [] as string[],
+ eventTypes: [] as string[],
+ filterType: "transaction" as const,
+ fromAddresses: "",
+ inputAbi: [] as any[],
+ name: "",
+ secret: "",
+ sigHash: "",
+ sigHashAbi: "",
+ toAddresses: "",
+ webhookUrl: "",
+ },
+ resolver: zodResolver(webhookFormSchema),
+ });
+
+ const eventChainIds = useWatch({
+ control: eventFilterForm.control,
+ name: "chainIds",
+ });
+ const eventAddresses = useWatch({
+ control: eventFilterForm.control,
+ name: "addresses",
+ });
+ const transactionChainIds = useWatch({
+ control: transactionFilterForm.control,
+ name: "chainIds",
+ });
+ const transactionToAddresses = useWatch({
+ control: transactionFilterForm.control,
+ name: "toAddresses",
+ });
+
+ // ABI processing hooks
+ const eventAbi = useAbiMultiFetch({
+ addresses: eventAddresses || "",
+ chainIds: eventChainIds,
+ extractSignatures: extractEventSignatures,
+ isOpen: props.open,
+ thirdwebClient: props.client,
+ type: "event",
+ });
+
+ const txAbi = useAbiMultiFetch({
+ addresses: transactionToAddresses || "",
+ chainIds: transactionChainIds,
+ extractSignatures: extractFunctionSignatures,
+ isOpen: props.open,
+ thirdwebClient: props.client,
+ type: "transaction",
+ });
+
const groupedTopics = useMemo(() => {
const groups: Record = {};
@@ -76,6 +178,8 @@ export function TopicSelectorModal(props: TopicSelectorModalProps) {
...prev,
{ id: topicId, filters: existingTopic?.filters || null },
]);
+
+ // Set filter type based on topic (no longer needed since we have separate forms)
} else {
setTempSelection((prev) => prev.filter((topic) => topic.id !== topicId));
}
@@ -83,13 +187,105 @@ export function TopicSelectorModal(props: TopicSelectorModalProps) {
function handleSave() {
const processedSelection = tempSelection.map((topic) => {
+ // Handle contract webhook topics with special filter processing
+ if (TOPIC_IDS_THAT_SUPPORT_FILTERS.includes(topic.id)) {
+ let formData: WebhookFormValues;
+
+ // Get form data based on topic type
+ if (topic.id === "insight.event.confirmed") {
+ formData = eventFilterForm.getValues();
+
+ // Validate required fields for events
+ if (!formData.chainIds || formData.chainIds.length === 0) {
+ toast.error("Please select at least one chain for event filters");
+ return topic;
+ }
+ if (!formData.addresses || formData.addresses.trim() === "") {
+ toast.error("Please enter contract addresses for event filters");
+ return topic;
+ }
+
+ // Build event filters
+ const filters: any = {
+ chain_ids: formData.chainIds?.map(String),
+ addresses: formData.addresses
+ ? formData.addresses
+ .split(/[,\s]+/)
+ .map((addr) => addr.trim())
+ .filter(Boolean)
+ : [],
+ };
+
+ if (formData.sigHash) {
+ filters.signatures = [
+ {
+ sig_hash: formData.sigHash.startsWith("0x")
+ ? formData.sigHash
+ : keccak256(new TextEncoder().encode(formData.sigHash)),
+ abi: formData.sigHashAbi || formData.abi,
+ params: {},
+ },
+ ];
+ }
+
+ return { ...topic, filters };
+ } else if (topic.id === "insight.transaction.confirmed") {
+ formData = transactionFilterForm.getValues();
+
+ // Validate required fields for transactions
+ if (!formData.chainIds || formData.chainIds.length === 0) {
+ toast.error(
+ "Please select at least one chain for transaction filters",
+ );
+ return topic;
+ }
+ if (!formData.fromAddresses || formData.fromAddresses.trim() === "") {
+ toast.error("Please enter from addresses for transaction filters");
+ return topic;
+ }
+
+ // Build transaction filters
+ const filters: any = {
+ chain_ids: formData.chainIds?.map(String),
+ from_addresses: formData.fromAddresses
+ ? formData.fromAddresses
+ .split(/[,\s]+/)
+ .map((addr) => addr.trim())
+ .filter(Boolean)
+ : [],
+ };
+
+ if (formData.toAddresses?.trim()) {
+ filters.to_addresses = formData.toAddresses
+ .split(/[,\s]+/)
+ .map((addr) => addr.trim())
+ .filter(Boolean);
+ }
+
+ if (formData.sigHash) {
+ filters.signatures = [
+ {
+ sig_hash: formData.sigHash.startsWith("0x")
+ ? formData.sigHash
+ : toFunctionSelector(formData.sigHash),
+ abi: formData.sigHashAbi || formData.abi,
+ params: {},
+ },
+ ];
+ }
+
+ return { ...topic, filters };
+ }
+ }
+
+ // Handle other topics with simple JSON filters
const filters = topicFilters[topic.id];
if (filters) {
try {
return { ...topic, filters: JSON.parse(filters) };
} catch (_error) {
toast.error(`Invalid JSON in filters for ${topic.id}`);
- throw new Error(`Invalid JSON in filters for ${topic.id}`);
+ return topic;
}
}
return topic;
@@ -114,7 +310,7 @@ export function TopicSelectorModal(props: TopicSelectorModalProps) {
return (
- {/* Show textarea when selecting a topic that supports filters */}
+ {/* Show contract webhook filter form when selecting contract webhook topics */}
{TOPIC_IDS_THAT_SUPPORT_FILTERS.includes(topic.id) &&
+ tempSelection.some((t) => t.id === topic.id) &&
+ props.client &&
+ props.supportedChainIds && (
+
+
+ Configure{" "}
+ {topic.id === "insight.event.confirmed"
+ ? "Event"
+ : "Transaction"}{" "}
+ Filters
+
+
+ {topic.id === "insight.event.confirmed" ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+ {/* Show fallback for contract webhook topics when client/chain IDs not available */}
+ {TOPIC_IDS_THAT_SUPPORT_FILTERS.includes(topic.id) &&
+ tempSelection.some((t) => t.id === topic.id) &&
+ (!props.client || !props.supportedChainIds) && (
+
+ )}
+
+ {/* Show simple textarea for other topics that support filters */}
+ {!TOPIC_IDS_THAT_SUPPORT_FILTERS.includes(topic.id) &&
tempSelection.some((t) => t.id === topic.id) && (