Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b98c228f26 | ||
|
|
5a08176eb0 | ||
|
|
647c285136 | ||
|
|
45ca0334bb | ||
|
|
a0f355717a | ||
|
|
b182302f59 | ||
|
|
942d20a5d9 | ||
|
|
739a2ae3c5 | ||
|
|
4158a796a8 | ||
|
|
a258e7eaf0 | ||
|
|
e386ac9476 | ||
|
|
0574eac02c | ||
|
|
52cbcd2d57 | ||
|
|
387891dde0 | ||
|
|
562c534d37 | ||
|
|
b882e29935 | ||
|
|
9ad1d3cbbf | ||
|
|
aab3da0729 | ||
|
|
78c9d2fe13 | ||
|
|
557d3e3dbd | ||
|
|
06d6504545 | ||
|
|
e201e3d423 | ||
|
|
c8ef941d8e | ||
|
|
39a5b5c8a3 | ||
|
|
6f381d768c | ||
|
|
0c5a51c5bf | ||
|
|
90428b4574 | ||
|
|
e24547bed1 | ||
|
|
3ff8c3fbb2 | ||
|
|
4bd03b06e7 | ||
|
|
b7158c2b54 | ||
|
|
5f26883fbd | ||
|
|
98b6198687 | ||
|
|
880148b793 | ||
|
|
ee1bc3eb07 | ||
|
|
e9ab8cb770 | ||
|
|
9c33981b72 | ||
|
|
c645b42684 | ||
|
|
7583074c06 | ||
|
|
fbafb65f5c | ||
|
|
6b286c7e83 | ||
|
|
159ffbfaee | ||
|
|
0c1bb6ccae | ||
|
|
dceb47de8b | ||
|
|
dfbdbd06e5 | ||
|
|
66bbedbe3a | ||
|
|
2fbf105d15 | ||
|
|
afcaa91873 | ||
|
|
51f955352d | ||
|
|
6b2a90ac89 | ||
|
|
cf53184bb1 | ||
|
|
815b9b1ce1 | ||
|
|
ae1baa5105 | ||
|
|
92266b7459 | ||
|
|
80ad84b803 | ||
|
|
633f0cadf1 | ||
|
|
91b0367b23 | ||
|
|
c86e13b522 | ||
|
|
bc42c2fdfb | ||
|
|
cad9c74bef | ||
|
|
6f2f92ddea | ||
|
|
97c9ab7e1c | ||
|
|
b73fa4b16f | ||
|
|
cbfe9ec459 | ||
|
|
fecd3a6c16 | ||
|
|
8aeb4d6308 | ||
|
|
f1cd2539d1 | ||
|
|
a322e16853 | ||
|
|
e50d77d28f | ||
|
|
7f3b91da37 | ||
|
|
41d50443d9 | ||
|
|
a8ac98bd49 | ||
|
|
0781da9dca | ||
|
|
c5dfad8dbb | ||
|
|
9663079062 | ||
|
|
1f6bb7c329 | ||
|
|
30e13e6b91 | ||
|
|
2dfc245d44 | ||
|
|
64d7bf358f | ||
|
|
a6e9c97c6f | ||
|
|
ee57ded902 | ||
|
|
6d3947ab0c | ||
|
|
632f1d138f | ||
|
|
18f0e575c4 | ||
|
|
e732b11e45 | ||
|
|
f77165e2d1 | ||
|
|
dfd64d2896 | ||
|
|
4e703fdb5d | ||
|
|
af88935680 | ||
|
|
c50c82aacd | ||
|
|
0fa8354dec | ||
|
|
769a927627 | ||
|
|
ceff873cba | ||
|
|
9a432af1cf | ||
|
|
ee6bd4f898 | ||
|
|
ccf6329810 | ||
|
|
821caa63d7 | ||
|
|
09e396d856 | ||
|
|
9573b2f9d2 | ||
|
|
b004a07e5d | ||
|
|
2396b4e461 | ||
|
|
ca6c913361 | ||
|
|
cf166e4c04 | ||
|
|
a51192ffbd | ||
|
|
6fd36aabc7 | ||
|
|
08cc48910d | ||
|
|
f03ded300f | ||
|
|
b651bae4ba | ||
|
|
66833a832c | ||
|
|
9d5828eeab | ||
|
|
b191658fcd | ||
|
|
a6b7e8d818 | ||
|
|
f4f93e6353 | ||
|
|
b693b78645 | ||
|
|
daceeed2ec | ||
|
|
5f7c726917 | ||
|
|
2405456e5f | ||
|
|
a76e6534c4 | ||
|
|
0fb586db52 | ||
|
|
f8ebbe54d9 | ||
|
|
ca756819db | ||
|
|
7217e774b2 | ||
|
|
5de72c5ecd | ||
|
|
60c5c26583 | ||
|
|
dc74c3b4b9 | ||
|
|
69a921d01f | ||
|
|
9abdb92028 | ||
|
|
6ed2fd13de | ||
|
|
a22457a69c | ||
|
|
ae675db8c8 | ||
|
|
54c9333fc9 | ||
|
|
541cf0b3e9 |
@@ -9,7 +9,8 @@ module.exports = [
|
||||
// -- connect
|
||||
"/connect",
|
||||
"/connect/sign-in",
|
||||
"/connect/pay",
|
||||
"/connect/account-abstraction",
|
||||
"/connect/universal-bridge",
|
||||
// -- nebula
|
||||
"/nebula",
|
||||
// --insight
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
||||
|
||||
@@ -46,9 +46,9 @@
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-switch": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "1.1.7",
|
||||
"@sentry/nextjs": "8.52.0",
|
||||
"@sentry/nextjs": "8.53.0",
|
||||
"@shazow/whatsabi": "^0.19.0",
|
||||
"@tanstack/react-query": "5.65.1",
|
||||
"@tanstack/react-query": "5.66.0",
|
||||
"@tanstack/react-table": "^8.20.6",
|
||||
"@thirdweb-dev/service-utils": "workspace:*",
|
||||
"@vercel/functions": "^1.6.0",
|
||||
@@ -72,7 +72,6 @@
|
||||
"next-plausible": "^3.12.4",
|
||||
"next-themes": "^0.4.4",
|
||||
"nextjs-toploader": "^1.6.12",
|
||||
"openapi-types": "^12.1.3",
|
||||
"p-limit": "^6.2.0",
|
||||
"papaparse": "^5.5.2",
|
||||
"pluralize": "^8.0.0",
|
||||
@@ -89,9 +88,10 @@
|
||||
"react-table": "^7.8.0",
|
||||
"recharts": "2.15.1",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"responsive-rsc": "0.0.7",
|
||||
"server-only": "^0.0.1",
|
||||
"shiki": "1.27.0",
|
||||
"sonner": "^1.7.3",
|
||||
"sonner": "^1.7.4",
|
||||
"spdx-correct": "^3.2.0",
|
||||
"swagger-ui-react": "^5.18.3",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
@@ -106,7 +106,7 @@
|
||||
"@chromatic-com/storybook": "3.2.4",
|
||||
"@next/bundle-analyzer": "15.1.6",
|
||||
"@next/eslint-plugin-next": "15.1.6",
|
||||
"@playwright/test": "1.50.0",
|
||||
"@playwright/test": "1.50.1",
|
||||
"@storybook/addon-essentials": "8.5.2",
|
||||
"@storybook/addon-interactions": "8.5.2",
|
||||
"@storybook/addon-links": "8.5.2",
|
||||
@@ -117,7 +117,7 @@
|
||||
"@storybook/react": "8.5.2",
|
||||
"@storybook/test": "8.5.2",
|
||||
"@types/color": "4.2.0",
|
||||
"@types/node": "22.10.10",
|
||||
"@types/node": "22.13.0",
|
||||
"@types/papaparse": "^5.3.15",
|
||||
"@types/pluralize": "^0.0.33",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
|
||||
|
Before Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 583 KiB |
|
Before Width: | Height: | Size: 167 KiB |
|
Before Width: | Height: | Size: 295 KiB |
|
Before Width: | Height: | Size: 319 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 333 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 234 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 182 KiB |
|
Before Width: | Height: | Size: 258 KiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 221 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 956 KiB |
|
Before Width: | Height: | Size: 695 KiB |
|
Before Width: | Height: | Size: 620 KiB |
|
Before Width: | Height: | Size: 709 KiB |
|
Before Width: | Height: | Size: 906 KiB |
|
Before Width: | Height: | Size: 551 KiB |
|
Before Width: | Height: | Size: 601 KiB |
|
Before Width: | Height: | Size: 541 KiB |
|
Before Width: | Height: | Size: 268 KiB |
|
Before Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 204 KiB |
|
Before Width: | Height: | Size: 733 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 3.4 MiB |
|
Before Width: | Height: | Size: 333 KiB |
@@ -331,6 +331,18 @@ async function redirects() {
|
||||
destination: "/templates/:slug",
|
||||
permanent: false,
|
||||
},
|
||||
// redirect /account-abstraction to /connect/account-abstraction
|
||||
{
|
||||
source: "/account-abstraction",
|
||||
destination: "/connect/account-abstraction",
|
||||
permanent: false,
|
||||
},
|
||||
// redirect /connect/pay to /connect/universal-bridge
|
||||
{
|
||||
source: "/connect/pay",
|
||||
destination: "/connect/universal-bridge",
|
||||
permanent: false,
|
||||
},
|
||||
// PREVIOUS CAMPAIGNS
|
||||
{
|
||||
source: "/unlimited-wallets",
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { promises as fs } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
// after running logUnusedAssets.ts,
|
||||
// 1. validate the output
|
||||
// 2. If you think that a file should not be deleted, remove it from the output
|
||||
// 3. paste the output files in `filesToDelete` and run this script to delete them
|
||||
const filesToDelete: string[] = [];
|
||||
|
||||
async function deleteFiles() {
|
||||
for (const filePath of filesToDelete) {
|
||||
try {
|
||||
const fullPath = path.join(process.cwd(), filePath);
|
||||
await fs.unlink(fullPath);
|
||||
console.log(`Deleted: ${filePath}`);
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
console.warn(`File not found: ${filePath}`);
|
||||
} else {
|
||||
console.error(`Error deleting ${filePath}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("--- DONE ---");
|
||||
}
|
||||
|
||||
deleteFiles();
|
||||
@@ -26,9 +26,6 @@ const filesToIgnore = new Set([
|
||||
// macOS stuff
|
||||
"public/assets/product-pages/.DS_Store",
|
||||
"public/assets/.DS_Store",
|
||||
// pdfs
|
||||
"public/Thirdweb_Terms_of_Service.pdf",
|
||||
"public/thirdweb_Privacy_Policy_May_2022.pdf",
|
||||
]);
|
||||
|
||||
function getAllFilesInFolder(folderPath: string) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { API_SERVER_URL } from "../constants/env";
|
||||
export async function updateAccount(values: {
|
||||
name?: string;
|
||||
email?: string;
|
||||
image?: string | null;
|
||||
}) {
|
||||
const token = await getAuthToken();
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import "server-only";
|
||||
import { API_SERVER_URL, THIRDWEB_API_SECRET } from "../constants/env";
|
||||
|
||||
export async function getGasSponsoredChains() {
|
||||
if (!THIRDWEB_API_SECRET) {
|
||||
throw new Error("API_SERVER_SECRET is not set");
|
||||
}
|
||||
const res = await fetch(`${API_SERVER_URL}/v1/chains/gas-sponsored`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-service-api-key": THIRDWEB_API_SECRET,
|
||||
},
|
||||
next: {
|
||||
revalidate: 15 * 60, //15 minutes
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
console.error(
|
||||
"Failed to fetch gas sponsored chains",
|
||||
res.status,
|
||||
res.statusText,
|
||||
);
|
||||
res.body?.cancel();
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
return (await res.json()).data as number[];
|
||||
} catch (e) {
|
||||
console.error("Failed to parse gas sponsored chains", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ export type Team = {
|
||||
image?: string;
|
||||
billingPlan: "pro" | "growth" | "free" | "starter";
|
||||
billingStatus: "validPayment" | (string & {}) | null;
|
||||
supportPlan: "pro" | "growth" | "free" | "starter";
|
||||
billingEmail: string | null;
|
||||
growthTrialEligible: false;
|
||||
enabledScopes: EnabledTeamScope[];
|
||||
@@ -47,6 +48,10 @@ export async function getTeamBySlug(slug: string) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getTeamById(id: string) {
|
||||
return getTeamBySlug(id);
|
||||
}
|
||||
|
||||
export async function getTeams() {
|
||||
const token = await getAuthToken();
|
||||
if (!token) {
|
||||
|
||||
@@ -28,7 +28,7 @@ export const Mobile: Story = {
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="container flex max-w-[1000px] flex-col gap-8 lg:p-10">
|
||||
<div className="container flex max-w-[1000px] flex-col gap-8 py-10">
|
||||
<BadgeContainer label="Base">
|
||||
<DangerSettingCard
|
||||
title="This is a title"
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { AsteriskIcon, InfoIcon } from "lucide-react";
|
||||
import type React from "react";
|
||||
|
||||
export function FormFieldSetup(props: {
|
||||
htmlFor?: string;
|
||||
label: string;
|
||||
label: React.ReactNode;
|
||||
errorMessage: React.ReactNode | undefined;
|
||||
children: React.ReactNode;
|
||||
tooltip?: React.ReactNode;
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
type ChartConfig,
|
||||
ChartContainer,
|
||||
@@ -15,8 +22,14 @@ import {
|
||||
EmptyChartState,
|
||||
LoadingChartState,
|
||||
} from "../../../../components/analytics/empty-chart-state";
|
||||
import { cn } from "../../../lib/utils";
|
||||
|
||||
type ThirdwebAreaChartProps<TConfig extends ChartConfig> = {
|
||||
header?: {
|
||||
title: string;
|
||||
description?: string;
|
||||
titleClassName?: string;
|
||||
};
|
||||
// chart config
|
||||
config: TConfig;
|
||||
data: Array<Record<keyof TConfig, number> & { time: number | string | Date }>;
|
||||
@@ -25,78 +38,99 @@ type ThirdwebAreaChartProps<TConfig extends ChartConfig> = {
|
||||
// chart className
|
||||
chartClassName?: string;
|
||||
isPending: boolean;
|
||||
hideLabel?: boolean;
|
||||
toolTipLabelFormatter?: (label: string, payload: unknown) => React.ReactNode;
|
||||
};
|
||||
|
||||
export function ThirdwebAreaChart<TConfig extends ChartConfig>(
|
||||
props: ThirdwebAreaChartProps<TConfig>,
|
||||
) {
|
||||
const configKeys = useMemo(() => Object.keys(props.config), [props.config]);
|
||||
return (
|
||||
<div className="rounded-lg border border-border px-4 pt-10 pb-4">
|
||||
<ChartContainer config={props.config} className={props.chartClassName}>
|
||||
{props.isPending ? (
|
||||
<LoadingChartState />
|
||||
) : props.data.length === 0 ? (
|
||||
<EmptyChartState />
|
||||
) : (
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
data={props.data}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={20}
|
||||
tickFormatter={(value) => formatDate(new Date(value), "MMM dd")}
|
||||
/>
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
|
||||
<defs>
|
||||
{configKeys.map((key) => (
|
||||
<linearGradient
|
||||
key={key}
|
||||
id={`fill_${key}`}
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop
|
||||
offset="5%"
|
||||
stopColor={`var(--color-${key})`}
|
||||
stopOpacity={0.8}
|
||||
/>
|
||||
<stop
|
||||
offset="95%"
|
||||
stopColor={`var(--color-${key})`}
|
||||
stopOpacity={0.1}
|
||||
/>
|
||||
</linearGradient>
|
||||
))}
|
||||
</defs>
|
||||
{configKeys.map((key) => (
|
||||
<Area
|
||||
key={key}
|
||||
dataKey={key}
|
||||
type="natural"
|
||||
fill={`url(#fill_${key})`}
|
||||
fillOpacity={0.4}
|
||||
stroke={`var(--color-${key})`}
|
||||
stackId="a"
|
||||
/>
|
||||
))}
|
||||
|
||||
{props.showLegend && (
|
||||
<ChartLegend content={<ChartLegendContent />} className="pt-8" />
|
||||
)}
|
||||
</AreaChart>
|
||||
)}
|
||||
</ChartContainer>
|
||||
</div>
|
||||
return (
|
||||
<Card>
|
||||
{props.header && (
|
||||
<CardHeader>
|
||||
<CardTitle className={cn("mb-2", props.header.titleClassName)}>
|
||||
{props.header.title}
|
||||
</CardTitle>
|
||||
{props.header.description && (
|
||||
<CardDescription>{props.header.description}</CardDescription>
|
||||
)}
|
||||
</CardHeader>
|
||||
)}
|
||||
|
||||
<CardContent className={cn(!props.header && "pt-6")}>
|
||||
<ChartContainer config={props.config} className={props.chartClassName}>
|
||||
{props.isPending ? (
|
||||
<LoadingChartState />
|
||||
) : props.data.length === 0 ? (
|
||||
<EmptyChartState />
|
||||
) : (
|
||||
<AreaChart accessibilityLayer data={props.data}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={20}
|
||||
tickFormatter={(value) => formatDate(new Date(value), "MMM dd")}
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
hideLabel={
|
||||
props.hideLabel !== undefined ? props.hideLabel : true
|
||||
}
|
||||
labelFormatter={props.toolTipLabelFormatter}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<defs>
|
||||
{configKeys.map((key) => (
|
||||
<linearGradient
|
||||
key={key}
|
||||
id={`fill_${key}`}
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop
|
||||
offset="5%"
|
||||
stopColor={`var(--color-${key})`}
|
||||
stopOpacity={0.8}
|
||||
/>
|
||||
<stop
|
||||
offset="95%"
|
||||
stopColor={`var(--color-${key})`}
|
||||
stopOpacity={0.1}
|
||||
/>
|
||||
</linearGradient>
|
||||
))}
|
||||
</defs>
|
||||
{configKeys.map((key) => (
|
||||
<Area
|
||||
key={key}
|
||||
dataKey={key}
|
||||
type="natural"
|
||||
fill={`url(#fill_${key})`}
|
||||
fillOpacity={0.4}
|
||||
stroke={`var(--color-${key})`}
|
||||
stackId="a"
|
||||
/>
|
||||
))}
|
||||
|
||||
{props.showLegend && (
|
||||
<ChartLegend
|
||||
content={<ChartLegendContent className="pt-8" />}
|
||||
/>
|
||||
)}
|
||||
</AreaChart>
|
||||
)}
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,8 +27,11 @@ import { cn } from "../../../lib/utils";
|
||||
|
||||
type ThirdwebBarChartProps<TConfig extends ChartConfig> = {
|
||||
// metadata
|
||||
title: string;
|
||||
description?: string;
|
||||
header?: {
|
||||
title: string;
|
||||
description?: string;
|
||||
titleClassName?: string;
|
||||
};
|
||||
// chart config
|
||||
config: TConfig;
|
||||
data: Array<Record<keyof TConfig, number> & { time: number | string | Date }>;
|
||||
@@ -39,7 +42,6 @@ type ThirdwebBarChartProps<TConfig extends ChartConfig> = {
|
||||
isPending: boolean;
|
||||
toolTipLabelFormatter?: (label: string, payload: unknown) => React.ReactNode;
|
||||
hideLabel?: boolean;
|
||||
titleClassName?: string;
|
||||
};
|
||||
|
||||
export function ThirdwebBarChart<TConfig extends ChartConfig>(
|
||||
@@ -52,15 +54,18 @@ export function ThirdwebBarChart<TConfig extends ChartConfig>(
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className={cn("mb-2", props.titleClassName)}>
|
||||
{props.title}
|
||||
</CardTitle>
|
||||
{props.description && (
|
||||
<CardDescription>{props.description}</CardDescription>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{props.header && (
|
||||
<CardHeader>
|
||||
<CardTitle className={cn("mb-2", props.header.titleClassName)}>
|
||||
{props.header.title}
|
||||
</CardTitle>
|
||||
{props.header.description && (
|
||||
<CardDescription>{props.header.description}</CardDescription>
|
||||
)}
|
||||
</CardHeader>
|
||||
)}
|
||||
|
||||
<CardContent className={cn(!props.header && "pt-6")}>
|
||||
<ChartContainer config={props.config} className={props.chartClassName}>
|
||||
{props.isPending ? (
|
||||
<LoadingChartState />
|
||||
@@ -87,7 +92,9 @@ export function ThirdwebBarChart<TConfig extends ChartConfig>(
|
||||
}
|
||||
/>
|
||||
{props.showLegend && (
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
<ChartLegend
|
||||
content={<ChartLegendContent className="pt-5" />}
|
||||
/>
|
||||
)}
|
||||
{configKeys.map((key, idx) => (
|
||||
<Bar
|
||||
|
||||
@@ -2,27 +2,29 @@
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ClientOnly } from "components/ClientOnly/ClientOnly";
|
||||
import { Moon, Sun } from "lucide-react";
|
||||
import { MoonIcon, SunIcon } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { Skeleton } from "./ui/skeleton";
|
||||
|
||||
export function ColorModeToggle() {
|
||||
export function ToggleThemeButton() {
|
||||
const { setTheme, theme } = useTheme();
|
||||
|
||||
return (
|
||||
<ClientOnly ssr={<Skeleton className="size-10 border bg-accent" />}>
|
||||
<ClientOnly
|
||||
ssr={<Skeleton className="size-[34px] rounded-full border bg-accent" />}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setTheme(theme === "dark" ? "light" : "dark");
|
||||
}}
|
||||
aria-label="Toggle theme"
|
||||
className="h-auto w-auto rounded-full border border-border bg-background p-2"
|
||||
>
|
||||
{theme === "light" ? (
|
||||
<Sun strokeWidth={1} className="size-5" />
|
||||
<SunIcon className="size-4" />
|
||||
) : (
|
||||
<Moon strokeWidth={1} className="size-5" />
|
||||
<MoonIcon className="size-4" />
|
||||
)}
|
||||
</Button>
|
||||
</ClientOnly>
|
||||
|
||||
@@ -27,6 +27,7 @@ export function DatePickerWithRange(props: {
|
||||
header?: React.ReactNode;
|
||||
footer?: React.ReactNode;
|
||||
labelOverride?: string;
|
||||
popoverAlign?: "start" | "end" | "center";
|
||||
}) {
|
||||
const [screen, setScreen] = React.useState<"from" | "to">("from");
|
||||
const { from, to, setFrom, setTo } = props;
|
||||
@@ -65,7 +66,11 @@ export function DatePickerWithRange(props: {
|
||||
</PopoverTrigger>
|
||||
|
||||
{/* Popover */}
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<PopoverContent
|
||||
className="w-auto p-0"
|
||||
align={props.popoverAlign || "start"}
|
||||
sideOffset={10}
|
||||
>
|
||||
<DynamicHeight>
|
||||
<div>
|
||||
{!isValid && (
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import Link from "next/link";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
type LinkProps = React.ComponentProps<typeof Link>;
|
||||
|
||||
export function UnderlineLink(props: LinkProps) {
|
||||
return (
|
||||
<Link
|
||||
{...props}
|
||||
className={cn(
|
||||
"underline decoration-muted-foreground/50 decoration-dotted underline-offset-[5px] hover:text-foreground hover:decoration-foreground hover:decoration-solid",
|
||||
props.className,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef<
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 bg-black/80 data-[state=closed]:animate-out data-[state=open]:animate-in",
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80 data-[state=closed]:animate-out data-[state=open]:animate-in",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -43,7 +43,7 @@ const DialogContent = React.forwardRef<
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed grid w-full gap-4 rounded-t-xl border border-border bg-background p-6 shadow-lg duration-300 md:max-w-lg md:rounded-lg",
|
||||
"fixed z-50 grid w-full gap-4 rounded-t-xl border border-border bg-background p-6 shadow-lg duration-300 md:max-w-lg md:rounded-lg",
|
||||
// on mobile - put the dialog at the bottom of the screen, animate - slide up and fade in
|
||||
"right-0 bottom-0 left-0",
|
||||
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
||||
|
||||
@@ -4,6 +4,8 @@ export const DASHBOARD_THIRDWEB_CLIENT_ID =
|
||||
export const DASHBOARD_THIRDWEB_SECRET_KEY =
|
||||
process.env.DASHBOARD_SECRET_KEY || "";
|
||||
|
||||
export const THIRDWEB_API_SECRET = process.env.API_SERVER_SECRET || "";
|
||||
|
||||
export const IPFS_GATEWAY_URL =
|
||||
(process.env.NEXT_PUBLIC_IPFS_GATEWAY_URL as string) ||
|
||||
"https://{clientId}.ipfscdn.io/ipfs/{cid}/{path}";
|
||||
|
||||
@@ -117,7 +117,7 @@ function DashboardRouterTopProgressBarInner() {
|
||||
const width = isLoading ? progress : 100;
|
||||
return (
|
||||
<span
|
||||
className="fixed top-0 block h-[2px] bg-blue-500"
|
||||
className="fixed top-0 block h-[3px] bg-foreground"
|
||||
style={{
|
||||
opacity: isLoading ? "100" : "0",
|
||||
width: `${width}%`,
|
||||
|
||||
@@ -95,7 +95,8 @@ export const engineKeys = {
|
||||
backendWallet: (address: string, chainId: number) =>
|
||||
["backendWallet", address, chainId] as const,
|
||||
backendWalletBalance: (address: string, chainId: number) =>
|
||||
[...engineKeys.backendWallet(address, chainId), "balance"] as const,
|
||||
["backendWallet", "balance", address, chainId] as const,
|
||||
backendWalletBalanceAll: () => ["backendWallet", "balance"] as const,
|
||||
corsUrls: (instance: string) =>
|
||||
[...engineKeys.all, instance, "corsUrls"] as const,
|
||||
ipAllowlist: (instance: string) =>
|
||||
@@ -124,4 +125,6 @@ export const engineKeys = {
|
||||
[...engineKeys.all, engineId, "alerts"] as const,
|
||||
notificationChannels: (engineId: string) =>
|
||||
[...engineKeys.all, engineId, "notificationChannels"] as const,
|
||||
walletCredentials: (instance: string) =>
|
||||
[...engineKeys.all, instance, "walletCredentials"] as const,
|
||||
};
|
||||
|
||||
@@ -39,6 +39,7 @@ export type Account = {
|
||||
id: string;
|
||||
isStaff: boolean;
|
||||
creatorWalletAddress: string;
|
||||
image?: string | null;
|
||||
name?: string;
|
||||
email?: string;
|
||||
advancedEnabled: boolean;
|
||||
@@ -548,41 +549,6 @@ export function useResendEmailConfirmation() {
|
||||
});
|
||||
}
|
||||
|
||||
export function useApiKeys(props: {
|
||||
isLoggedIn: boolean;
|
||||
}) {
|
||||
const address = useActiveAccount()?.address;
|
||||
return useQuery({
|
||||
queryKey: apiKeys.keys(address || ""),
|
||||
queryFn: async () => {
|
||||
type Result = {
|
||||
data: ApiKey[];
|
||||
error?: { message: string };
|
||||
};
|
||||
|
||||
const res = await apiServerProxy<Result>({
|
||||
pathname: "/v1/keys",
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
const json = res.data;
|
||||
|
||||
if (json.error) {
|
||||
throw new Error(json.error.message);
|
||||
}
|
||||
return json.data;
|
||||
},
|
||||
enabled: !!address && props.isLoggedIn,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateApiKey() {
|
||||
const address = useActiveAccount()?.address;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -15,6 +15,7 @@ export function useDashboardContractMetadata(contract: ThirdwebContract) {
|
||||
return useQuery({
|
||||
queryKey: ["contract-metadata-header", contract.chain.id, contract.address],
|
||||
queryFn: () => fetchDashboardContractMetadata(contract),
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -93,7 +93,8 @@ type EngineFeature =
|
||||
| "CONTRACT_SUBSCRIPTIONS"
|
||||
| "IP_ALLOWLIST"
|
||||
| "HETEROGENEOUS_WALLET_TYPES"
|
||||
| "SMART_BACKEND_WALLETS";
|
||||
| "SMART_BACKEND_WALLETS"
|
||||
| "WALLET_CREDENTIALS";
|
||||
|
||||
interface EngineSystemHealth {
|
||||
status: string;
|
||||
@@ -860,6 +861,10 @@ export type SetWalletConfigInput =
|
||||
gcpKmsKeyRingId: string;
|
||||
gcpApplicationCredentialEmail: string;
|
||||
gcpApplicationCredentialPrivateKey: string;
|
||||
}
|
||||
| {
|
||||
type: "circle";
|
||||
circleApiKey: string;
|
||||
};
|
||||
|
||||
export function useEngineSetWalletConfig(params: {
|
||||
@@ -869,8 +874,8 @@ export function useEngineSetWalletConfig(params: {
|
||||
const { instanceUrl, authToken } = params;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<WalletConfigResponse, void, SetWalletConfigInput>({
|
||||
mutationFn: async (input) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: SetWalletConfigInput) => {
|
||||
invariant(instanceUrl, "instance is required");
|
||||
|
||||
const res = await fetch(`${instanceUrl}configuration/wallets`, {
|
||||
@@ -884,7 +889,7 @@ export function useEngineSetWalletConfig(params: {
|
||||
throw new Error(json.error.message);
|
||||
}
|
||||
|
||||
return json.result;
|
||||
return json.result as WalletConfigResponse;
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
@@ -894,10 +899,17 @@ export function useEngineSetWalletConfig(params: {
|
||||
});
|
||||
}
|
||||
|
||||
export type CreateBackendWalletInput = {
|
||||
type: EngineBackendWalletType;
|
||||
label?: string;
|
||||
};
|
||||
export type CreateBackendWalletInput =
|
||||
| {
|
||||
type: Exclude<EngineBackendWalletType, "circle" | "smart:circle">;
|
||||
label?: string;
|
||||
}
|
||||
| {
|
||||
type: "circle" | "smart:circle";
|
||||
label?: string;
|
||||
credentialId: string;
|
||||
isTestnet: boolean;
|
||||
};
|
||||
|
||||
export function useEngineCreateBackendWallet(params: {
|
||||
instanceUrl: string;
|
||||
@@ -1851,3 +1863,118 @@ export function useEngineDeleteNotificationChannel(engineId: string) {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export interface WalletCredential {
|
||||
id: string;
|
||||
type: string;
|
||||
label: string;
|
||||
isDefault: boolean | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface CreateWalletCredentialInput {
|
||||
type: "circle";
|
||||
label: string;
|
||||
entitySecret?: string;
|
||||
isDefault?: boolean;
|
||||
}
|
||||
|
||||
export function useEngineWalletCredentials(params: {
|
||||
instanceUrl: string;
|
||||
authToken: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}) {
|
||||
const { instanceUrl, authToken, page = 1, limit = 100 } = params;
|
||||
|
||||
return useQuery({
|
||||
queryKey: [...engineKeys.walletCredentials(instanceUrl), page, limit],
|
||||
queryFn: async () => {
|
||||
const res = await fetch(
|
||||
`${instanceUrl}wallet-credentials?page=${page}&limit=${limit}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: getEngineRequestHeaders(authToken),
|
||||
},
|
||||
);
|
||||
|
||||
const json = await res.json();
|
||||
return (json.result as WalletCredential[]) || [];
|
||||
},
|
||||
enabled: !!instanceUrl,
|
||||
});
|
||||
}
|
||||
|
||||
export function useEngineCreateWalletCredential(params: {
|
||||
instanceUrl: string;
|
||||
authToken: string;
|
||||
}) {
|
||||
const { instanceUrl, authToken } = params;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (input: CreateWalletCredentialInput) => {
|
||||
invariant(instanceUrl, "instance is required");
|
||||
|
||||
const res = await fetch(`${instanceUrl}wallet-credentials`, {
|
||||
method: "POST",
|
||||
headers: getEngineRequestHeaders(authToken),
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
if (json.error) {
|
||||
throw new Error(json.error.message);
|
||||
}
|
||||
|
||||
return json.result as WalletCredential;
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.walletCredentials(instanceUrl),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
interface UpdateWalletCredentialInput {
|
||||
label?: string;
|
||||
isDefault?: boolean;
|
||||
entitySecret?: string;
|
||||
}
|
||||
|
||||
export function useEngineUpdateWalletCredential(params: {
|
||||
instanceUrl: string;
|
||||
authToken: string;
|
||||
}) {
|
||||
const { instanceUrl, authToken } = params;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
id,
|
||||
...input
|
||||
}: UpdateWalletCredentialInput & { id: string }) => {
|
||||
invariant(instanceUrl, "instance is required");
|
||||
|
||||
const res = await fetch(`${instanceUrl}wallet-credentials/${id}`, {
|
||||
method: "PUT",
|
||||
headers: getEngineRequestHeaders(authToken),
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
if (json.error) {
|
||||
throw new Error(json.error.message);
|
||||
}
|
||||
|
||||
return json.result as WalletCredential;
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.walletCredentials(instanceUrl),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { addContractToMultiChainRegistry } from "components/contract-components/utils";
|
||||
import { MULTICHAIN_REGISTRY_CONTRACT } from "constants/contracts";
|
||||
import {
|
||||
DASHBOARD_ENGINE_RELAYER_URL,
|
||||
DASHBOARD_FORWARDER_ADDRESS,
|
||||
} from "constants/misc";
|
||||
import type { BasicContract } from "contract-ui/types/types";
|
||||
import { getAllMultichainRegistry } from "dashboard-extensions/common/read/getAllMultichainRegistry";
|
||||
import { useAllChainsData } from "hooks/chains/allChains";
|
||||
import { useMemo } from "react";
|
||||
import { sendAndConfirmTransaction } from "thirdweb";
|
||||
import { remove } from "thirdweb/extensions/thirdweb";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import invariant from "tiny-invariant";
|
||||
|
||||
export function useMultiChainRegContractList(walletAddress?: string) {
|
||||
return useQuery({
|
||||
queryKey: ["dashboard-registry", walletAddress, "multichain-contract-list"],
|
||||
queryFn: async () => {
|
||||
invariant(walletAddress, "walletAddress is required");
|
||||
const contracts = await getAllMultichainRegistry({
|
||||
contract: MULTICHAIN_REGISTRY_CONTRACT,
|
||||
address: walletAddress,
|
||||
});
|
||||
|
||||
return contracts;
|
||||
},
|
||||
|
||||
enabled: !!walletAddress,
|
||||
});
|
||||
}
|
||||
|
||||
interface Options {
|
||||
onlyMainnet?: boolean;
|
||||
}
|
||||
|
||||
export const useAllContractList = (
|
||||
walletAddress: string | undefined,
|
||||
{ onlyMainnet }: Options = { onlyMainnet: false },
|
||||
) => {
|
||||
const multiChainQuery = useMultiChainRegContractList(walletAddress);
|
||||
|
||||
// TODO - instead of using ALL chains, fetch only the ones used here
|
||||
const { idToChain } = useAllChainsData();
|
||||
const contractList = useMemo(() => {
|
||||
const data = multiChainQuery.data || [];
|
||||
|
||||
const mainnets: BasicContract[] = [];
|
||||
const testnets: BasicContract[] = [];
|
||||
|
||||
// biome-ignore lint/complexity/noForEach: FIXME
|
||||
data.forEach((net) => {
|
||||
const chn = idToChain.get(net.chainId);
|
||||
if (chn && chn.status !== "deprecated") {
|
||||
if (chn.testnet) {
|
||||
testnets.push(net);
|
||||
} else {
|
||||
mainnets.push(net);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
mainnets.sort((a, b) => a.chainId - b.chainId);
|
||||
|
||||
if (onlyMainnet) {
|
||||
return mainnets;
|
||||
}
|
||||
|
||||
testnets.sort((a, b) => a.chainId - b.chainId);
|
||||
return mainnets.concat(testnets);
|
||||
}, [multiChainQuery.data, onlyMainnet, idToChain]);
|
||||
|
||||
return {
|
||||
...multiChainQuery,
|
||||
data: contractList,
|
||||
};
|
||||
};
|
||||
|
||||
type RemoveContractParams = {
|
||||
contractAddress: string;
|
||||
chainId: number;
|
||||
};
|
||||
|
||||
export function useRemoveContractMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
const account = useActiveAccount();
|
||||
return useMutation({
|
||||
mutationFn: async (data: RemoveContractParams) => {
|
||||
invariant(data.chainId, "chainId not provided");
|
||||
invariant(account, "No wallet connected");
|
||||
const { contractAddress, chainId } = data;
|
||||
const transaction = remove({
|
||||
contract: MULTICHAIN_REGISTRY_CONTRACT,
|
||||
deployer: account.address,
|
||||
deployment: contractAddress,
|
||||
chainId: BigInt(chainId),
|
||||
});
|
||||
return await sendAndConfirmTransaction({
|
||||
transaction,
|
||||
account,
|
||||
gasless: {
|
||||
experimentalChainlessSupport: true,
|
||||
provider: "engine",
|
||||
relayerUrl: DASHBOARD_ENGINE_RELAYER_URL,
|
||||
relayerForwarderAddress: DASHBOARD_FORWARDER_ADDRESS,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
onSettled: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: ["dashboard-registry"],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
type AddContractParams = {
|
||||
contractAddress: string;
|
||||
chainId: number;
|
||||
};
|
||||
|
||||
export function useAddContractMutation() {
|
||||
const account = useActiveAccount();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (data: AddContractParams) => {
|
||||
invariant(account, "cannot add a contract without an address");
|
||||
|
||||
return await addContractToMultiChainRegistry(
|
||||
{
|
||||
address: data.contractAddress,
|
||||
chainId: data.chainId,
|
||||
},
|
||||
account,
|
||||
300000n,
|
||||
);
|
||||
},
|
||||
|
||||
onSettled: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: ["dashboard-registry"],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,13 +1,32 @@
|
||||
"use client";
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Form, FormControl, FormField, FormItem } from "@/components/ui/form";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
THIRDWEB_ENGINE_FAUCET_WALLET,
|
||||
TURNSTILE_SITE_KEY,
|
||||
} from "@/constants/env";
|
||||
import { useThirdwebClient } from "@/constants/thirdweb.client";
|
||||
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Turnstile } from "@marsidev/react-turnstile";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { CanClaimResponseType } from "app/api/testnet-faucet/can-claim/CanClaimResponseType";
|
||||
@@ -17,9 +36,21 @@ import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { toUnits } from "thirdweb";
|
||||
import {
|
||||
type ThirdwebClient,
|
||||
prepareTransaction,
|
||||
toUnits,
|
||||
toWei,
|
||||
} from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { useActiveAccount, useWalletBalance } from "thirdweb/react";
|
||||
import type { Chain } from "thirdweb/chains";
|
||||
import {
|
||||
useActiveAccount,
|
||||
useActiveWalletChain,
|
||||
useSendTransaction,
|
||||
useSwitchActiveWalletChain,
|
||||
useWalletBalance,
|
||||
} from "thirdweb/react";
|
||||
import { z } from "zod";
|
||||
import { isOnboardingComplete } from "../../../../../../login/onboarding/isOnboardingRequired";
|
||||
|
||||
@@ -137,6 +168,35 @@ export function FaucetButton({
|
||||
|
||||
const form = useForm<z.infer<typeof claimFaucetSchema>>();
|
||||
|
||||
// loading state
|
||||
if (faucetWalletBalanceQuery.isPending || canClaimFaucetQuery.isPending) {
|
||||
return (
|
||||
<Button variant="outline" className="w-full gap-2">
|
||||
Checking Faucet <Spinner className="size-3" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
// faucet is empty
|
||||
if (isFaucetEmpty) {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="mb-3 text-center text-muted-foreground text-sm">
|
||||
Faucet is empty right now
|
||||
</div>
|
||||
<SendFundsToFaucetModalButton
|
||||
chain={definedChain}
|
||||
isLoggedIn={!!twAccount}
|
||||
client={client}
|
||||
chainMeta={chain}
|
||||
onFaucetRefill={() => {
|
||||
faucetWalletBalanceQuery.refetch();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Force users to log in to claim the faucet
|
||||
if (!address || !twAccount) {
|
||||
return (
|
||||
@@ -164,24 +224,6 @@ export function FaucetButton({
|
||||
);
|
||||
}
|
||||
|
||||
// loading state
|
||||
if (faucetWalletBalanceQuery.isPending || canClaimFaucetQuery.isPending) {
|
||||
return (
|
||||
<Button variant="outline" className="w-full gap-2">
|
||||
Checking Faucet <Spinner className="size-3" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
// faucet is empty
|
||||
if (isFaucetEmpty) {
|
||||
return (
|
||||
<Button variant="outline" disabled className="!opacity-100 w-full">
|
||||
Faucet is empty right now
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
// Can not claim
|
||||
if (canClaimFaucetQuery.data && canClaimFaucetQuery.data.canClaim === false) {
|
||||
return (
|
||||
@@ -250,3 +292,165 @@ export function FaucetButton({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const faucetFormSchema = z.object({
|
||||
amount: z.coerce.number().refine((value) => value > 0, {
|
||||
message: "Amount must be greater than 0",
|
||||
}),
|
||||
});
|
||||
|
||||
function SendFundsToFaucetModalButton(props: {
|
||||
chain: Chain;
|
||||
isLoggedIn: boolean;
|
||||
client: ThirdwebClient;
|
||||
chainMeta: ChainMetadata;
|
||||
onFaucetRefill: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="default" className="w-full">
|
||||
Refill Faucet
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader className="mb-2">
|
||||
<DialogTitle>Refill Faucet</DialogTitle>
|
||||
<DialogDescription>Send funds to faucet wallet</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<SendFundsToFaucetModalContent {...props} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function SendFundsToFaucetModalContent(props: {
|
||||
chain: Chain;
|
||||
isLoggedIn: boolean;
|
||||
client: ThirdwebClient;
|
||||
chainMeta: ChainMetadata;
|
||||
onFaucetRefill: () => void;
|
||||
}) {
|
||||
const account = useActiveAccount();
|
||||
const activeChain = useActiveWalletChain();
|
||||
const switchActiveWalletChain = useSwitchActiveWalletChain();
|
||||
const sendTxMutation = useSendTransaction({
|
||||
payModal: false,
|
||||
});
|
||||
const switchChainMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
await switchActiveWalletChain(props.chain);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<z.infer<typeof faucetFormSchema>>({
|
||||
resolver: zodResolver(faucetFormSchema),
|
||||
defaultValues: {
|
||||
amount: 0.1,
|
||||
},
|
||||
});
|
||||
|
||||
function onSubmit(values: z.infer<typeof faucetFormSchema>) {
|
||||
const sendNativeTokenTx = prepareTransaction({
|
||||
chain: props.chain,
|
||||
client: props.client,
|
||||
to: THIRDWEB_ENGINE_FAUCET_WALLET,
|
||||
value: toWei(values.amount.toString()),
|
||||
});
|
||||
|
||||
const promise = sendTxMutation.mutateAsync(sendNativeTokenTx);
|
||||
|
||||
toast.promise(promise, {
|
||||
success: `Sent ${values.amount} ${props.chainMeta.nativeCurrency.symbol} to faucet`,
|
||||
error: `Failed to send ${values.amount} ${props.chainMeta.nativeCurrency.symbol} to faucet`,
|
||||
});
|
||||
|
||||
promise.then(() => {
|
||||
props.onFaucetRefill();
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="flex min-w-0 flex-col gap-5"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<p className="mb-2 text-foreground text-sm"> Faucet Wallet </p>
|
||||
<CopyTextButton
|
||||
copyIconPosition="right"
|
||||
variant="outline"
|
||||
className="w-full justify-between bg-card py-2 font-mono"
|
||||
textToCopy={THIRDWEB_ENGINE_FAUCET_WALLET}
|
||||
textToShow={THIRDWEB_ENGINE_FAUCET_WALLET}
|
||||
tooltip={undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="amount"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Amount</FormLabel>
|
||||
<FormControl>
|
||||
<div className="relative">
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
className="h-auto bg-card text-2xl md:text-2xl [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
|
||||
/>
|
||||
<div className="-translate-y-1/2 absolute top-1/2 right-4 text-muted-foreground text-sm">
|
||||
{props.chainMeta.nativeCurrency.symbol}
|
||||
</div>
|
||||
</div>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{!account && (
|
||||
<CustomConnectWallet
|
||||
chain={props.chain}
|
||||
loginRequired={false}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
connectButtonClassName="!w-full"
|
||||
detailsButtonClassName="!w-full"
|
||||
/>
|
||||
)}
|
||||
|
||||
{account && activeChain && (
|
||||
<div>
|
||||
{activeChain.id === props.chain.id ? (
|
||||
<Button
|
||||
key="submit"
|
||||
type="submit"
|
||||
className="mt-4 w-full gap-2"
|
||||
disabled={sendTxMutation.isPending}
|
||||
>
|
||||
{sendTxMutation.isPending && <Spinner className="size-4" />}
|
||||
Send funds to faucet
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
key="switch"
|
||||
className="mt-4 w-full gap-2"
|
||||
disabled={switchChainMutation.isPending}
|
||||
onClick={() => switchChainMutation.mutate()}
|
||||
>
|
||||
Switch to {props.chainMeta.name}{" "}
|
||||
{switchChainMutation.isPending && (
|
||||
<Spinner className="size-4" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ThirdwebClient, ThirdwebContract } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import type { MinimalTeamsAndProjects } from "../../../../../../components/contract-components/contract-deploy-form/add-to-project-card";
|
||||
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
|
||||
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
|
||||
import { getContractPageSidebarLinks } from "../_utils/getContractPageSidebarLinks";
|
||||
@@ -13,6 +14,8 @@ export function ContractPageLayoutClient(props: {
|
||||
chainMetadata: ChainMetadata;
|
||||
contract: ThirdwebContract;
|
||||
children: React.ReactNode;
|
||||
teamsAndProjects: MinimalTeamsAndProjects | undefined;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
const headerMetadataQuery = useQuery({
|
||||
|
||||
@@ -3,8 +3,9 @@ import type { SidebarLink } from "@/components/blocks/Sidebar";
|
||||
import { SidebarLayout } from "@/components/blocks/SidebarLayout";
|
||||
import type { DashboardContractMetadata } from "@3rdweb-sdk/react/hooks/useDashboardContractMetadata";
|
||||
import { DeprecatedAlert } from "components/shared/DeprecatedAlert";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ThirdwebClient, ThirdwebContract } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import type { MinimalTeamsAndProjects } from "../../../../../../components/contract-components/contract-deploy-form/add-to-project-card";
|
||||
import { ContractMetadata } from "./contract-metadata";
|
||||
import { PrimaryDashboardButton } from "./primary-dashboard-button";
|
||||
|
||||
@@ -14,6 +15,8 @@ export function ContractPageLayout(props: {
|
||||
children: React.ReactNode;
|
||||
sidebarLinks: SidebarLink[];
|
||||
dashboardContractMetadata: DashboardContractMetadata | undefined;
|
||||
client: ThirdwebClient;
|
||||
teamsAndProjects: MinimalTeamsAndProjects | undefined;
|
||||
externalLinks:
|
||||
| {
|
||||
name: string;
|
||||
@@ -27,6 +30,8 @@ export function ContractPageLayout(props: {
|
||||
sidebarLinks,
|
||||
dashboardContractMetadata,
|
||||
externalLinks,
|
||||
teamsAndProjects,
|
||||
client,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
@@ -49,6 +54,8 @@ export function ContractPageLayout(props: {
|
||||
chainSlug: chainMetadata.slug,
|
||||
contractAddress: contract.address,
|
||||
}}
|
||||
teamsAndProjects={teamsAndProjects}
|
||||
client={client}
|
||||
/>
|
||||
</div>
|
||||
<DeprecatedAlert chain={chainMetadata} />
|
||||
|
||||
@@ -1,19 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { UnderlineLink } from "@/components/ui/UnderlineLink";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { EVMContractInfo } from "@3rdweb-sdk/react";
|
||||
import {
|
||||
useAddContractMutation,
|
||||
useAllContractList,
|
||||
} from "@3rdweb-sdk/react/hooks/useRegistry";
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import type { EVMContractInfo } from "@3rdweb-sdk/react";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
import { CodeIcon, PlusIcon } from "lucide-react";
|
||||
import { CircleAlertIcon, ExternalLinkIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import type { Chain } from "thirdweb";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import type { Chain, ThirdwebClient } from "thirdweb";
|
||||
import {
|
||||
AddToProjectSelector,
|
||||
type MinimalTeamsAndProjects,
|
||||
} from "../../../../../../components/contract-components/contract-deploy-form/add-to-project-card";
|
||||
import { useAddContractToProject } from "../../../../../team/[team_slug]/[project_slug]/hooks/project-contracts";
|
||||
|
||||
const TRACKING_CATEGORY = "add_to_dashboard_upsell";
|
||||
|
||||
@@ -22,6 +34,8 @@ type AddToDashboardCardProps = {
|
||||
chain: Chain;
|
||||
contractInfo: EVMContractInfo;
|
||||
hideCodePageLink?: boolean;
|
||||
teamsAndProjects: MinimalTeamsAndProjects | undefined;
|
||||
client: ThirdwebClient;
|
||||
};
|
||||
|
||||
export const PrimaryDashboardButton: React.FC<AddToDashboardCardProps> = ({
|
||||
@@ -29,40 +43,13 @@ export const PrimaryDashboardButton: React.FC<AddToDashboardCardProps> = ({
|
||||
chain,
|
||||
contractInfo,
|
||||
hideCodePageLink,
|
||||
teamsAndProjects,
|
||||
client,
|
||||
}) => {
|
||||
const addContract = useAddContractMutation();
|
||||
const trackEvent = useTrack();
|
||||
const walletAddress = useActiveAccount()?.address;
|
||||
const pathname = usePathname();
|
||||
const registry = useAllContractList(walletAddress);
|
||||
|
||||
const { onSuccess: onAddSuccess, onError: onAddError } = useTxNotifications(
|
||||
"Successfully imported",
|
||||
"Failed to import",
|
||||
);
|
||||
|
||||
const isInRegistry =
|
||||
registry.isFetched &&
|
||||
registry.data?.find(
|
||||
(c) =>
|
||||
contractAddress &&
|
||||
// compare address...
|
||||
c.address.toLowerCase() === contractAddress.toLowerCase() &&
|
||||
// ... and chainId
|
||||
c.chainId === chain.id,
|
||||
) &&
|
||||
registry.isSuccess;
|
||||
|
||||
if (
|
||||
!walletAddress ||
|
||||
!contractAddress ||
|
||||
!chain ||
|
||||
pathname?.includes("payments")
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isInRegistry) {
|
||||
// if user is not logged in
|
||||
if (!teamsAndProjects) {
|
||||
if (hideCodePageLink) {
|
||||
return null;
|
||||
}
|
||||
@@ -84,54 +71,163 @@ export const PrimaryDashboardButton: React.FC<AddToDashboardCardProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
className="gap-2"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
if (!chain) {
|
||||
return;
|
||||
}
|
||||
trackEvent({
|
||||
category: TRACKING_CATEGORY,
|
||||
action: "add-to-dashboard",
|
||||
label: "attempt",
|
||||
contractAddress,
|
||||
});
|
||||
addContract.mutate(
|
||||
{
|
||||
contractAddress,
|
||||
chainId: chain.id,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
onAddSuccess();
|
||||
trackEvent({
|
||||
category: TRACKING_CATEGORY,
|
||||
action: "add-to-dashboard",
|
||||
label: "success",
|
||||
contractAddress,
|
||||
});
|
||||
},
|
||||
onError: (err) => {
|
||||
onAddError(err);
|
||||
trackEvent({
|
||||
category: TRACKING_CATEGORY,
|
||||
action: "add-to-dashboard",
|
||||
label: "error",
|
||||
contractAddress,
|
||||
error: err,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
}}
|
||||
>
|
||||
{addContract.isPending ? (
|
||||
<Spinner className="size-4" />
|
||||
) : (
|
||||
<PlusIcon className="size-4" />
|
||||
)}
|
||||
Import contract
|
||||
</Button>
|
||||
<AddToProjectButton
|
||||
contractAddress={contractAddress}
|
||||
teamsAndProjects={teamsAndProjects}
|
||||
chainId={chain.id.toString()}
|
||||
client={client}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
function AddToProjectButton(props: {
|
||||
contractAddress: string;
|
||||
teamsAndProjects: MinimalTeamsAndProjects;
|
||||
chainId: string;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="gap-2" variant="outline">
|
||||
<PlusIcon className="size-4" />
|
||||
Add To Project
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent className="gap-0 overflow-hidden p-0">
|
||||
<DialogHeader className="p-6">
|
||||
<DialogTitle>Add To Project</DialogTitle>
|
||||
<DialogDescription>
|
||||
Add the contract in a project's contract list on dashboard
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<AddToProjectModalContent
|
||||
teamsAndProjects={props.teamsAndProjects}
|
||||
chainId={props.chainId}
|
||||
contractAddress={props.contractAddress}
|
||||
client={props.client}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function AddToProjectModalContent(props: {
|
||||
teamsAndProjects: MinimalTeamsAndProjects;
|
||||
chainId: string;
|
||||
contractAddress: string;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
const trackEvent = useTrack();
|
||||
const addContractToProject = useAddContractToProject();
|
||||
|
||||
const [importSelection, setImportSelection] = useState({
|
||||
team: props.teamsAndProjects[0]?.team,
|
||||
project: props.teamsAndProjects[0]?.projects[0],
|
||||
});
|
||||
|
||||
const selectedTeam = props.teamsAndProjects.find(
|
||||
(t) => t.team.id === importSelection.team?.id,
|
||||
);
|
||||
|
||||
function handleImport(params: {
|
||||
teamId: string;
|
||||
projectId: string;
|
||||
}) {
|
||||
trackEvent({
|
||||
category: TRACKING_CATEGORY,
|
||||
action: "add-to-dashboard",
|
||||
label: "attempt",
|
||||
contractAddress: props.contractAddress,
|
||||
});
|
||||
addContractToProject.mutate(
|
||||
{
|
||||
contractAddress: props.contractAddress,
|
||||
teamId: params.teamId,
|
||||
projectId: params.projectId,
|
||||
chainId: props.chainId,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success("Contract added to the project successfully");
|
||||
trackEvent({
|
||||
category: TRACKING_CATEGORY,
|
||||
action: "add-to-dashboard",
|
||||
label: "success",
|
||||
contractAddress: props.contractAddress,
|
||||
});
|
||||
},
|
||||
onError: (err) => {
|
||||
if (err.message.includes("PROJECT_CONTRACT_ALREADY_EXISTS")) {
|
||||
toast.error("Contract is already added to the project");
|
||||
} else {
|
||||
toast.error("Failed to import contract");
|
||||
}
|
||||
trackEvent({
|
||||
category: TRACKING_CATEGORY,
|
||||
action: "add-to-dashboard",
|
||||
label: "error",
|
||||
contractAddress: props.contractAddress,
|
||||
error: err,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const isImportEnabled = !!importSelection.project && !!importSelection.team;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col gap-4 border-border border-t px-6 py-6">
|
||||
<AddToProjectSelector
|
||||
selection={importSelection}
|
||||
onSelectionChange={setImportSelection}
|
||||
teamsAndProjects={props.teamsAndProjects}
|
||||
client={props.client}
|
||||
/>
|
||||
|
||||
{/* No projects alert */}
|
||||
{selectedTeam?.projects.length === 0 && (
|
||||
<Alert variant="info">
|
||||
<CircleAlertIcon className="size-5" />
|
||||
<AlertTitle> Selected team has no projects </AlertTitle>
|
||||
<AlertDescription>
|
||||
<UnderlineLink
|
||||
href={`/team/${selectedTeam.team.slug}`}
|
||||
className="inline-flex items-center gap-2"
|
||||
target="_blank"
|
||||
>
|
||||
Create Project <ExternalLinkIcon className="size-3.5" />
|
||||
</UnderlineLink>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-3 flex justify-end border-border border-t bg-card p-6">
|
||||
<Button
|
||||
disabled={!isImportEnabled}
|
||||
className="gap-2"
|
||||
onClick={() => {
|
||||
if (!importSelection.team || !importSelection.project) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleImport({
|
||||
teamId: importSelection.team.id,
|
||||
projectId: importSelection.project.id,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{addContractToProject.isPending ? (
|
||||
<Spinner className="size-4" />
|
||||
) : (
|
||||
<PlusIcon className="size-4" />
|
||||
)}
|
||||
Add to Project
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,11 @@ export function getContractPageSidebarLinks(data: {
|
||||
hide: !data.metadata.isModularCore,
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
label: "Cross Chain (Beta)",
|
||||
href: `${layoutPrefix}/cross-chain`,
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
label: "Code Snippets",
|
||||
href: `${layoutPrefix}/code`,
|
||||
@@ -39,12 +44,6 @@ export function getContractPageSidebarLinks(data: {
|
||||
href: `${layoutPrefix}/events`,
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
label: "Embed",
|
||||
href: `${layoutPrefix}/embed`,
|
||||
hide: data.metadata.embedType === null,
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
label: "Analytics",
|
||||
href: `${layoutPrefix}/analytics`,
|
||||
|
||||
@@ -126,8 +126,11 @@ function UniqueWalletsChart(props: ChartProps) {
|
||||
|
||||
return (
|
||||
<ThirdwebBarChart
|
||||
title="Unique Wallets"
|
||||
description="The number of unique wallet addresses that have sent a transaction to this contract."
|
||||
header={{
|
||||
title: "Unique Wallets",
|
||||
description:
|
||||
"The number of unique wallet addresses that have sent a transaction to this contract.",
|
||||
}}
|
||||
data={analyticsQuery.data || []}
|
||||
isPending={analyticsQuery.isPending}
|
||||
config={{
|
||||
@@ -151,8 +154,11 @@ function TotalTransactionsChart(props: ChartProps) {
|
||||
|
||||
return (
|
||||
<ThirdwebBarChart
|
||||
title="Total Transactions"
|
||||
description="The number of transactions that have been sent to this contract."
|
||||
header={{
|
||||
title: "Total Transactions",
|
||||
description:
|
||||
"The number of transactions that have been sent to this contract.",
|
||||
}}
|
||||
data={analyticsQuery.data || []}
|
||||
isPending={analyticsQuery.isPending}
|
||||
config={{
|
||||
@@ -171,8 +177,11 @@ function TotalEventsChart(props: ChartProps) {
|
||||
|
||||
return (
|
||||
<ThirdwebBarChart
|
||||
title="Total Events"
|
||||
description="The number of on-chain events that have been emitted from this contract."
|
||||
header={{
|
||||
title: "Total Events",
|
||||
description:
|
||||
"The number of on-chain events that have been emitted from this contract.",
|
||||
}}
|
||||
data={analyticsQuery.data || []}
|
||||
isPending={analyticsQuery.isPending}
|
||||
config={{
|
||||
@@ -216,8 +225,11 @@ function FunctionBreakdownChart(
|
||||
|
||||
return (
|
||||
<ThirdwebBarChart
|
||||
title="Function Breakdown"
|
||||
description="The breakdown of calls to each write function from transactions."
|
||||
header={{
|
||||
title: "Function Breakdown",
|
||||
description:
|
||||
"The breakdown of calls to each write function from transactions.",
|
||||
}}
|
||||
data={mappedQueryData || []}
|
||||
isPending={analyticsQuery.isPending}
|
||||
config={Object.keys(mappedQueryData?.[0] || {}).reduce(
|
||||
@@ -269,8 +281,10 @@ function EventBreakdownChart(
|
||||
|
||||
return (
|
||||
<ThirdwebBarChart
|
||||
title="Event Breakdown"
|
||||
description="The breakdown of events emitted by this contract."
|
||||
header={{
|
||||
title: "Event Breakdown",
|
||||
description: "The breakdown of events emitted by this contract.",
|
||||
}}
|
||||
data={mappedQueryData || []}
|
||||
isPending={analyticsQuery.isPending}
|
||||
config={Object.keys(mappedQueryData?.[0] || {}).reduce(
|
||||
@@ -319,7 +333,7 @@ const AnalyticsStat: React.FC<AnalyticsStatProps> = ({
|
||||
|
||||
const AnalyticsSkeleton: React.FC<{ label: string }> = ({ label }) => {
|
||||
return (
|
||||
<Card as={Stat}>
|
||||
<Card as={Stat} className="bg-card">
|
||||
<StatLabel mb={{ base: 1, md: 0 }}>{label}</StatLabel>
|
||||
<Skeleton isLoaded={false}>
|
||||
<StatNumber>{0}</StatNumber>
|
||||
@@ -350,7 +364,7 @@ const AnalyticsData: React.FC<AnalyticsStatProps> = ({
|
||||
}, [totalQuery.data]);
|
||||
|
||||
return (
|
||||
<Card as={Stat}>
|
||||
<Card as={Stat} className="bg-card">
|
||||
<StatLabel mb={{ base: 1, md: 0 }}>{label}</StatLabel>
|
||||
<Skeleton isLoaded={totalQuery.isFetched}>
|
||||
<StatNumber>{data.toLocaleString()}</StatNumber>
|
||||
|
||||
@@ -0,0 +1,389 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import {
|
||||
type ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import { verifyContract } from "app/(dashboard)/(chain)/[chain_id]/[contractAddress]/sources/ContractSourcesPage";
|
||||
import {
|
||||
type DeployModalStep,
|
||||
DeployStatusModal,
|
||||
useDeployStatusModal,
|
||||
} from "components/contract-components/contract-deploy-form/deploy-context-modal";
|
||||
import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import {
|
||||
defineChain,
|
||||
eth_getCode,
|
||||
getRpcClient,
|
||||
prepareTransaction,
|
||||
sendAndConfirmTransaction,
|
||||
} from "thirdweb";
|
||||
import type {
|
||||
FetchDeployMetadataResult,
|
||||
ThirdwebContract,
|
||||
} from "thirdweb/contract";
|
||||
import {
|
||||
deployContractfromDeployMetadata,
|
||||
getOrDeployInfraForPublishedContract,
|
||||
} from "thirdweb/deploys";
|
||||
import { useActiveAccount, useSwitchActiveWalletChain } from "thirdweb/react";
|
||||
import { concatHex, padHex } from "thirdweb/utils";
|
||||
import { z } from "zod";
|
||||
import { SingleNetworkSelector } from "./single-network-selector";
|
||||
|
||||
type CrossChain = {
|
||||
id: number;
|
||||
network: string;
|
||||
chainId: number;
|
||||
status: "DEPLOYED" | "NOT_DEPLOYED";
|
||||
};
|
||||
|
||||
const formSchema = z.object({
|
||||
amounts: z.object({
|
||||
"84532": z.string(),
|
||||
"11155420": z.string(),
|
||||
"919": z.string(),
|
||||
"111557560": z.string(),
|
||||
"999999999": z.string(),
|
||||
"11155111": z.string(),
|
||||
"421614": z.string(),
|
||||
}),
|
||||
});
|
||||
type FormSchema = z.output<typeof formSchema>;
|
||||
|
||||
export function DataTable({
|
||||
data,
|
||||
coreMetadata,
|
||||
coreContract,
|
||||
modulesMetadata,
|
||||
initializeData,
|
||||
inputSalt,
|
||||
initCode,
|
||||
isDirectDeploy,
|
||||
}: {
|
||||
data: CrossChain[];
|
||||
coreMetadata: FetchDeployMetadataResult;
|
||||
coreContract: ThirdwebContract;
|
||||
modulesMetadata?: FetchDeployMetadataResult[];
|
||||
initializeData?: `0x${string}`;
|
||||
inputSalt?: `0x${string}`;
|
||||
initCode?: `0x${string}`;
|
||||
isDirectDeploy: boolean;
|
||||
}) {
|
||||
const activeAccount = useActiveAccount();
|
||||
const switchChain = useSwitchActiveWalletChain();
|
||||
const deployStatusModal = useDeployStatusModal();
|
||||
const { onError } = useTxNotifications(
|
||||
"Successfully deployed contract",
|
||||
"Failed to deploy contract",
|
||||
);
|
||||
|
||||
const addRowMutation = useMutation({
|
||||
mutationFn: async (chain: { chainId: number; name: string }) => {
|
||||
const code = await eth_getCode(
|
||||
getRpcClient({
|
||||
client: getThirdwebClient(),
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
chain: defineChain(chain.chainId),
|
||||
}),
|
||||
{ address: coreContract.address },
|
||||
);
|
||||
|
||||
const newRow: CrossChain = {
|
||||
id: chain.chainId,
|
||||
network: chain.name,
|
||||
chainId: chain.chainId,
|
||||
status: code?.length > 2 ? "DEPLOYED" : "NOT_DEPLOYED",
|
||||
};
|
||||
|
||||
if (!customChainData.some((row) => row.chainId === chain.chainId)) {
|
||||
setCustomChainData((prevData) => [...prevData, newRow]);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const [customChainData, setCustomChainData] = useState<CrossChain[]>(() => {
|
||||
try {
|
||||
const storedData = window.localStorage.getItem(
|
||||
`crosschain-${coreContract.address}`,
|
||||
);
|
||||
return storedData ? JSON.parse(storedData) : [];
|
||||
} catch (error) {
|
||||
console.error("Failed to read from localStorage", error);
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
try {
|
||||
window.localStorage.setItem(
|
||||
`crosschain-${coreContract.address}`,
|
||||
JSON.stringify(customChainData),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to write to localStorage", error);
|
||||
}
|
||||
}, [customChainData, coreContract.address]);
|
||||
|
||||
const mergedChainData = useMemo(() => {
|
||||
const chainMap = new Map<number, CrossChain>();
|
||||
for (const item of [...data, ...customChainData]) {
|
||||
chainMap.set(item.chainId, item); // Deduplicate by chainId
|
||||
}
|
||||
return Array.from(chainMap.values());
|
||||
}, [data, customChainData]);
|
||||
|
||||
const form = useForm<FormSchema>({
|
||||
resolver: zodResolver(formSchema),
|
||||
values: {
|
||||
amounts: {
|
||||
"84532": "", // Base
|
||||
"11155420": "", // OP testnet
|
||||
"919": "", // Mode Network
|
||||
"111557560": "", // Cyber
|
||||
"999999999": "", // Zora
|
||||
"11155111": "", // Sepolia
|
||||
"421614": "",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const columns: ColumnDef<CrossChain>[] = [
|
||||
{
|
||||
accessorKey: "network",
|
||||
header: "Network",
|
||||
cell: ({ row }) => {
|
||||
if (row.getValue("status") === "DEPLOYED") {
|
||||
return (
|
||||
<Link
|
||||
target="_blank"
|
||||
className="text-blue-500 underline"
|
||||
href={`/${row.getValue("chainId")}/${coreContract.address}`}
|
||||
>
|
||||
{row.getValue("network")}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
return row.getValue("network");
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "chainId",
|
||||
header: "Chain ID",
|
||||
},
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: "Status",
|
||||
cell: ({ row }) => {
|
||||
if (row.getValue("status") === "DEPLOYED") {
|
||||
return <p>Deployed</p>;
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => deployContract(row.getValue("chainId"))}
|
||||
>
|
||||
Deploy
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
data: mergedChainData,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
const deployContract = async (chainId: number) => {
|
||||
try {
|
||||
if (!activeAccount) {
|
||||
throw new Error("No active account");
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const chain = defineChain(chainId);
|
||||
const client = getThirdwebClient();
|
||||
const salt =
|
||||
inputSalt || concatHex(["0x03", padHex("0x", { size: 31 })]).toString();
|
||||
|
||||
await switchChain(chain);
|
||||
|
||||
const steps: DeployModalStep[] = [
|
||||
{
|
||||
type: "deploy",
|
||||
signatureCount: 1,
|
||||
},
|
||||
];
|
||||
|
||||
deployStatusModal.setViewContractLink("");
|
||||
deployStatusModal.open(steps);
|
||||
|
||||
let crosschainContractAddress: string | undefined;
|
||||
if (initCode && isDirectDeploy) {
|
||||
const tx = prepareTransaction({
|
||||
client,
|
||||
chain,
|
||||
to: "0x4e59b44847b379578588920cA78FbF26c0B4956C",
|
||||
data: initCode,
|
||||
});
|
||||
|
||||
await sendAndConfirmTransaction({
|
||||
transaction: tx,
|
||||
account: activeAccount,
|
||||
});
|
||||
|
||||
const code = await eth_getCode(
|
||||
getRpcClient({
|
||||
client,
|
||||
chain,
|
||||
}),
|
||||
{
|
||||
address: coreContract.address,
|
||||
},
|
||||
);
|
||||
|
||||
if (code && code.length > 2) {
|
||||
crosschainContractAddress = coreContract.address;
|
||||
}
|
||||
} else {
|
||||
crosschainContractAddress = await deployContractfromDeployMetadata({
|
||||
account: activeAccount,
|
||||
chain,
|
||||
client,
|
||||
deployMetadata: coreMetadata,
|
||||
isCrosschain: true,
|
||||
initializeData,
|
||||
salt,
|
||||
});
|
||||
|
||||
verifyContract({
|
||||
address: crosschainContractAddress as `0x${string}`,
|
||||
chain,
|
||||
client,
|
||||
});
|
||||
if (modulesMetadata) {
|
||||
for (const m of modulesMetadata) {
|
||||
await getOrDeployInfraForPublishedContract({
|
||||
chain,
|
||||
client,
|
||||
account: activeAccount,
|
||||
contractId: m.name,
|
||||
publisher: m.publisher,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
deployStatusModal.nextStep();
|
||||
deployStatusModal.setViewContractLink(
|
||||
`/${chain.id}/${crosschainContractAddress}`,
|
||||
);
|
||||
deployStatusModal.close();
|
||||
|
||||
setCustomChainData((prevData) =>
|
||||
prevData.map((row) =>
|
||||
row.chainId === chainId ? { ...row, status: "DEPLOYED" } : row,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
onError(e);
|
||||
console.error("failed to deploy contract", e);
|
||||
deployStatusModal.close();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form>
|
||||
<div
|
||||
style={{
|
||||
maxHeight: "500px",
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext(),
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<DeployStatusModal deployStatusModal={deployStatusModal} />
|
||||
</TableContainer>
|
||||
</div>
|
||||
|
||||
{addRowMutation.isPending && (
|
||||
<div className="my-4 animate-pulse text-center text-blue-500">
|
||||
Processing...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{addRowMutation.isError && (
|
||||
<div className="my-4 text-center text-red-500">
|
||||
Error while adding selected chain!
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4">
|
||||
<SingleNetworkSelector
|
||||
onAddRow={addRowMutation.mutate}
|
||||
isAddingRow={addRowMutation.isPending}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import Link from "next/link";
|
||||
|
||||
export function NoCrossChainPrompt() {
|
||||
return (
|
||||
<div>
|
||||
Multi-chain deployments are not available for this contract (or chain).
|
||||
<br />
|
||||
Please ensure the chain is supported or deploy a new contract to enable
|
||||
this functionality.
|
||||
<br />
|
||||
<br />
|
||||
<Link href="/explore" target="_blank" className="text-blue-500 underline">
|
||||
Explore contracts
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
import { fetchPublishedContractsFromDeploy } from "components/contract-components/fetchPublishedContractsFromDeploy";
|
||||
import { notFound } from "next/navigation";
|
||||
import {
|
||||
type ContractOptions,
|
||||
eth_getTransactionByHash,
|
||||
eth_getTransactionReceipt,
|
||||
getContractEvents,
|
||||
parseEventLogs,
|
||||
prepareEvent,
|
||||
} from "thirdweb";
|
||||
import { defineChain, getChainMetadata } from "thirdweb/chains";
|
||||
import {
|
||||
type FetchDeployMetadataResult,
|
||||
getContract,
|
||||
getDeployedCloneFactoryContract,
|
||||
resolveContractAbi,
|
||||
} from "thirdweb/contract";
|
||||
import { moduleInstalledEvent } from "thirdweb/modules";
|
||||
import { eth_getCode, getRpcClient } from "thirdweb/rpc";
|
||||
import type { TransactionReceipt } from "thirdweb/transaction";
|
||||
import { type AbiFunction, decodeFunctionData } from "thirdweb/utils";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { DataTable } from "./data-table";
|
||||
import { NoCrossChainPrompt } from "./no-crosschain-prompt";
|
||||
|
||||
export function getModuleInstallParams(mod: FetchDeployMetadataResult) {
|
||||
return (
|
||||
mod.abi
|
||||
.filter((a) => a.type === "function")
|
||||
.find((f) => f.name === "encodeBytesOnInstall")?.inputs || []
|
||||
);
|
||||
}
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
}>;
|
||||
}) {
|
||||
const ProxyDeployedEvent = prepareEvent({
|
||||
signature:
|
||||
"event ProxyDeployedV2(address indexed implementation, address indexed proxy, address indexed deployer, bytes32 inputSalt, bytes data, bytes extraData)",
|
||||
});
|
||||
|
||||
const params = await props.params;
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { contract } = info;
|
||||
|
||||
const isModularCore = (await getContractPageMetadata(contract)).isModularCore;
|
||||
|
||||
let twCloneFactoryContract: Readonly<
|
||||
ContractOptions<[], `0x${string}`>
|
||||
> | null = null;
|
||||
try {
|
||||
twCloneFactoryContract = await getDeployedCloneFactoryContract({
|
||||
chain: contract.chain,
|
||||
client: contract.client,
|
||||
});
|
||||
} catch {}
|
||||
|
||||
const originalCode = await eth_getCode(
|
||||
getRpcClient({
|
||||
client: contract.client,
|
||||
chain: contract.chain,
|
||||
}),
|
||||
{
|
||||
address: contract.address,
|
||||
},
|
||||
);
|
||||
|
||||
let initCode: `0x${string}` = "0x";
|
||||
let creationTxReceipt: TransactionReceipt | undefined;
|
||||
let isDirectDeploy = false;
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://contract.thirdweb-dev.com/creation/${contract.chain.id}/${contract.address}`,
|
||||
);
|
||||
const creationData = await res.json();
|
||||
|
||||
if (creationData.status === "1" && creationData.result[0]?.txHash) {
|
||||
const rpcClient = getRpcClient({
|
||||
client: contract.client,
|
||||
chain: contract.chain,
|
||||
});
|
||||
creationTxReceipt = await eth_getTransactionReceipt(rpcClient, {
|
||||
hash: creationData.result[0]?.txHash,
|
||||
});
|
||||
|
||||
const creationTx = await eth_getTransactionByHash(rpcClient, {
|
||||
hash: creationData.result[0]?.txHash,
|
||||
});
|
||||
|
||||
initCode = creationTx.input;
|
||||
isDirectDeploy =
|
||||
creationTx.to?.toLowerCase() ===
|
||||
"0x4e59b44847b379578588920cA78FbF26c0B4956C".toLowerCase();
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug(e);
|
||||
}
|
||||
|
||||
let initializeData: `0x${string}` | undefined;
|
||||
let inputSalt: `0x${string}` | undefined;
|
||||
let creationBlockNumber: bigint | undefined;
|
||||
|
||||
if (twCloneFactoryContract) {
|
||||
const events = await getContractEvents({
|
||||
contract: twCloneFactoryContract,
|
||||
events: [ProxyDeployedEvent],
|
||||
blockRange: 123456n,
|
||||
});
|
||||
const event = events.find(
|
||||
(e) =>
|
||||
e.args.proxy.toLowerCase() === params.contractAddress.toLowerCase(),
|
||||
);
|
||||
|
||||
initializeData = event?.args.data;
|
||||
inputSalt = event?.args.inputSalt;
|
||||
creationBlockNumber = event?.blockNumber;
|
||||
}
|
||||
|
||||
const defaultChains = [
|
||||
1, 137, 8453, 10, 42161, 11155111, 84532, 11155420, 421614,
|
||||
];
|
||||
|
||||
const chainsDeployedOn = (
|
||||
await Promise.all(
|
||||
defaultChains.map(async (c) => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const chain = defineChain(c);
|
||||
|
||||
try {
|
||||
const chainMetadata = await getChainMetadata(chain);
|
||||
|
||||
const rpcRequest = getRpcClient({
|
||||
client: contract.client,
|
||||
chain,
|
||||
});
|
||||
const code = await eth_getCode(rpcRequest, {
|
||||
address: params.contractAddress,
|
||||
});
|
||||
|
||||
return {
|
||||
id: chain.id,
|
||||
network: chainMetadata.name,
|
||||
chainId: chain.id,
|
||||
status:
|
||||
code === originalCode
|
||||
? ("DEPLOYED" as const)
|
||||
: ("NOT_DEPLOYED" as const),
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
id: chain.id,
|
||||
network: "",
|
||||
chainId: chain.id,
|
||||
status: "NOT_DEPLOYED" as const,
|
||||
};
|
||||
}
|
||||
}),
|
||||
)
|
||||
).filter((c) => c.status === "DEPLOYED");
|
||||
|
||||
let coreMetadata: FetchDeployMetadataResult | undefined;
|
||||
try {
|
||||
coreMetadata = (
|
||||
await fetchPublishedContractsFromDeploy({
|
||||
contract,
|
||||
client: contract.client,
|
||||
})
|
||||
).at(-1) as FetchDeployMetadataResult;
|
||||
} catch {}
|
||||
|
||||
if (!coreMetadata) {
|
||||
return NoCrossChainPrompt();
|
||||
}
|
||||
|
||||
let modulesMetadata: FetchDeployMetadataResult[] | undefined;
|
||||
|
||||
if (isModularCore) {
|
||||
let modules: string[] | undefined;
|
||||
const moduleEvent = moduleInstalledEvent();
|
||||
// extract module address in ModuleInstalled events from transaction receipt
|
||||
if (creationTxReceipt) {
|
||||
const decodedEvent = parseEventLogs({
|
||||
events: [moduleEvent],
|
||||
logs: creationTxReceipt.logs,
|
||||
});
|
||||
|
||||
modules = decodedEvent.map((e) => e.args.installedModule);
|
||||
}
|
||||
|
||||
// fetch events from contract
|
||||
if (!modules && creationBlockNumber) {
|
||||
const events = await getContractEvents({
|
||||
contract: contract,
|
||||
events: [moduleEvent],
|
||||
blockRange: 123456n,
|
||||
});
|
||||
|
||||
const filteredEvents = events.filter(
|
||||
(e) => e.blockNumber === creationBlockNumber,
|
||||
);
|
||||
|
||||
modules = filteredEvents.map((e) => e.args.installedModule);
|
||||
}
|
||||
|
||||
// if receipt not available, try extracting module address from initialize data
|
||||
if (!modules && initializeData) {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
const decodedData: any = await decodeFunctionData({
|
||||
contract,
|
||||
data: initializeData,
|
||||
});
|
||||
|
||||
const abi = await resolveContractAbi(contract).catch(() => undefined);
|
||||
|
||||
if (abi) {
|
||||
const initializeFunction = abi.find(
|
||||
(i: AbiFunction) => i.type === "function" && i.name === "initialize",
|
||||
) as unknown as AbiFunction;
|
||||
|
||||
const moduleIndex = initializeFunction.inputs.findIndex(
|
||||
(i) => i.name === "_modules" || i.name === "modules",
|
||||
);
|
||||
|
||||
modules = moduleIndex ? decodedData[moduleIndex] : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
modulesMetadata = modules
|
||||
? ((await Promise.all(
|
||||
modules.map(async (m) =>
|
||||
(
|
||||
await fetchPublishedContractsFromDeploy({
|
||||
contract: getContract({
|
||||
chain: contract.chain,
|
||||
client: contract.client,
|
||||
address: m,
|
||||
}),
|
||||
client: contract.client,
|
||||
})
|
||||
).at(-1),
|
||||
),
|
||||
)) as FetchDeployMetadataResult[])
|
||||
: undefined;
|
||||
}
|
||||
|
||||
if (!isDirectDeploy && !initializeData) {
|
||||
return NoCrossChainPrompt();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<h2 className="mb-1 font-bold text-2xl tracking-tight">
|
||||
Deploy Cross-chain
|
||||
</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Deterministically deploy your contracts on multiple networks.
|
||||
</p>
|
||||
</div>
|
||||
<div className="h-10" />
|
||||
<DataTable
|
||||
coreMetadata={coreMetadata}
|
||||
modulesMetadata={modulesMetadata}
|
||||
initializeData={initializeData}
|
||||
inputSalt={inputSalt}
|
||||
data={chainsDeployedOn}
|
||||
coreContract={contract}
|
||||
initCode={initCode}
|
||||
isDirectDeploy={isDirectDeploy}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { SelectWithSearch } from "@/components/blocks/select-with-search";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { ChainIcon } from "components/icons/ChainIcon";
|
||||
import { useAllChainsData } from "hooks/chains/allChains";
|
||||
import { useCallback, useMemo } from "react";
|
||||
|
||||
type Option = { label: string; value: string };
|
||||
|
||||
export function SingleNetworkSelector(props: {
|
||||
onAddRow: (chain: { chainId: number; name: string }) => void;
|
||||
isAddingRow: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
const { allChains, idToChain } = useAllChainsData();
|
||||
|
||||
const options = useMemo(() => {
|
||||
return allChains.map((chain) => ({
|
||||
label: chain.name,
|
||||
value: String(chain.chainId),
|
||||
}));
|
||||
}, [allChains]);
|
||||
|
||||
const searchFn = useCallback(
|
||||
(option: Option, searchValue: string) => {
|
||||
const chain = idToChain.get(Number(option.value));
|
||||
if (!chain) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Number.isInteger(Number.parseInt(searchValue))) {
|
||||
return String(chain.chainId).startsWith(searchValue);
|
||||
}
|
||||
return chain.name.toLowerCase().includes(searchValue.toLowerCase());
|
||||
},
|
||||
[idToChain],
|
||||
);
|
||||
|
||||
const renderOption = useCallback(
|
||||
(option: Option) => {
|
||||
const chain = idToChain.get(Number(option.value));
|
||||
if (!chain) {
|
||||
return option.label;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-between gap-4">
|
||||
<span className="flex grow gap-2 truncate text-left">
|
||||
<ChainIcon
|
||||
className="size-5"
|
||||
ipfsSrc={chain.icon?.url}
|
||||
loading="lazy"
|
||||
/>
|
||||
{chain.name}
|
||||
</span>
|
||||
<Badge variant="outline" className="gap-2 max-sm:hidden">
|
||||
<span className="text-muted-foreground">Chain ID</span>
|
||||
{chain.chainId}
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[idToChain],
|
||||
);
|
||||
|
||||
const handleChange = async (chainId: string) => {
|
||||
const chain = idToChain.get(Number(chainId));
|
||||
if (chain) {
|
||||
props.onAddRow({ chainId: chain.chainId, name: chain.name });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SelectWithSearch
|
||||
searchPlaceholder="Search by Name or Chain ID"
|
||||
value={undefined}
|
||||
options={options}
|
||||
onValueChange={handleChange}
|
||||
placeholder={
|
||||
allChains.length === 0
|
||||
? "Loading chains..."
|
||||
: "Select chain to deploy on"
|
||||
}
|
||||
overrideSearchFn={searchFn}
|
||||
renderOption={renderOption}
|
||||
className={props.className}
|
||||
disabled={props.isAddingRow || allChains.length === 0}
|
||||
closeOnSelect={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
|
||||
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
|
||||
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
|
||||
import { EmbedSetup } from "./embed-setup";
|
||||
|
||||
export function EmbedSetupClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
twAccount: Account | undefined;
|
||||
}) {
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
|
||||
if (metadataQuery.isPending) {
|
||||
return <LoadingPage />;
|
||||
}
|
||||
|
||||
if (metadataQuery.isError) {
|
||||
return <ErrorPage />;
|
||||
}
|
||||
|
||||
if (metadataQuery.data.embedType === null) {
|
||||
return <RedirectToContractOverview contract={props.contract} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<EmbedSetup
|
||||
contract={props.contract}
|
||||
ercOrMarketplace={metadataQuery.data.embedType}
|
||||
twAccount={props.twAccount}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,613 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CodeClient } from "@/components/ui/code/code.client";
|
||||
import {
|
||||
type Account,
|
||||
useApiKeys,
|
||||
useCreateApiKey,
|
||||
} from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { Flex, FormControl, Input, Select } from "@chakra-ui/react";
|
||||
import { LazyCreateAPIKeyDialog } from "components/settings/ApiKeys/Create/LazyCreateAPIKeyDialog";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useAllChainsData } from "hooks/chains/allChains";
|
||||
import { AlertCircleIcon } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import type { StoredChain } from "stores/chainStores";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import { Card, FormHelperText, FormLabel, Heading, Text } from "tw-components";
|
||||
|
||||
interface EmbedSetupProps {
|
||||
contract: ThirdwebContract;
|
||||
ercOrMarketplace: string;
|
||||
twAccount: Account | undefined;
|
||||
}
|
||||
|
||||
// MAKE SURE THIS IS v1 embed hashes!!
|
||||
const IPFS_URI = "bafybeigdie2yyiazou7grjowoevmuip6akk33nqb55vrpezqdwfssrxyfy";
|
||||
const ERC721_IPFS_URI =
|
||||
"bafybeicd3qfzelz4su7ng6n523virdsgobrc5pcbarhwqv3dj3drh645pi";
|
||||
|
||||
interface IframeSrcOptions {
|
||||
clientId: string;
|
||||
chain: string;
|
||||
tokenId?: string;
|
||||
listingId?: string;
|
||||
listingType?: string;
|
||||
directListingId?: string;
|
||||
englishAuctionId?: string;
|
||||
relayUrl?: string;
|
||||
theme?: string;
|
||||
primaryColor?: string;
|
||||
secondaryColor?: string;
|
||||
biconomyApiKey?: string;
|
||||
biconomyApiId?: string;
|
||||
}
|
||||
|
||||
const colorOptions = [
|
||||
"purple",
|
||||
"blue",
|
||||
"orange",
|
||||
"pink",
|
||||
"green",
|
||||
"red",
|
||||
"teal",
|
||||
"cyan",
|
||||
"yellow",
|
||||
];
|
||||
|
||||
const isValidUrl = (url: string | undefined) => {
|
||||
if (!url) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const buildIframeSrc = (
|
||||
contract: ThirdwebContract,
|
||||
ercOrMarketplace?: string,
|
||||
options?: IframeSrcOptions,
|
||||
): string => {
|
||||
const contractPath =
|
||||
ercOrMarketplace === "erc721" ? "" : `${ercOrMarketplace}.html`;
|
||||
const contractEmbedHash =
|
||||
ercOrMarketplace === "erc721" ? ERC721_IPFS_URI : IPFS_URI;
|
||||
|
||||
if (!contract || !options || !contractEmbedHash) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const {
|
||||
clientId,
|
||||
chain,
|
||||
tokenId,
|
||||
listingId,
|
||||
listingType,
|
||||
directListingId,
|
||||
englishAuctionId,
|
||||
relayUrl,
|
||||
theme,
|
||||
primaryColor,
|
||||
secondaryColor,
|
||||
biconomyApiKey,
|
||||
biconomyApiId,
|
||||
} = options;
|
||||
|
||||
const url = new URL(
|
||||
`https://embed.ipfscdn.io/ipfs/${contractEmbedHash}/${contractPath}`,
|
||||
);
|
||||
|
||||
url.searchParams.append("contract", contract.address);
|
||||
url.searchParams.append("chain", chain);
|
||||
url.searchParams.append("clientId", clientId);
|
||||
|
||||
if (tokenId !== undefined && ercOrMarketplace === "erc1155") {
|
||||
url.searchParams.append("tokenId", tokenId.toString());
|
||||
}
|
||||
if (listingId !== undefined && ercOrMarketplace === "marketplace") {
|
||||
url.searchParams.append("listingId", listingId.toString());
|
||||
}
|
||||
if (
|
||||
directListingId !== undefined &&
|
||||
ercOrMarketplace === "marketplace-v3" &&
|
||||
listingType === "direct-listing"
|
||||
) {
|
||||
url.searchParams.append("directListingId", directListingId.toString());
|
||||
}
|
||||
if (
|
||||
englishAuctionId !== undefined &&
|
||||
ercOrMarketplace === "marketplace-v3" &&
|
||||
listingType === "english-auction"
|
||||
) {
|
||||
url.searchParams.append("englishAuctionId", englishAuctionId.toString());
|
||||
}
|
||||
if (isValidUrl(relayUrl)) {
|
||||
url.searchParams.append("relayUrl", relayUrl || "");
|
||||
}
|
||||
if (biconomyApiKey) {
|
||||
url.searchParams.append("biconomyApiKey", biconomyApiKey);
|
||||
}
|
||||
if (biconomyApiId) {
|
||||
url.searchParams.append("biconomyApiId", biconomyApiId);
|
||||
}
|
||||
if (theme) {
|
||||
url.searchParams.append("theme", theme);
|
||||
}
|
||||
if (primaryColor && primaryColor !== "default") {
|
||||
url.searchParams.append("primaryColor", primaryColor);
|
||||
}
|
||||
if (secondaryColor && secondaryColor !== "orange") {
|
||||
url.searchParams.append("secondaryColor", secondaryColor);
|
||||
}
|
||||
return url.toString();
|
||||
};
|
||||
|
||||
// taken from @thirdweb-dev /chains
|
||||
// @internal
|
||||
function minimizeChain(
|
||||
chain: ChainMetadata,
|
||||
): Pick<
|
||||
ChainMetadata,
|
||||
| "name"
|
||||
| "chain"
|
||||
| "rpc"
|
||||
| "nativeCurrency"
|
||||
| "shortName"
|
||||
| "chainId"
|
||||
| "testnet"
|
||||
| "slug"
|
||||
| "icon"
|
||||
> {
|
||||
const [firstRpc] = chain.rpc;
|
||||
return {
|
||||
name: chain.name,
|
||||
chain: chain.chain,
|
||||
rpc: [firstRpc || ""],
|
||||
nativeCurrency: chain.nativeCurrency,
|
||||
shortName: chain.shortName,
|
||||
chainId: chain.chainId,
|
||||
testnet: chain.testnet,
|
||||
slug: chain.slug,
|
||||
icon: chain.icon,
|
||||
};
|
||||
}
|
||||
|
||||
// taken from @thirdweb-dev /chains
|
||||
// @internal
|
||||
type ChainConfiguration = {
|
||||
rpc?: string | string[];
|
||||
};
|
||||
|
||||
function configureChain(
|
||||
chain: ChainMetadata,
|
||||
chainConfig: ChainConfiguration,
|
||||
): ChainMetadata {
|
||||
let additionalRPCs: string[] = [];
|
||||
if (chainConfig?.rpc) {
|
||||
if (typeof chainConfig.rpc === "string") {
|
||||
additionalRPCs = [chainConfig.rpc];
|
||||
} else {
|
||||
additionalRPCs = chainConfig.rpc;
|
||||
}
|
||||
}
|
||||
// prepend additional RPCs to the chain's RPCs
|
||||
return { ...chain, rpc: [...additionalRPCs, ...chain.rpc] };
|
||||
}
|
||||
|
||||
export const EmbedSetup: React.FC<EmbedSetupProps> = ({
|
||||
contract,
|
||||
ercOrMarketplace,
|
||||
twAccount,
|
||||
}) => {
|
||||
const trackEvent = useTrack();
|
||||
const { theme } = useTheme();
|
||||
|
||||
const apiKeys = useApiKeys({
|
||||
isLoggedIn: !!twAccount,
|
||||
});
|
||||
const createKeyMutation = useCreateApiKey();
|
||||
|
||||
const validApiKey = (apiKeys.data || []).find(
|
||||
(apiKey) =>
|
||||
(apiKey.domains.includes("*") ||
|
||||
apiKey.domains.includes("embed.ipfscdn.io") ||
|
||||
apiKey.domains.includes("*.ipfscdn.io")) &&
|
||||
(apiKey.services || [])
|
||||
.find((service) => service.name === "storage")
|
||||
?.actions.includes("read") &&
|
||||
!!(apiKey.services || []).find((service) => service.name === "rpc"),
|
||||
);
|
||||
|
||||
const chainId = contract.chain.id;
|
||||
const { idToChain } = useAllChainsData();
|
||||
|
||||
const chain: StoredChain = (chainId ? idToChain.get(chainId) : undefined) || {
|
||||
name: "Unknown Chain",
|
||||
chainId: chainId || -1,
|
||||
rpc: [],
|
||||
chain: "unknown",
|
||||
nativeCurrency: {
|
||||
name: "Ethereum",
|
||||
symbol: "ETH",
|
||||
decimals: 18,
|
||||
},
|
||||
shortName: "unknown",
|
||||
slug: "unknown",
|
||||
testnet: false,
|
||||
stackType: "",
|
||||
};
|
||||
|
||||
const { register, watch } = useForm<{
|
||||
rpcUrl: string;
|
||||
relayUrl: string;
|
||||
tokenId: string;
|
||||
listingId: string;
|
||||
listingType: string;
|
||||
directListingId: string;
|
||||
englishAuctionId: string;
|
||||
theme: string;
|
||||
primaryColor: string;
|
||||
secondaryColor: string;
|
||||
biconomyApiKey: string;
|
||||
biconomyApiId: string;
|
||||
gasless: string;
|
||||
}>({
|
||||
defaultValues: {
|
||||
rpcUrl: chain?.rpc[0],
|
||||
tokenId: "0",
|
||||
listingId: "0",
|
||||
directListingId: "0",
|
||||
englishAuctionId: "0",
|
||||
theme: theme === "light" ? "light" : "dark",
|
||||
primaryColor: "purple",
|
||||
secondaryColor: "orange",
|
||||
},
|
||||
reValidateMode: "onChange",
|
||||
});
|
||||
|
||||
const configuredChainWithNewRpc = configureChain(chain, {
|
||||
rpc: watch("rpcUrl"),
|
||||
});
|
||||
const minimizedChain = minimizeChain(configuredChainWithNewRpc);
|
||||
|
||||
const iframeSrc = buildIframeSrc(contract, ercOrMarketplace, {
|
||||
chain: JSON.stringify(minimizedChain),
|
||||
clientId: validApiKey?.key || "",
|
||||
...watch(),
|
||||
});
|
||||
|
||||
const embedCode = useMemo(
|
||||
() =>
|
||||
`<iframe
|
||||
src="${iframeSrc}"
|
||||
width="100%"
|
||||
height="750px"
|
||||
style="max-width:100%;"
|
||||
frameborder="0"
|
||||
></iframe>`,
|
||||
[iframeSrc],
|
||||
);
|
||||
|
||||
const [showCreateAPIKeyModal, setShowCreateAPIKeyModal] = useState(false);
|
||||
const activeAccount = useActiveAccount();
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<LazyCreateAPIKeyDialog
|
||||
prefill={{
|
||||
name: "Embed API Key",
|
||||
domains: "embed.ipfscdn.io",
|
||||
}}
|
||||
open={showCreateAPIKeyModal}
|
||||
onOpenChange={setShowCreateAPIKeyModal}
|
||||
onCreateAndComplete={() => {
|
||||
trackEvent({
|
||||
category: "api-keys",
|
||||
action: "create",
|
||||
label: "success",
|
||||
fromEmbed: true,
|
||||
});
|
||||
apiKeys.refetch();
|
||||
}}
|
||||
enableNebulaServiceByDefault={false}
|
||||
teamSlug={undefined}
|
||||
/>
|
||||
|
||||
<Alert variant="warning">
|
||||
<AlertCircleIcon className="size-5" />
|
||||
<AlertTitle>Deprecated</AlertTitle>
|
||||
<AlertDescription className="leading-relaxed">
|
||||
<span>
|
||||
thirdweb NFT Embeds are deprecated and not actively maintained
|
||||
</span>{" "}
|
||||
<br />
|
||||
<span>
|
||||
Use the{" "}
|
||||
<Link
|
||||
href="https://github.com/thirdweb-example/nft-minting-template"
|
||||
target="_blank"
|
||||
className="underline underline-offset-2 hover:text-foreground"
|
||||
>
|
||||
{" "}
|
||||
NFT Minting Template
|
||||
</Link>{" "}
|
||||
or build a custom NFT minting app using{" "}
|
||||
<Link
|
||||
href="https://portal.thirdweb.com/react/v5"
|
||||
className="underline underline-offset-2 hover:text-foreground"
|
||||
>
|
||||
thirdweb React SDK components
|
||||
</Link>{" "}
|
||||
instead
|
||||
</span>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="h-8" />
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
<div>
|
||||
<h2 className="mb-3 font-semibold text-xl tracking-tight">
|
||||
Configuration
|
||||
</h2>
|
||||
|
||||
<Card className="flex w-full flex-col gap-5 bg-card">
|
||||
{ercOrMarketplace === "marketplace" ? (
|
||||
<FormControl>
|
||||
<FormLabel>Listing ID</FormLabel>
|
||||
<Input type="number" {...register("listingId")} />
|
||||
<FormHelperText>
|
||||
The listing ID the embed should display
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
) : null}
|
||||
|
||||
{ercOrMarketplace === "marketplace-v3" ? (
|
||||
<FormControl>
|
||||
<FormLabel>Listing type</FormLabel>
|
||||
<Select {...register("listingType")}>
|
||||
<option value="direct-listing">Direct Listing</option>
|
||||
<option value="english-auction">English Auction</option>
|
||||
</Select>
|
||||
<FormHelperText>
|
||||
The type of listing the embed should display
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
) : null}
|
||||
|
||||
{ercOrMarketplace === "marketplace-v3" &&
|
||||
watch("listingType") === "direct-listing" ? (
|
||||
<FormControl>
|
||||
<FormLabel>Direct Listing ID</FormLabel>
|
||||
<Input type="number" {...register("directListingId")} />
|
||||
<FormHelperText>
|
||||
The direct listing ID the embed should display
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
) : null}
|
||||
|
||||
{ercOrMarketplace === "marketplace-v3" &&
|
||||
watch("listingType") === "english-auction" ? (
|
||||
<FormControl>
|
||||
<FormLabel>English Auction ID</FormLabel>
|
||||
<Input type="number" {...register("englishAuctionId")} />
|
||||
<FormHelperText>
|
||||
The english auction ID the embed should display
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
) : null}
|
||||
|
||||
{ercOrMarketplace === "erc1155" ? (
|
||||
<FormControl>
|
||||
<FormLabel>Token ID</FormLabel>
|
||||
<Input type="number" {...register("tokenId")} />
|
||||
<FormHelperText>
|
||||
The token ID the embed should display
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
) : null}
|
||||
|
||||
<FormControl>
|
||||
<FormLabel>Client ID</FormLabel>
|
||||
{!activeAccount ? (
|
||||
<Button asChild className="w-full">
|
||||
<Link
|
||||
href={`/login${
|
||||
pathname ? `?next=${encodeURIComponent(pathname)}` : ""
|
||||
}`}
|
||||
>
|
||||
Sign in to create a client ID
|
||||
</Link>
|
||||
</Button>
|
||||
) : validApiKey ? (
|
||||
<Input
|
||||
readOnly
|
||||
disabled
|
||||
value={`${validApiKey?.name} - ${validApiKey?.key}`}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
trackEvent({
|
||||
category: "api-keys",
|
||||
action: "create",
|
||||
label: "attempt",
|
||||
fromEmbed: true,
|
||||
});
|
||||
|
||||
setShowCreateAPIKeyModal(true);
|
||||
}}
|
||||
disabled={createKeyMutation.isPending}
|
||||
>
|
||||
Create Client ID
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<FormHelperText>
|
||||
You need a client ID to use embeds.{" "}
|
||||
<Link
|
||||
href="https://portal.thirdweb.com/account/api-keys"
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
</Link>
|
||||
.
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<FormLabel>RPC Url</FormLabel>
|
||||
<Input type="url" {...register("rpcUrl")} />
|
||||
<FormHelperText>
|
||||
RPC the embed should use to connect to the blockchain.
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
{ercOrMarketplace === "marketplace" ||
|
||||
ercOrMarketplace === "marketplace-v3" ? null : (
|
||||
<FormControl gap={4}>
|
||||
<Heading size="title.sm" mb={2}>
|
||||
Gasless
|
||||
</Heading>
|
||||
<Select {...register("gasless")} mb={4}>
|
||||
<option value="false">Disabled</option>
|
||||
<option value="openZeppelin">OpenZeppelin Relayer</option>
|
||||
<option value="biconomy">Biconomy Relayer</option>
|
||||
</Select>
|
||||
{watch("gasless") === "openZeppelin" && (
|
||||
<FormControl>
|
||||
<FormLabel>OpenZeppelin Relayer URL</FormLabel>
|
||||
<Input type="url" {...register("relayUrl")} />
|
||||
</FormControl>
|
||||
)}
|
||||
{watch("gasless") === "biconomy" && (
|
||||
<Flex gap={4} flexDir="column">
|
||||
<FormControl>
|
||||
<FormLabel>Biconomy API key</FormLabel>
|
||||
<Input type="url" {...register("biconomyApiKey")} />
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel>Biconomy API ID</FormLabel>
|
||||
<Input type="url" {...register("biconomyApiId")} />
|
||||
</FormControl>
|
||||
</Flex>
|
||||
)}
|
||||
<FormHelperText>
|
||||
A relayer can be used to make the transaction gasless for the
|
||||
end user.{" "}
|
||||
<Link
|
||||
target="_blank"
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
href="https://blog.thirdweb.com/guides/setup-gasless-transactions"
|
||||
>
|
||||
Learn more
|
||||
</Link>
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||
<FormControl>
|
||||
<FormLabel>Theme</FormLabel>
|
||||
<Select {...register("theme")}>
|
||||
<option value="light">Light</option>
|
||||
<option value="dark">Dark</option>
|
||||
<option value="system">User system</option>
|
||||
</Select>
|
||||
<FormHelperText>
|
||||
Selecting system will make it so the embed would change
|
||||
depending on the user system's preferences.
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<FormLabel>Primary Color</FormLabel>
|
||||
<Select {...register("primaryColor")}>
|
||||
{ercOrMarketplace === "erc721" && (
|
||||
<option value="default">Default</option>
|
||||
)}
|
||||
{colorOptions.map((color) => (
|
||||
<option key={color} value={color}>
|
||||
{color[0]?.toUpperCase() + color.substring(1)}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<FormHelperText>
|
||||
Used for the main actions button backgrounds.
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
{ercOrMarketplace === "marketplace" ||
|
||||
ercOrMarketplace === "marketplace-v3" ? (
|
||||
<FormControl>
|
||||
<FormLabel>Secondary Color</FormLabel>
|
||||
<Select {...register("secondaryColor")}>
|
||||
{colorOptions.map((color) => (
|
||||
<option key={color} value={color}>
|
||||
{color[0]?.toUpperCase() + color.substring(1)}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<FormHelperText>
|
||||
Use for secondary actions (like when the user is connected to
|
||||
the wrong network)
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
) : null}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="mb-3 font-semibold text-xl tracking-tight">
|
||||
Embed Code
|
||||
</h2>
|
||||
<CodeClient
|
||||
code={embedCode}
|
||||
lang="html"
|
||||
onCopy={() => {
|
||||
trackEvent({
|
||||
category: "embed",
|
||||
action: "click",
|
||||
label: "copy-code",
|
||||
address: contract.address,
|
||||
chainId,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-8" />
|
||||
|
||||
<h2 className="mb-3 font-semibold text-xl tracking-tight">Preview</h2>
|
||||
|
||||
<div className="overflow-hidden rounded-lg border border-border">
|
||||
{!validApiKey ? (
|
||||
<Text>You need to create a client ID to use embeds</Text>
|
||||
) : iframeSrc ? (
|
||||
<iframe
|
||||
title="thirdweb embed"
|
||||
src={iframeSrc}
|
||||
width="100%"
|
||||
height="750px"
|
||||
frameBorder="0"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,42 +0,0 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { localhost } from "thirdweb/chains";
|
||||
import { getRawAccount } from "../../../../../account/settings/getAccount";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { EmbedSetupClient } from "./EmbedSetup.client";
|
||||
import { EmbedSetup } from "./embed-setup";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
}>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const account = await getRawAccount();
|
||||
|
||||
const { contract } = info;
|
||||
if (contract.chain.id === localhost.id) {
|
||||
return <EmbedSetupClient contract={contract} twAccount={account} />;
|
||||
}
|
||||
|
||||
const { embedType } = await getContractPageMetadata(contract);
|
||||
|
||||
if (embedType === null) {
|
||||
redirect(`/${params.chain_id}/${params.contractAddress}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<EmbedSetup
|
||||
contract={contract}
|
||||
ercOrMarketplace={embedType}
|
||||
twAccount={account}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
import { getProjects } from "@/api/projects";
|
||||
import { getTeams } from "@/api/team";
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { localhost } from "thirdweb/chains";
|
||||
import { getContractMetadata } from "thirdweb/extensions/common";
|
||||
import { isAddress, isContractDeployed } from "thirdweb/utils";
|
||||
import type { MinimalTeamsAndProjects } from "../../../../../components/contract-components/contract-deploy-form/add-to-project-card";
|
||||
import { resolveFunctionSelectors } from "../../../../../lib/selectors";
|
||||
import { shortenIfAddress } from "../../../../../utils/usedapp-external";
|
||||
import { ConfigureCustomChain } from "./_layout/ConfigureCustomChain";
|
||||
@@ -38,11 +42,16 @@ export default async function Layout(props: {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const client = getThirdwebClient();
|
||||
const teamsAndProjects = await getTeamsAndProjectsIfLoggedIn();
|
||||
|
||||
if (contract.chain.id === localhost.id) {
|
||||
return (
|
||||
<ContractPageLayoutClient
|
||||
chainMetadata={chainMetadata}
|
||||
contract={contract}
|
||||
teamsAndProjects={teamsAndProjects}
|
||||
client={client}
|
||||
>
|
||||
{props.children}
|
||||
</ContractPageLayoutClient>
|
||||
@@ -73,12 +82,43 @@ export default async function Layout(props: {
|
||||
sidebarLinks={sidebarLinks}
|
||||
dashboardContractMetadata={contractMetadata}
|
||||
externalLinks={externalLinks}
|
||||
teamsAndProjects={teamsAndProjects}
|
||||
client={client}
|
||||
>
|
||||
{props.children}
|
||||
</ContractPageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
async function getTeamsAndProjectsIfLoggedIn() {
|
||||
try {
|
||||
const teams = await getTeams();
|
||||
|
||||
if (!teams) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const teamsAndProjects: MinimalTeamsAndProjects = await Promise.all(
|
||||
teams.map(async (team) => ({
|
||||
team: {
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
slug: team.slug,
|
||||
image: team.image,
|
||||
},
|
||||
projects: (await getProjects(team.slug)).map((x) => ({
|
||||
id: x.id,
|
||||
name: x.name,
|
||||
})),
|
||||
})),
|
||||
);
|
||||
|
||||
return teamsAndProjects;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateMetadata(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
|
||||
@@ -26,6 +26,7 @@ import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { TransactionButton } from "components/buttons/TransactionButton";
|
||||
import { addDays, fromUnixTime } from "date-fns";
|
||||
import { useAllChainsData } from "hooks/chains/allChains";
|
||||
@@ -119,22 +120,39 @@ function ClaimableModule(props: ModuleInstanceProps) {
|
||||
claimConditionQuery.data?.startTimestamp === 0 &&
|
||||
claimConditionQuery.data?.endTimestamp === 0;
|
||||
|
||||
const currencyContract = getContract({
|
||||
address: claimConditionQuery.data?.currency || "",
|
||||
chain: props.contract.chain,
|
||||
client: props.contract.client,
|
||||
});
|
||||
const claimConditionCurrency = claimConditionQuery.data?.currency;
|
||||
|
||||
const shouldFetchTokenDecimals =
|
||||
claimConditionQuery.data &&
|
||||
claimConditionQuery.data?.currency !== ZERO_ADDRESS;
|
||||
const shouldFetchCurrencyDecimals =
|
||||
!!claimConditionCurrency && claimConditionCurrency !== ZERO_ADDRESS;
|
||||
|
||||
const currencyDecimalsQuery = useReadContract(decimals, {
|
||||
contract: currencyContract,
|
||||
queryOptions: {
|
||||
enabled: shouldFetchTokenDecimals,
|
||||
const currencyDecimalsQuery = useQuery({
|
||||
queryKey: [
|
||||
"currency-decimals",
|
||||
{
|
||||
contractAddress: props.contract.address,
|
||||
chainId: props.contract.chain.id,
|
||||
claimConditionCurrency,
|
||||
},
|
||||
],
|
||||
queryFn: async () => {
|
||||
if (!claimConditionCurrency) {
|
||||
throw new Error();
|
||||
}
|
||||
const currencyContract = getContract({
|
||||
address: claimConditionCurrency,
|
||||
chain: props.contract.chain,
|
||||
client: props.contract.client,
|
||||
});
|
||||
|
||||
const decimalsVal = await decimals({
|
||||
contract: currencyContract,
|
||||
});
|
||||
|
||||
return decimalsVal;
|
||||
},
|
||||
enabled: shouldFetchCurrencyDecimals,
|
||||
});
|
||||
|
||||
const tokenDecimalsQuery = useReadContract(decimals, {
|
||||
contract: contract,
|
||||
queryOptions: {
|
||||
@@ -265,7 +283,7 @@ function ClaimableModule(props: ModuleInstanceProps) {
|
||||
// claim conditions data is present
|
||||
claimConditionQuery.data &&
|
||||
// token decimals is fetched if it should be fetched
|
||||
(shouldFetchTokenDecimals ? currencyDecimalsQuery.isFetched : true)
|
||||
(shouldFetchCurrencyDecimals ? currencyDecimalsQuery.isFetched : true)
|
||||
? {
|
||||
claimCondition: claimConditionQuery.data,
|
||||
currencyDecimals: currencyDecimalsQuery.data,
|
||||
@@ -276,7 +294,7 @@ function ClaimableModule(props: ModuleInstanceProps) {
|
||||
tokenId,
|
||||
isLoading:
|
||||
claimConditionQuery.isLoading ||
|
||||
(!!shouldFetchTokenDecimals && currencyDecimalsQuery.isLoading),
|
||||
(!!shouldFetchCurrencyDecimals && currencyDecimalsQuery.isLoading),
|
||||
}}
|
||||
isOwnerAccount={!!ownerAccount}
|
||||
name={props.contractInfo.name}
|
||||
@@ -565,6 +583,7 @@ function ClaimConditionSection(props: {
|
||||
onClick={() => setAddClaimConditionButtonClicked(true)}
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
disabled={!props.isOwnerAccount}
|
||||
>
|
||||
Add Claim Condition
|
||||
</Button>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import { fetchPublishedContractVersions } from "components/contract-components/fetch-contracts-with-versions";
|
||||
import {
|
||||
fetchLatestPublishedContractVersion,
|
||||
fetchPublishedContractVersions,
|
||||
} from "components/contract-components/fetch-contracts-with-versions";
|
||||
import { isAddress } from "thirdweb";
|
||||
import type { FetchDeployMetadataResult } from "thirdweb/contract";
|
||||
import { resolveAddress } from "thirdweb/extensions/ens";
|
||||
import invariant from "tiny-invariant";
|
||||
import type { ModuleMeta } from "./install-module-params";
|
||||
@@ -14,16 +18,24 @@ export async function getModuleInstalledParams(ext: ModuleMeta) {
|
||||
client: getThirdwebClient(),
|
||||
name: ext.publisherAddress,
|
||||
});
|
||||
const allPublishedModules = await fetchPublishedContractVersions(
|
||||
publisherAddress,
|
||||
ext.moduleName,
|
||||
);
|
||||
|
||||
// find the version we want
|
||||
const publishedModule =
|
||||
ext.moduleVersion === "latest"
|
||||
? allPublishedModules[0]
|
||||
: allPublishedModules.find((v) => v.version === ext.moduleVersion);
|
||||
let publishedModule: FetchDeployMetadataResult | undefined = undefined;
|
||||
|
||||
if (ext.moduleVersion === "latest") {
|
||||
publishedModule = await fetchLatestPublishedContractVersion(
|
||||
publisherAddress,
|
||||
ext.moduleName,
|
||||
);
|
||||
} else {
|
||||
const allPublishedModules = await fetchPublishedContractVersions(
|
||||
publisherAddress,
|
||||
ext.moduleName,
|
||||
);
|
||||
|
||||
publishedModule = allPublishedModules.find(
|
||||
(v) => v.version === ext.moduleVersion,
|
||||
);
|
||||
}
|
||||
|
||||
invariant(
|
||||
publishedModule,
|
||||
|
||||
@@ -135,7 +135,7 @@ function OverviewAnalytics(props: ChartProps) {
|
||||
data={mergedData || []}
|
||||
isPending={isPending}
|
||||
showLegend
|
||||
chartClassName="aspect-[1.5] lg:aspect-[4.5]"
|
||||
chartClassName="aspect-[1.5] lg:aspect-[3.5]"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ export function ContractSettingsPageClient(props: {
|
||||
contract={props.contract}
|
||||
functionSelectors={metadataQuery.data.functionSelectors}
|
||||
twAccount={props.twAccount}
|
||||
hasDefaultFeeConfig={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -79,8 +79,9 @@ export function ContractSettingsPage(props: {
|
||||
contract: ThirdwebContract;
|
||||
functionSelectors: string[];
|
||||
twAccount: Account | undefined;
|
||||
hasDefaultFeeConfig: boolean;
|
||||
}) {
|
||||
const { functionSelectors, contract, twAccount } = props;
|
||||
const { functionSelectors, contract, twAccount, hasDefaultFeeConfig } = props;
|
||||
return (
|
||||
<ContractSettingsPageInner
|
||||
contract={contract}
|
||||
@@ -96,10 +97,13 @@ export function ContractSettingsPage(props: {
|
||||
CommonExt.isGetDefaultRoyaltyInfoSupported(functionSelectors),
|
||||
CommonExt.isSetDefaultRoyaltyInfoSupported(functionSelectors),
|
||||
].every(Boolean)}
|
||||
isPlatformFeesSupported={[
|
||||
CommonExt.isGetPlatformFeeInfoSupported(functionSelectors),
|
||||
CommonExt.isSetPlatformFeeInfoSupported(functionSelectors),
|
||||
].every(Boolean)}
|
||||
isPlatformFeesSupported={
|
||||
!hasDefaultFeeConfig &&
|
||||
[
|
||||
CommonExt.isGetPlatformFeeInfoSupported(functionSelectors),
|
||||
CommonExt.isSetPlatformFeeInfoSupported(functionSelectors),
|
||||
].every(Boolean)
|
||||
}
|
||||
twAccount={twAccount}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -132,9 +132,8 @@ export const SettingsPlatformFees = ({
|
||||
<SolidityInput
|
||||
solidityType="address"
|
||||
formContext={form}
|
||||
variant="filled"
|
||||
{...form.register("platform_fee_recipient")}
|
||||
isDisabled={!address || sendAndConfirmTx.isPending}
|
||||
disabled={!address || sendAndConfirmTx.isPending}
|
||||
/>
|
||||
<FormErrorMessage>
|
||||
{
|
||||
@@ -155,7 +154,6 @@ export const SettingsPlatformFees = ({
|
||||
>
|
||||
<FormLabel>Percentage</FormLabel>
|
||||
<BasisPointsInput
|
||||
variant="filled"
|
||||
value={form.watch("platform_fee_basis_points")}
|
||||
onChange={(value) =>
|
||||
form.setValue("platform_fee_basis_points", value, {
|
||||
@@ -163,7 +161,7 @@ export const SettingsPlatformFees = ({
|
||||
shouldTouch: true,
|
||||
})
|
||||
}
|
||||
isDisabled={sendAndConfirmTx.isPending}
|
||||
disabled={sendAndConfirmTx.isPending}
|
||||
/>
|
||||
<FormErrorMessage>
|
||||
{
|
||||
|
||||
@@ -128,10 +128,9 @@ export const SettingsPrimarySale = ({
|
||||
>
|
||||
<FormLabel>Recipient Address</FormLabel>
|
||||
<SolidityInput
|
||||
isDisabled={mutation.isPending || !address}
|
||||
disabled={mutation.isPending || !address}
|
||||
solidityType="address"
|
||||
formContext={form}
|
||||
variant="filled"
|
||||
{...form.register("primary_sale_recipient")}
|
||||
/>
|
||||
<FormErrorMessage>
|
||||
|
||||
@@ -144,9 +144,8 @@ export const SettingsRoyalties = ({
|
||||
<SolidityInput
|
||||
solidityType="address"
|
||||
formContext={form}
|
||||
variant="filled"
|
||||
{...form.register("fee_recipient")}
|
||||
isDisabled={!address}
|
||||
disabled={!address}
|
||||
/>
|
||||
<FormErrorMessage>
|
||||
{
|
||||
@@ -165,7 +164,6 @@ export const SettingsRoyalties = ({
|
||||
>
|
||||
<FormLabel>Percentage</FormLabel>
|
||||
<BasisPointsInput
|
||||
variant="filled"
|
||||
value={form.watch("seller_fee_basis_points")}
|
||||
onChange={(value) =>
|
||||
form.setValue("seller_fee_basis_points", value, {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { DEFAULT_FEE_RECIPIENT } from "constants/addresses";
|
||||
import { notFound } from "next/navigation";
|
||||
import { localhost } from "thirdweb/chains";
|
||||
import { getPlatformFeeInfo } from "thirdweb/extensions/common";
|
||||
import { getRawAccount } from "../../../../../account/settings/getAccount";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
@@ -33,11 +35,19 @@ export default async function Page(props: {
|
||||
getContractPageMetadata(info.contract),
|
||||
]);
|
||||
|
||||
let hasDefaultFeeConfig = true;
|
||||
try {
|
||||
const feeInfo = await getPlatformFeeInfo({ contract });
|
||||
hasDefaultFeeConfig =
|
||||
feeInfo[0].toLowerCase() === DEFAULT_FEE_RECIPIENT.toLowerCase();
|
||||
} catch {}
|
||||
|
||||
return (
|
||||
<ContractSettingsPage
|
||||
contract={info.contract}
|
||||
functionSelectors={metadata.functionSelectors}
|
||||
twAccount={account}
|
||||
hasDefaultFeeConfig={hasDefaultFeeConfig}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@ export async function ChainsData(props: {
|
||||
) : props.activeView === "table" ? (
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableHeader className="z-0">
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Chain ID</TableHead>
|
||||
|
||||
@@ -83,14 +83,14 @@ export async function ChainListRow({
|
||||
|
||||
<TableCell>
|
||||
<div className="flex w-[520px] flex-row items-center gap-14 ">
|
||||
<div className="z-10 flex items-center gap-7">
|
||||
<div className="z-10 flex items-center gap-4">
|
||||
{products.map((p) => {
|
||||
return (
|
||||
<ProductIcon
|
||||
key={p.name}
|
||||
icon={p.icon}
|
||||
label={p.name}
|
||||
href={pidToHref(p.id)}
|
||||
href={p.link}
|
||||
isEnabled={enabledServices.includes(p.id)}
|
||||
/>
|
||||
);
|
||||
@@ -102,32 +102,6 @@ export async function ChainListRow({
|
||||
);
|
||||
}
|
||||
|
||||
function pidToHref(pid: (typeof products)[number]["id"]) {
|
||||
switch (pid) {
|
||||
case "account-abstraction": {
|
||||
return "https://portal.thirdweb.com/connect/account-abstraction/overview";
|
||||
}
|
||||
case "contracts": {
|
||||
return "https://portal.thirdweb.com/contracts";
|
||||
}
|
||||
case "connect-sdk": {
|
||||
return "https://portal.thirdweb.com/connect";
|
||||
}
|
||||
case "engine": {
|
||||
return "https://portal.thirdweb.com/engine";
|
||||
}
|
||||
case "pay": {
|
||||
return "https://portal.thirdweb.com/connect/pay/overview";
|
||||
}
|
||||
case "rpc-edge": {
|
||||
return "https://portal.thirdweb.com/infrastructure/rpc-edge/overview";
|
||||
}
|
||||
case "insight": {
|
||||
return "https://portal.thirdweb.com/insight";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function ProductIcon(props: {
|
||||
icon: React.FC<{ className?: string }>;
|
||||
label: string;
|
||||
@@ -148,8 +122,15 @@ function ProductIcon(props: {
|
||||
<Link
|
||||
href={props.href}
|
||||
target={props.href.startsWith("http") ? "_blank" : undefined}
|
||||
className="group rounded-lg p-2 hover:bg-accent"
|
||||
>
|
||||
<props.icon className={cn("size-8", !props.isEnabled && "grayscale")} />
|
||||
<props.icon
|
||||
className={cn(
|
||||
"size-6 text-foreground group-hover:text-link-foreground",
|
||||
!props.isEnabled &&
|
||||
"text-muted-foreground opacity-50 group-hover:opacity-100",
|
||||
)}
|
||||
/>
|
||||
</Link>
|
||||
</ToolTipLabel>
|
||||
);
|
||||
|
||||
@@ -106,9 +106,8 @@ export function StarButton(props: {
|
||||
) : (
|
||||
<Star
|
||||
className={cn(
|
||||
"size-6 transition-all",
|
||||
"size-6 text-foreground transition-all",
|
||||
props.iconClassName,
|
||||
isPreferred ? "text-yellow-400" : "text-foreground",
|
||||
)}
|
||||
fill={isPreferred ? "currentColor" : "none"}
|
||||
strokeWidth="1px"
|
||||
|
||||
@@ -7,85 +7,15 @@ export function ConnectSDKIcon(props: { className?: string }) {
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={props.className}
|
||||
role="presentation"
|
||||
>
|
||||
<title>Connect SDK</title>
|
||||
<rect
|
||||
x="3"
|
||||
y="17.9988"
|
||||
width="14.9988"
|
||||
height="10.6499"
|
||||
rx="2"
|
||||
transform="rotate(-90 3 17.9988)"
|
||||
fill="url(#paint0_linear_195_4047)"
|
||||
/>
|
||||
<g filter="url(#filter0_bi_195_4047)">
|
||||
<path
|
||||
d="M3 9C3 7.89543 3.89543 7 5 7H19C20.1046 7 21 7.89543 21 9V19C21 20.1046 20.1046 21 19 21H5C3.89543 21 3 20.1046 3 19V9Z"
|
||||
fill="#B9DDFF"
|
||||
fillOpacity="0.8"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M18.3004 12.6995V12.6995C17.5822 13.4177 17.5822 14.5822 18.3004 15.3004V15.3004"
|
||||
stroke="white"
|
||||
strokeWidth="1.5"
|
||||
d="M16 8.00007V4.50073C16 3.66899 16 3.25312 15.8248 2.99755C15.6717 2.77425 15.4346 2.62257 15.1678 2.57715C14.8623 2.52517 14.4847 2.69944 13.7295 3.04799L4.85901 7.14206C4.18551 7.45291 3.84875 7.60834 3.60211 7.84939C3.38406 8.06249 3.21762 8.32262 3.1155 8.6099C3 8.93486 3 9.30575 3 10.0475V15.0001M16.5 14.5001H16.51M3 11.2001L3 17.8001C3 18.9202 3 19.4802 3.21799 19.9081C3.40973 20.2844 3.71569 20.5903 4.09202 20.7821C4.51984 21.0001 5.07989 21.0001 6.2 21.0001H17.8C18.9201 21.0001 19.4802 21.0001 19.908 20.7821C20.2843 20.5903 20.5903 20.2844 20.782 19.9081C21 19.4802 21 18.9202 21 17.8001V11.2001C21 10.08 21 9.51992 20.782 9.09209C20.5903 8.71577 20.2843 8.40981 19.908 8.21806C19.4802 8.00007 18.9201 8.00007 17.8 8.00007L6.2 8.00007C5.0799 8.00007 4.51984 8.00007 4.09202 8.21806C3.7157 8.40981 3.40973 8.71577 3.21799 9.09209C3 9.51991 3 10.08 3 11.2001ZM17 14.5001C17 14.7762 16.7761 15.0001 16.5 15.0001C16.2239 15.0001 16 14.7762 16 14.5001C16 14.2239 16.2239 14.0001 16.5 14.0001C16.7761 14.0001 17 14.2239 17 14.5001Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<defs>
|
||||
<filter
|
||||
id="filter0_bi_195_4047"
|
||||
x="1"
|
||||
y="5"
|
||||
width="22"
|
||||
height="18"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feGaussianBlur in="BackgroundImageFix" stdDeviation="1" />
|
||||
<feComposite
|
||||
in2="SourceAlpha"
|
||||
operator="in"
|
||||
result="effect1_backgroundBlur_195_4047"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_backgroundBlur_195_4047"
|
||||
result="shape"
|
||||
/>
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="0.2" />
|
||||
<feGaussianBlur stdDeviation="0.05" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="shape"
|
||||
result="effect2_innerShadow_195_4047"
|
||||
/>
|
||||
</filter>
|
||||
<linearGradient
|
||||
id="paint0_linear_195_4047"
|
||||
x1="15.026"
|
||||
y1="22.4602"
|
||||
x2="3.73188"
|
||||
y2="27.2831"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#2567FF" />
|
||||
<stop offset="1" stopColor="#22A7FF" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,98 +1,21 @@
|
||||
export function ContractIcon(props: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={props.className}
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={props.className}
|
||||
role="presentation"
|
||||
>
|
||||
<title>Contract</title>
|
||||
<g>
|
||||
<rect
|
||||
x="3"
|
||||
y="21"
|
||||
width="14.9988"
|
||||
height="10.6499"
|
||||
rx="1.5"
|
||||
transform="rotate(-90 3 21)"
|
||||
fill="url(#paint0_linear_281_45147)"
|
||||
/>
|
||||
<g filter="url(#filter0_bi_281_45147)">
|
||||
<path
|
||||
d="M7.39429 4.5C7.39429 3.67157 8.06586 3 8.89429 3H19.5C20.3285 3 21 3.67157 21 4.5V19.5C21 20.3284 20.3285 21 19.5 21H8.89429C8.06586 21 7.39429 20.3284 7.39429 19.5V4.5Z"
|
||||
fill="#EBA4D2"
|
||||
fillOpacity="0.8"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M11.1971 7L17.1971 7.0001"
|
||||
stroke="white"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M11.1971 10L14.1971 10.0001"
|
||||
stroke="white"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter
|
||||
id="filter0_bi_281_45147"
|
||||
x="5.39429"
|
||||
y="1"
|
||||
width="17.6057"
|
||||
height="22"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feGaussianBlur in="BackgroundImageFix" stdDeviation="1" />
|
||||
<feComposite
|
||||
in2="SourceAlpha"
|
||||
operator="in"
|
||||
result="effect1_backgroundBlur_281_45147"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_backgroundBlur_281_45147"
|
||||
result="shape"
|
||||
/>
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="0.2" />
|
||||
<feGaussianBlur stdDeviation="0.05" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="shape"
|
||||
result="effect2_innerShadow_281_45147"
|
||||
/>
|
||||
</filter>
|
||||
<linearGradient
|
||||
id="paint0_linear_281_45147"
|
||||
x1="-2.77663"
|
||||
y1="30.016"
|
||||
x2="10.9305"
|
||||
y2="13.6496"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#F4009F" />
|
||||
<stop offset="1" stopColor="#F856C8" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path
|
||||
d="M14 11H8M10 15H8M16 7H8M20 6.8V17.2C20 18.8802 20 19.7202 19.673 20.362C19.3854 20.9265 18.9265 21.3854 18.362 21.673C17.7202 22 16.8802 22 15.2 22H8.8C7.11984 22 6.27976 22 5.63803 21.673C5.07354 21.3854 4.6146 20.9265 4.32698 20.362C4 19.7202 4 18.8802 4 17.2V6.8C4 5.11984 4 4.27976 4.32698 3.63803C4.6146 3.07354 5.07354 2.6146 5.63803 2.32698C6.27976 2 7.11984 2 8.8 2H15.2C16.8802 2 17.7202 2 18.362 2.32698C18.9265 2.6146 19.3854 3.07354 19.673 3.63803C20 4.27976 20 5.11984 20 6.8Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,77 +7,15 @@ export function EngineIcon(props: { className?: string }) {
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={props.className}
|
||||
role="presentation"
|
||||
>
|
||||
<title>Engine</title>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M14.9523 11.9988C14.9523 11.9981 14.9518 11.9975 14.951 11.9975C14.9089 11.9992 14.8665 12 14.8239 12C13.0588 12 11.6278 10.5691 11.6278 8.8039C11.6278 7.03873 13.0588 5.60779 14.8239 5.60779C15.9762 5.60779 16.986 6.21756 17.5484 7.13202C17.695 7.37046 17.9908 7.48805 18.2658 7.43583C18.4057 7.40926 18.5501 7.39536 18.6978 7.39536C19.9694 7.39536 21.0001 8.42614 21.0001 9.69767C21.0001 10.9692 19.9694 12 18.6979 12C18.6979 12 18.6979 12 18.6979 12C18.6979 12 18.6979 12 18.6978 12H14.9536C14.9529 12 14.9523 11.9994 14.9523 11.9988Z"
|
||||
fill="url(#paint0_linear_195_3943)"
|
||||
d="M6.5 19C4.01472 19 2 16.9853 2 14.5C2 12.1564 3.79151 10.2313 6.07974 10.0194C6.54781 7.17213 9.02024 5 12 5C14.9798 5 17.4522 7.17213 17.9203 10.0194C20.2085 10.2313 22 12.1564 22 14.5C22 16.9853 19.9853 19 17.5 19C13.1102 19 10.3433 19 6.5 19Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<g filter="url(#filter0_bi_195_3943)">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M6.18764 18.9251C6.17601 18.9253 6.16437 18.9253 6.15271 18.9253C4.41138 18.9253 2.99976 17.5137 2.99976 15.7724C2.99976 14.031 4.41138 12.6194 6.15271 12.6194C6.18301 12.6194 6.20685 12.5932 6.20379 12.563C6.18785 12.4057 6.1797 12.2463 6.1797 12.085C6.1797 9.37046 8.48997 7.16992 11.3398 7.16992C13.8545 7.16992 15.949 8.88324 16.407 11.1512C16.4682 11.4541 16.7615 11.6671 17.0697 11.6434C17.1634 11.6361 17.2581 11.6325 17.3536 11.6325C19.3675 11.6325 21 13.265 21 15.2789C21 17.2928 19.3675 18.9253 17.3536 18.9253C17.285 18.9253 17.2169 18.9234 17.1493 18.9197C17.1063 18.9234 17.0628 18.9253 17.0188 18.9253H6.20985C6.20243 18.9253 6.19503 18.9252 6.18764 18.9251Z"
|
||||
fill="#BFB5FF"
|
||||
fillOpacity="0.8"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter
|
||||
id="filter0_bi_195_3943"
|
||||
x="0.999756"
|
||||
y="5.16992"
|
||||
width="22.0003"
|
||||
height="15.7554"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feGaussianBlur in="BackgroundImageFix" stdDeviation="1" />
|
||||
<feComposite
|
||||
in2="SourceAlpha"
|
||||
operator="in"
|
||||
result="effect1_backgroundBlur_195_3943"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_backgroundBlur_195_3943"
|
||||
result="shape"
|
||||
/>
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="0.2" />
|
||||
<feGaussianBlur stdDeviation="0.05" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="shape"
|
||||
result="effect2_innerShadow_195_3943"
|
||||
/>
|
||||
</filter>
|
||||
<linearGradient
|
||||
id="paint0_linear_195_3943"
|
||||
x1="13.1976"
|
||||
y1="10.8463"
|
||||
x2="28.7967"
|
||||
y2="2.94276"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#3F2DAF" />
|
||||
<stop offset="1" stopColor="#917FFB" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,47 +7,15 @@ export function InsightIcon(props: { className?: string }) {
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={props.className}
|
||||
role="presentation"
|
||||
>
|
||||
<title>Insight</title>
|
||||
<circle
|
||||
cx="8.11963"
|
||||
cy="12.9889"
|
||||
r="4.505"
|
||||
fill="url(#paint0_linear_1095_479)"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M21.2163 7.97235C21.2163 7.96914 21.2164 7.96592 21.2164 7.96271C21.2164 5.60751 18.4863 3.69824 15.1186 3.69824C11.7509 3.69824 9.02089 5.60751 9.02089 7.96271C9.02089 7.96324 9.02089 7.96378 9.02089 7.96431H9.02087V17.0369C9.02087 17.0372 9.02087 17.0374 9.02087 17.0376C9.02137 19.3925 11.7512 21.3014 15.1186 21.3014C18.486 21.3014 21.2158 19.3925 21.2163 17.0376C21.2163 17.0374 21.2163 17.0372 21.2163 17.0369V7.97235Z"
|
||||
fill="#06B6D4"
|
||||
/>
|
||||
<path
|
||||
d="M11.1232 10.3516V10.3516C13.5336 11.9944 16.704 11.9944 19.1145 10.3515V10.3515"
|
||||
stroke="white"
|
||||
strokeWidth="1.5"
|
||||
d="M20.5 7.27783L12 12.0001M12 12.0001L3.49997 7.27783M12 12.0001L12 21.5001M21 12.0001V7.94153C21 7.59889 21 7.42757 20.9495 7.27477C20.9049 7.13959 20.8318 7.01551 20.7354 6.91082C20.6263 6.79248 20.4766 6.70928 20.177 6.54288L12.777 2.43177C12.4934 2.27421 12.3516 2.19543 12.2015 2.16454C12.0685 2.13721 11.9315 2.13721 11.7986 2.16454C11.6484 2.19543 11.5066 2.27421 11.223 2.43177L3.82297 6.54288C3.52345 6.70928 3.37368 6.79248 3.26463 6.91082C3.16816 7.01551 3.09515 7.13959 3.05048 7.27477C3 7.42757 3 7.59889 3 7.94153V16.0586C3 16.4013 3 16.5726 3.05048 16.7254C3.09515 16.8606 3.16816 16.9847 3.26463 17.0893C3.37368 17.2077 3.52346 17.2909 3.82297 17.4573L11.223 21.5684C11.5066 21.726 11.6484 21.8047 11.7986 21.8356C11.9315 21.863 12.0685 21.863 12.2015 21.8356C12.3516 21.8047 12.4934 21.726 12.777 21.5684L13 21.4445M7.5 4.50008L16.5 9.50008M22 21.5001L21 20.5001M22 18.0001C22 19.6569 20.6569 21.0001 19 21.0001C17.3431 21.0001 16 19.6569 16 18.0001C16 16.3432 17.3431 15.0001 19 15.0001C20.6569 15.0001 22 16.3432 22 18.0001Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M11.1232 15.0107V15.0107C13.5336 16.6536 16.704 16.6535 19.1145 15.0107V15.0107"
|
||||
stroke="white"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_1095_479"
|
||||
x1="8.11963"
|
||||
y1="16.3861"
|
||||
x2="-1.14166"
|
||||
y2="3.67577"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#155E75" />
|
||||
<stop offset="1" stopColor="#27B0DB" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||