Skip to content

Commit 8b7718d

Browse files
author
Amine Afia
committed
Add webhooks feature to Insight dashboard
1 parent 1e82083 commit 8b7718d

File tree

9 files changed

+2283
-77
lines changed

9 files changed

+2283
-77
lines changed
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
"use server";
2+
3+
const INSIGHT_API_BASE_URL = "https://insight.thirdweb-dev.com";
4+
5+
export interface WebhookResponse {
6+
id: string;
7+
team_id: string;
8+
project_id: string;
9+
webhook_url: string;
10+
webhook_secret: string;
11+
filters: WebhookFilters;
12+
suspended_at: string | null;
13+
suspended_reason: string | null;
14+
disabled: boolean;
15+
created_at: string;
16+
updated_at: string | null;
17+
}
18+
19+
export interface WebhookFilters {
20+
"v1.events"?: {
21+
chain_ids?: string[];
22+
addresses?: string[];
23+
signatures?: Array<{
24+
sig_hash: string;
25+
abi?: string;
26+
params?: Record<string, unknown>;
27+
}>;
28+
};
29+
"v1.transactions"?: {
30+
chain_ids?: string[];
31+
from_addresses?: string[];
32+
to_addresses?: string[];
33+
signatures?: Array<{
34+
sig_hash: string;
35+
abi?: string;
36+
params?: string[];
37+
}>;
38+
};
39+
}
40+
41+
interface CreateWebhookPayload {
42+
webhook_url: string;
43+
filters: WebhookFilters;
44+
}
45+
46+
interface WebhooksListResponse {
47+
data: WebhookResponse[];
48+
}
49+
50+
interface WebhookSingleResponse {
51+
data: WebhookResponse;
52+
}
53+
54+
interface TestWebhookPayload {
55+
webhook_url: string;
56+
type?: "event" | "transaction";
57+
}
58+
59+
interface TestWebhookResponse {
60+
success: boolean;
61+
}
62+
63+
// Create a new webhook
64+
export async function createWebhook(
65+
payload: CreateWebhookPayload,
66+
clientId: string,
67+
): Promise<WebhookSingleResponse> {
68+
const response = await fetch(`${INSIGHT_API_BASE_URL}/v1/webhooks`, {
69+
method: "POST",
70+
headers: {
71+
"Content-Type": "application/json",
72+
"x-client-id": clientId,
73+
},
74+
body: JSON.stringify(payload),
75+
});
76+
77+
console.log({
78+
method: "POST",
79+
headers: {
80+
"Content-Type": "application/json",
81+
"x-client-id": clientId,
82+
},
83+
body: JSON.stringify(payload),
84+
});
85+
86+
if (!response.ok) {
87+
const errorText = await response.text();
88+
throw new Error(`Failed to create webhook: ${errorText}`);
89+
}
90+
91+
return await response.json();
92+
}
93+
94+
// Get all webhooks for a project
95+
export async function getWebhooks(
96+
clientId: string,
97+
): Promise<WebhooksListResponse> {
98+
const response = await fetch(`${INSIGHT_API_BASE_URL}/v1/webhooks`, {
99+
method: "GET",
100+
headers: {
101+
"x-client-id": clientId,
102+
},
103+
});
104+
105+
if (!response.ok) {
106+
const errorText = await response.text();
107+
throw new Error(`Failed to get webhooks: ${errorText}`);
108+
}
109+
110+
return await response.json();
111+
}
112+
113+
// Delete a webhook by ID
114+
export async function deleteWebhook(
115+
webhookId: string,
116+
clientId: string,
117+
): Promise<WebhookSingleResponse> {
118+
const response = await fetch(
119+
`${INSIGHT_API_BASE_URL}/v1/webhooks/${webhookId}`,
120+
{
121+
method: "DELETE",
122+
headers: {
123+
"x-client-id": clientId,
124+
},
125+
},
126+
);
127+
128+
if (!response.ok) {
129+
const errorText = await response.text();
130+
throw new Error(`Failed to delete webhook: ${errorText}`);
131+
}
132+
133+
return await response.json();
134+
}
135+
136+
// Test a webhook
137+
export async function testWebhook(
138+
payload: TestWebhookPayload,
139+
clientId: string,
140+
): Promise<TestWebhookResponse> {
141+
const response = await fetch(`${INSIGHT_API_BASE_URL}/v1/webhooks/test`, {
142+
method: "POST",
143+
headers: {
144+
"Content-Type": "application/json",
145+
"x-client-id": clientId,
146+
},
147+
body: JSON.stringify(payload),
148+
});
149+
150+
if (!response.ok) {
151+
const errorText = await response.text();
152+
throw new Error(`Failed to test webhook: ${errorText}`);
153+
}
154+
155+
return await response.json();
156+
}

