From 704ba1a1eee065a776e4002792ae7c92922d54e3 Mon Sep 17 00:00:00 2001 From: iuwqyir Date: Fri, 11 Jul 2025 21:50:02 +0300 Subject: [PATCH] add insight filter selection to webhook creation and update --- .../create-webhook-config-modal.tsx | 5 + .../components/edit-webhook-config-modal.tsx | 5 + .../webhooks/components/overview.tsx | 7 + .../components/topic-selector-modal.tsx | 747 +++++++++++++++++- .../components/webhook-config-modal.tsx | 48 +- .../components/webhook-configs-table.tsx | 7 + .../webhooks/hooks/useAbiProcessing.ts | 8 +- .../(sidebar)/webhooks/page.tsx | 34 +- 8 files changed, 829 insertions(+), 32 deletions(-) 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 ( - + Select Topics @@ -149,8 +345,553 @@ export function TopicSelectorModal(props: TopicSelectorModalProps) { {topic.description}

- {/* 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" ? ( +
+
+ {/* Chain IDs Field */} + ( + +
+ + Chain IDs{" "} + + * + + +
+ + {props.client ? ( + + field.onChange( + chainIds.map(String), + ) + } + selectedChainIds={ + Array.isArray(field.value) + ? field.value.map(Number) + : [] + } + /> + ) : ( +
+ Client not available +
+ )} +
+ +
+ )} + /> + + {/* Contract Addresses for Events */} + ( + +
+ + Contract Addresses{" "} + + * + + +
+ +
+ + + {/* ABI fetch status */} +
+ {topic.id === + "insight.event.confirmed" && + eventAbi.isFetching && ( +
+ + + Fetching ABIs... + +
+ )} +
+ + {/* ABI fetch results */} + {(Object.keys( + eventAbi.fetchedAbis, + ).length > 0 || + Object.keys(eventAbi.errors) + .length > 0) && ( +
+ {Object.keys( + eventAbi.fetchedAbis, + ).length > 0 && ( +
+ + ✓{" "} + { + Object.keys( + eventAbi.fetchedAbis, + ).length + }{" "} + ABI + {Object.keys( + eventAbi.fetchedAbis, + ).length !== 1 + ? "s" + : ""}{" "} + fetched + +
+ )} + + {Object.keys(eventAbi.errors) + .length > 0 && ( +
+ + ✗{" "} + { + Object.keys( + eventAbi.errors, + ).length + }{" "} + error + {Object.keys( + eventAbi.errors, + ).length !== 1 + ? "s" + : ""} + +
+ )} +
+ )} +
+
+ +
+ )} + /> + + {/* Signature Hash Field */} + ( + +
+ + {topic.id === + "insight.event.confirmed" + ? "Event Signature (optional)" + : "Function Signature (optional)"} + +
+ + {topic.id === + "insight.event.confirmed" && + Object.keys(eventAbi.fetchedAbis) + .length > 0 && + eventAbi.signatures.length > 0 ? ( + { + field.onChange(val); + // If custom signature, clear ABI field + const known = + eventAbi.signatures.map( + (sig) => sig.signature, + ); + if ( + val && + !known.includes(val) + ) { + eventFilterForm.setValue( + "abi", + "", + ); + } + }} + options={eventAbi.signatures.map( + (sig) => ({ + abi: sig.abi, + label: truncateMiddle( + sig.name, + 30, + 15, + ), + value: sig.signature, + }), + )} + placeholder="Select or enter an event signature" + setAbi={(abi) => + eventFilterForm.setValue( + "sigHashAbi", + abi, + ) + } + value={field.value || ""} + /> + ) : topic.id === + "insight.transaction.confirmed" && + Object.keys(txAbi.fetchedAbis) + .length > 0 && + txAbi.signatures.length > 0 ? ( + { + field.onChange(val); + // If custom signature, clear ABI field + const known = + txAbi.signatures.map( + (sig) => sig.signature, + ); + if ( + val && + !known.includes(val) + ) { + transactionFilterForm.setValue( + "abi", + "", + ); + } + }} + options={txAbi.signatures.map( + (sig) => ({ + abi: sig.abi, + label: truncateMiddle( + sig.name, + 30, + 15, + ), + value: sig.signature, + }), + )} + placeholder="Select or enter a function signature" + setAbi={(abi) => + transactionFilterForm.setValue( + "sigHashAbi", + abi, + ) + } + value={field.value || ""} + /> + ) : ( + + )} + + +
+ )} + /> +
+
+ ) : ( +
+
+ {/* Chain IDs Field */} + ( + +
+ + Chain IDs{" "} + + * + + +
+ + {props.client ? ( + + field.onChange( + chainIds.map(String), + ) + } + selectedChainIds={ + Array.isArray(field.value) + ? field.value.map(Number) + : [] + } + /> + ) : ( +
+ Client not available +
+ )} +
+ +
+ )} + /> + + {/* From/To Addresses for Transactions */} + ( + +
+ + From Address{" "} + + * + + +
+ + + + +
+ )} + /> + + ( + +
+ To Address +
+ +
+ + + {/* ABI fetch status */} +
+ {txAbi.isFetching && ( +
+ + + Fetching ABIs... + +
+ )} +
+ + {/* ABI fetch results */} + {(Object.keys(txAbi.fetchedAbis) + .length > 0 || + Object.keys(txAbi.errors) + .length > 0) && ( +
+ {Object.keys( + txAbi.fetchedAbis, + ).length > 0 && ( +
+ + ✓{" "} + { + Object.keys( + txAbi.fetchedAbis, + ).length + }{" "} + ABI + {Object.keys( + txAbi.fetchedAbis, + ).length !== 1 + ? "s" + : ""}{" "} + fetched + +
+ )} + + {Object.keys(txAbi.errors) + .length > 0 && ( +
+ + ⚠️{" "} + { + Object.keys( + txAbi.errors, + ).length + }{" "} + error + {Object.keys( + txAbi.errors, + ).length !== 1 + ? "s" + : ""} + +
+ )} +
+ )} +
+
+ +
+ )} + /> + + {/* Signature Hash Field */} + ( + +
+ + Function Signature (optional) + +
+ + {Object.keys(txAbi.fetchedAbis) + .length > 0 && + txAbi.signatures.length > 0 ? ( + { + field.onChange(val); + // If custom signature, clear ABI field + const known = + txAbi.signatures.map( + (sig) => sig.signature, + ); + if ( + val && + !known.includes(val) + ) { + transactionFilterForm.setValue( + "abi", + "", + ); + } + }} + options={txAbi.signatures.map( + (sig) => ({ + abi: sig.abi, + label: truncateMiddle( + sig.name, + 30, + 15, + ), + value: sig.signature, + }), + )} + placeholder="Select or enter a function signature" + setAbi={(abi) => + transactionFilterForm.setValue( + "sigHashAbi", + abi, + ) + } + value={field.value || ""} + /> + ) : ( + + )} + + +
+ )} + /> +
+
+ )} +
+ )} + + {/* 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) && ( +
+