apps/dashboard/src/@/components/blocks/SidebarLayout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ function RenderSidebarGroup(props: {
137137
}
138138

139139
if ("separator" in link) {
140-
return <SidebarSeparator className="my-1" />;
140+
return <SidebarSeparator className="my-1" key="separator" />;
141141
}
142142

143143
return (
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
"use client";
2+
3+
import { TabPathLinks } from "@/components/ui/tabs";
4+
import { FooterLinksSection } from "../components/footer/FooterLinksSection";
5+
6+
export function InsightPageLayout(props: {
7+
projectSlug: string;
8+
projectId: string;
9+
teamSlug: string;
10+
children: React.ReactNode;
11+
}) {
12+
const insightLayoutSlug = `/team/${props.teamSlug}/${props.projectSlug}/insight`;
13+
14+
return (
15+
<div className="flex grow flex-col">
16+
{/* header */}
17+
<div className="pt-4 lg:pt-6">
18+
<div className="container flex max-w-7xl flex-col gap-4">
19+
<div>
20+
<h1 className="mb-1 font-semibold text-2xl tracking-tight lg:text-3xl">
21+
Insight
22+
</h1>
23+
<p className="text-muted-foreground text-sm">
24+
APIs to retrieve blockchain data from any EVM chain, enrich it
25+
with metadata, and transform it using custom logic
26+
</p>
27+
</div>
28+
</div>
29+
30+
<div className="h-4" />
31+
32+
{/* Nav */}
33+
<TabPathLinks
34+
scrollableClassName="container max-w-7xl"
35+
links={[
36+
{
37+
name: "Overview",
38+
path: `${insightLayoutSlug}`,
39+
exactMatch: true,
40+
},
41+
{
42+
name: "Webhooks",
43+
path: `${insightLayoutSlug}/webhooks`,
44+
},
45+
]}
46+
/>
47+
</div>
48+
49+
{/* content */}
50+
<div className="h-6" />
51+
<div className="container flex max-w-7xl grow flex-col gap-6">
52+
<div>{props.children}</div>
53+
</div>
54+
<div className="h-20" />
55+
56+
{/* footer */}
57+
<div className="border-border border-t">
58+
<div className="container max-w-7xl">
59+
<InsightFooter />
60+
</div>
61+
</div>
62+
</div>
63+
);
64+
}
65+
66+
function InsightFooter() {
67+
return (
68+
<FooterLinksSection
69+
left={{
70+
title: "Documentation",
71+
links: [
72+
{
73+
label: "Overview",
74+
href: "https://portal.thirdweb.com/insight",
75+
},
76+
{
77+
label: "API Reference",
78+
href: "https://insight-api.thirdweb.com/reference",
79+
},
80+
],
81+
}}
82+
center={{
83+
title: "Tutorials",
84+
links: [
85+
{
86+
label:
87+
"Blockchain Data on Any EVM - Quick and Easy REST APIs for Onchain Data",
88+
href: "https://www.youtube.com/watch?v=U2aW7YIUJVw",
89+
},
90+
{
91+
label: "Build a Whale Alerts Telegram Bot with Insight",
92+
href: "https://www.youtube.com/watch?v=HvqewXLVRig",
93+
},
94+
],
95+
}}
96+
right={{
97+
title: "Demos",
98+
links: [
99+
{
100+
label: "API Playground",
101+
href: "https://playground.thirdweb.com/insight",
102+
},
103+
],
104+
}}
105+
trackingCategory="insight"
106+
/>
107+
);
108+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { getProject } from "@/api/projects";
2+
import { getTeamBySlug } from "@/api/team";
3+
import { redirect } from "next/navigation";
4+
import { InsightPageLayout } from "./InsightPageLayout";
5+
6+
export default async function Layout(props: {
7+
params: Promise<{ team_slug: string; project_slug: string }>;
8+
children: React.ReactNode;
9+
}) {
10+
const { team_slug, project_slug } = await props.params;
11+
12+
const [team, project] = await Promise.all([
13+
getTeamBySlug(team_slug),
14+
getProject(team_slug, project_slug),
15+
]);
16+
17+
if (!team) {
18+
redirect("/team");
19+
}
20+
21+
if (!project) {
22+
redirect(`/team/${team_slug}`);
23+
}
24+
25+
return (
26+
<InsightPageLayout
27+
projectSlug={project.slug}
28+
teamSlug={team_slug}
29+
projectId={project.id}
30+
>
31+
{props.children}
32+
</InsightPageLayout>
33+
);
34+
}

0 commit comments

Comments
 (0)