Compare commits
67
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd42363602 | ||
|
|
42cdc3a2bc | ||
|
|
4abf7a7f25 | ||
|
|
8b0fd13adb | ||
|
|
20bd3aa399 | ||
|
|
fcfd22c01e | ||
|
|
8dc23e376e | ||
|
|
542f97dc4b | ||
|
|
46ca21d128 | ||
|
|
551349cbc6 | ||
|
|
4bf8296546 | ||
|
|
7efef7d9e1 | ||
|
|
4d85aa56c6 | ||
|
|
decc1a56d2 | ||
|
|
a60db3a057 | ||
|
|
377f176b10 | ||
|
|
5ead75e41a | ||
|
|
8836c27b8d | ||
|
|
cbd6fd87d0 | ||
|
|
ef9cc55b69 | ||
|
|
15899ea231 | ||
|
|
bd4669984d | ||
|
|
fb799ccb6d | ||
|
|
c8f7a68225 | ||
|
|
246701d615 | ||
|
|
cb591aa6af | ||
|
|
09927f3ac5 | ||
|
|
7778063458 | ||
|
|
574ff0295d | ||
|
|
a43907e2df | ||
|
|
f6d6c86ec0 | ||
|
|
23041c013f | ||
|
|
43f9319ac7 | ||
|
|
fc8daaa2bc | ||
|
|
9133b45700 | ||
|
|
44c78f4643 | ||
|
|
7aee37b10f | ||
|
|
23a665bdaa | ||
|
|
9e46b5b94e | ||
|
|
e0dcc64256 | ||
|
|
f0d6e343ee | ||
|
|
cf4443a9eb | ||
|
|
355795a2d4 | ||
|
|
5fda794569 | ||
|
|
93b3bad783 | ||
|
|
f5952621ab | ||
|
|
1d9f4e5e3e | ||
|
|
2e99057f90 | ||
|
|
13764f1abb | ||
|
|
997c3945a7 | ||
|
|
2bf2e1eec4 | ||
|
|
9b3725a56e | ||
|
|
beafe43f46 | ||
|
|
214a74c36a | ||
|
|
d0e3c4defc | ||
|
|
0cdf34ea98 | ||
|
|
b1632a3df1 | ||
|
|
d3f43a757b | ||
|
|
825352476c | ||
|
|
8caa095a67 | ||
|
|
997e1197e9 | ||
|
|
c9ac25fe3f | ||
|
|
79260c8cd0 | ||
|
|
cf8badbc46 | ||
|
|
09e4d80b89 | ||
|
|
1edd6c7e4c | ||
|
|
588f19639f |
@@ -19,7 +19,21 @@ env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
jobs:
|
||||
optimize_ci:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
skip: ${{ steps.check_skip.outputs.skip }}
|
||||
steps:
|
||||
- name: Optimize CI
|
||||
id: check_skip
|
||||
uses: withgraphite/graphite-ci-action@main
|
||||
with:
|
||||
graphite_token: ${{ secrets.GRAPHITE_OMTIMIZE_TOKEN }}
|
||||
|
||||
|
||||
build:
|
||||
needs: optimize_ci
|
||||
if: needs.optimize_ci.outputs.skip == 'false'
|
||||
runs-on: ubuntu-latest
|
||||
name: Build Packages
|
||||
steps:
|
||||
@@ -33,6 +47,8 @@ jobs:
|
||||
run: pnpm build:packages
|
||||
|
||||
lint:
|
||||
needs: optimize_ci
|
||||
if: needs.optimize_ci.outputs.skip == 'false'
|
||||
timeout-minutes: 15
|
||||
name: Lint Packages
|
||||
runs-on: ubuntu-latest
|
||||
@@ -51,6 +67,8 @@ jobs:
|
||||
- run: pnpm lint
|
||||
|
||||
test:
|
||||
needs: optimize_ci
|
||||
if: needs.optimize_ci.outputs.skip == 'false'
|
||||
timeout-minutes: 15
|
||||
name: Unit Tests
|
||||
runs-on: ubuntu-latest
|
||||
@@ -77,6 +95,8 @@ jobs:
|
||||
verbose: true
|
||||
|
||||
e2e:
|
||||
needs: optimize_ci
|
||||
if: needs.optimize_ci.outputs.skip == 'false'
|
||||
timeout-minutes: 15
|
||||
name: E2E Tests
|
||||
runs-on: ubuntu-latest
|
||||
@@ -144,8 +164,9 @@ jobs:
|
||||
fi
|
||||
|
||||
size:
|
||||
needs: optimize_ci
|
||||
# only run on pull requests
|
||||
if: github.event_name == 'pull_request'
|
||||
if: github.event_name == 'pull_request' && needs.optimize_ci.outputs.skip == 'false'
|
||||
timeout-minutes: 15
|
||||
name: "Size"
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -27,7 +27,20 @@ env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
jobs:
|
||||
optimize_ci:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
skip: ${{ steps.check_skip.outputs.skip }}
|
||||
steps:
|
||||
- name: Optimize CI
|
||||
id: check_skip
|
||||
uses: withgraphite/graphite-ci-action@main
|
||||
with:
|
||||
graphite_token: ${{ secrets.GRAPHITE_OMTIMIZE_TOKEN }}
|
||||
|
||||
build:
|
||||
needs: optimize_ci
|
||||
if: needs.optimize_ci.outputs.skip == 'false'
|
||||
runs-on: ubuntu-latest
|
||||
name: Build Packages
|
||||
steps:
|
||||
@@ -41,6 +54,8 @@ jobs:
|
||||
run: pnpm build:legacy_packages
|
||||
|
||||
lint:
|
||||
needs: optimize_ci
|
||||
if: needs.optimize_ci.outputs.skip == 'false'
|
||||
timeout-minutes: 15
|
||||
name: Lint Packages
|
||||
runs-on: ubuntu-latest
|
||||
@@ -54,6 +69,8 @@ jobs:
|
||||
- run: pnpm lint:legacy
|
||||
|
||||
test:
|
||||
needs: optimize_ci
|
||||
if: needs.optimize_ci.outputs.skip == 'false'
|
||||
timeout-minutes: 15
|
||||
name: Unit Tests
|
||||
runs-on: ubuntu-latest
|
||||
@@ -74,6 +91,8 @@ jobs:
|
||||
verbose: true
|
||||
|
||||
e2e:
|
||||
needs: optimize_ci
|
||||
if: needs.optimize_ci.outputs.skip == 'false'
|
||||
timeout-minutes: 15
|
||||
name: E2E Tests
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -40,6 +40,9 @@ NEXT_PUBLIC_STRIPE_KEY=
|
||||
|
||||
NEXT_PUBLIC_STRIPE_PAYMENT_METHOD_CFG_ID=
|
||||
|
||||
# Needed for contract analytics / blockchain data information
|
||||
CHAINSAW_API_KEY=
|
||||
|
||||
#
|
||||
# Private (server)
|
||||
#
|
||||
|
||||
@@ -80,6 +80,12 @@ module.exports = {
|
||||
message:
|
||||
"Import from `@chakra-ui/react` instead of `@chakra-ui/menu`.",
|
||||
},
|
||||
{
|
||||
name: "next/navigation",
|
||||
importNames: ["useRouter"],
|
||||
message:
|
||||
'Use `import { useDashboardRouter } from "@/lib/DashboardRouter";` instead',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"sonner": "^1.5.0",
|
||||
"spdx-correct": "^3.2.0",
|
||||
"swagger-ui-react": "^5.17.14",
|
||||
"tailwind-merge": "^2.4.0",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"thirdweb": "workspace:*",
|
||||
"tiny-invariant": "^1.3.3",
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
{
|
||||
"applinks": {},
|
||||
"applinks": {
|
||||
"apps": ["XYHGSFAMHX.com.thirdweb.demo"],
|
||||
"details": [
|
||||
{
|
||||
"appIDs": ["XYHGSFAMHX.com.thirdweb.demo"],
|
||||
"components": [
|
||||
{
|
||||
"#": "no_universal_links",
|
||||
"exclude": true,
|
||||
"comment": "Matches any URL with a fragment that equals no_universal_links and instructs the system not to open it as a universal link."
|
||||
},
|
||||
{
|
||||
"/": "/mobile-wallet-protocol",
|
||||
"comment": "Matches any URL with a path that starts with /mobile-wallet-protocol for coinbase wallet mobile redirects."
|
||||
},
|
||||
{
|
||||
"/": "/dashboard/*",
|
||||
"exclude": true,
|
||||
"comment": "no universal link for dashboard"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"webcredentials": {
|
||||
"apps": ["XYHGSFAMHX.com.thirdweb.demo"]
|
||||
},
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
"use client";
|
||||
|
||||
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
type ChartConfig,
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/components/ui/chart";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export type ThirdwebAreaChartProps<TConfig extends ChartConfig> = {
|
||||
// metadata
|
||||
title: string;
|
||||
description?: string;
|
||||
// chart config
|
||||
config: TConfig;
|
||||
data: Array<Record<keyof TConfig, number> & { time: number | string | Date }>;
|
||||
showLegend?: boolean;
|
||||
|
||||
// chart className
|
||||
chartClassName?: string;
|
||||
};
|
||||
|
||||
export function ThirdwebAreaChart<TConfig extends ChartConfig>(
|
||||
props: ThirdwebAreaChartProps<TConfig>,
|
||||
) {
|
||||
const configKeys = useMemo(() => Object.keys(props.config), [props.config]);
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{props.title}</CardTitle>
|
||||
{props.description && (
|
||||
<CardDescription>{props.description}</CardDescription>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={props.config} className={props.chartClassName}>
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
data={props.data}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickFormatter={(value) => new Date(value).toLocaleDateString()}
|
||||
/>
|
||||
<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 />} />
|
||||
)}
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
"use client";
|
||||
|
||||
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts";
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
type ChartConfig,
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/components/ui/chart";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export type ThirdwebBarChartProps<TConfig extends ChartConfig> = {
|
||||
// metadata
|
||||
title: string;
|
||||
description?: string;
|
||||
// chart config
|
||||
config: TConfig;
|
||||
data: Array<Record<keyof TConfig, number> & { time: number | string | Date }>;
|
||||
showLegend?: boolean;
|
||||
variant?: "stacked" | "grouped";
|
||||
// chart className
|
||||
chartClassName?: string;
|
||||
};
|
||||
|
||||
export function ThirdwebBarChart<TConfig extends ChartConfig>(
|
||||
props: ThirdwebBarChartProps<TConfig>,
|
||||
) {
|
||||
const configKeys = useMemo(() => Object.keys(props.config), [props.config]);
|
||||
// if there are more than 4 keys then we should stack them by default
|
||||
const variant =
|
||||
props.variant || configKeys.length > 4 ? "stacked" : "grouped";
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{props.title}</CardTitle>
|
||||
{props.description && (
|
||||
<CardDescription>{props.description}</CardDescription>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={props.config} className={props.chartClassName}>
|
||||
<BarChart accessibilityLayer data={props.data}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={10}
|
||||
tickFormatter={(value) => new Date(value).toLocaleDateString()}
|
||||
/>
|
||||
<ChartTooltip content={<ChartTooltipContent hideLabel />} />
|
||||
{props.showLegend && (
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
)}
|
||||
{configKeys.map((key, idx) => (
|
||||
<Bar
|
||||
key={key}
|
||||
dataKey={key}
|
||||
// if stacked then they should all be the same stackId
|
||||
// if grouped then they should all be unique stackId (so the key works great)
|
||||
stackId={variant === "stacked" ? "a" : key}
|
||||
fill={`var(--color-${key})`}
|
||||
// if stacked then we need to figure out the radius based on the index in the array
|
||||
// if grouped then we can just use the same radius for all
|
||||
radius={
|
||||
variant === "stacked"
|
||||
? idx === 0
|
||||
? [0, 0, 4, 4]
|
||||
: idx === configKeys.length - 1
|
||||
? [4, 4, 0, 0]
|
||||
: [0, 0, 0, 0]
|
||||
: [4, 4, 4, 4]
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
.unorderedList,
|
||||
.orderedList {
|
||||
@apply pl-5 leading-relaxed list-none text-secondary-foreground;
|
||||
}
|
||||
|
||||
.unorderedList > li {
|
||||
@apply pb-1 pl-2 list-disc;
|
||||
}
|
||||
|
||||
.orderedList > li {
|
||||
@apply pb-1 pl-2 list-decimal;
|
||||
}
|
||||
|
||||
.unorderedList > li::marker {
|
||||
@apply text-muted-foreground !m-0 text-left;
|
||||
}
|
||||
|
||||
.orderedList > li::marker {
|
||||
@apply text-muted-foreground text-left;
|
||||
}
|
||||
|
||||
.unorderedList > li:hover::marker,
|
||||
.orderedList > li:hover::marker {
|
||||
@apply text-foreground;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import styles from "./List.module.css";
|
||||
|
||||
export function UnorderedList(props: { children?: React.ReactNode }) {
|
||||
return <ul className={styles.unorderedList}>{props.children}</ul>;
|
||||
}
|
||||
|
||||
export function OrderedList(props: { children?: React.ReactNode }) {
|
||||
return <ul className={styles.orderedList}>{props.children}</ul>;
|
||||
}
|
||||
@@ -9,7 +9,7 @@ const Card = React.forwardRef<
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||
"rounded-lg border border-border bg-card text-card-foreground shadow-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -283,7 +283,7 @@ const ChartLegendContent = React.forwardRef<
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex items-center justify-center gap-4",
|
||||
"flex items-center justify-center gap-4 overflow-x-auto",
|
||||
verticalAlign === "top" ? "pb-3" : "pt-3",
|
||||
className,
|
||||
)}
|
||||
|
||||
@@ -47,7 +47,7 @@ const DropdownMenuSubContent = React.forwardRef<
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -65,7 +65,7 @@ const DropdownMenuContent = React.forwardRef<
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -31,7 +31,7 @@ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
|
||||
export function ToolTipLabel(props: {
|
||||
children: React.ReactNode;
|
||||
label: string | undefined;
|
||||
label: React.ReactNode;
|
||||
rightIcon?: React.ReactNode;
|
||||
leftIcon?: React.ReactNode;
|
||||
}) {
|
||||
@@ -47,7 +47,7 @@ export function ToolTipLabel(props: {
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
sideOffset={10}
|
||||
className="max-w-[300px] leading-relaxed"
|
||||
className="max-w-[400px] leading-relaxed whitespace-normal"
|
||||
>
|
||||
<div className="p-2 text-sm flex items-center gap-1.5">
|
||||
{props.leftIcon}
|
||||
|
||||
@@ -1,11 +1,30 @@
|
||||
import {
|
||||
DASHBOARD_THIRDWEB_CLIENT_ID,
|
||||
DASHBOARD_THIRDWEB_SECRET_KEY,
|
||||
IPFS_GATEWAY_URL,
|
||||
} from "@/constants/env";
|
||||
import { createThirdwebClient } from "thirdweb";
|
||||
import { DASHBOARD_THIRDWEB_CLIENT_ID, IPFS_GATEWAY_URL } from "./env";
|
||||
|
||||
export const thirdwebClient = createThirdwebClient({
|
||||
clientId: DASHBOARD_THIRDWEB_CLIENT_ID,
|
||||
config: {
|
||||
storage: {
|
||||
gatewayUrl: IPFS_GATEWAY_URL,
|
||||
},
|
||||
},
|
||||
});
|
||||
export const thirdwebClient = createThirdwebClient(
|
||||
DASHBOARD_THIRDWEB_SECRET_KEY
|
||||
? {
|
||||
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
|
||||
config: {
|
||||
storage: {
|
||||
gatewayUrl: IPFS_GATEWAY_URL,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
clientId: DASHBOARD_THIRDWEB_CLIENT_ID,
|
||||
config: {
|
||||
storage: {
|
||||
gatewayUrl: IPFS_GATEWAY_URL,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* DO NOT ADD ANYTHING TO THIS FILE IF YOU ARE NOT ABSOLUTELY SURE IT IS OK
|
||||
*/
|
||||
|
||||
@@ -10,3 +10,9 @@ export const IPFS_GATEWAY_URL =
|
||||
|
||||
export const THIRDWEB_ENGINE_FAUCET_WALLET =
|
||||
process.env.NEXT_PUBLIC_THIRDWEB_ENGINE_FAUCET_WALLET || "";
|
||||
|
||||
export const isProd =
|
||||
(process.env.VERCEL_ENV || process.env.NEXT_PUBLIC_VERCEL_ENV) ===
|
||||
"production";
|
||||
|
||||
export const PROD_OR_DEV_URL = isProd ? "thirdweb.com" : "thirdweb-dev.com";
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
"use client";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import {
|
||||
Suspense,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
useSyncExternalStore,
|
||||
} from "react";
|
||||
import { createStore } from "./reactive";
|
||||
|
||||
// Using useDashboardRouter instead of useRouter gives us a nice progress bar on top of the page when navigating using router.push or router.replace
|
||||
|
||||
// using a store instead of context to avoid triggering re-renders on root component
|
||||
export const LoadingRouteHref = createStore<string | undefined>(undefined);
|
||||
|
||||
export function useDashboardRouter() {
|
||||
const router = useRouter();
|
||||
return useMemo(() => {
|
||||
return {
|
||||
...router,
|
||||
replace(href: string, options?: { scroll?: boolean }) {
|
||||
LoadingRouteHref.setValue(href);
|
||||
router.replace(href, options);
|
||||
},
|
||||
push(href: string, options?: { scroll?: boolean }) {
|
||||
LoadingRouteHref.setValue(href);
|
||||
router.push(href, options);
|
||||
},
|
||||
};
|
||||
}, [router]);
|
||||
}
|
||||
|
||||
function useRouterLoadingStatus() {
|
||||
const loadingHref = useSyncExternalStore(
|
||||
LoadingRouteHref.subscribe,
|
||||
LoadingRouteHref.getValue,
|
||||
LoadingRouteHref.getValue,
|
||||
);
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const searchParamsStr = searchParams?.toString();
|
||||
|
||||
const routerHref = pathname + (searchParamsStr ? `?${searchParamsStr}` : "");
|
||||
const isLoading = loadingHref && loadingHref !== routerHref;
|
||||
|
||||
// reset loading on route load
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
if (!isLoading) {
|
||||
LoadingRouteHref.setValue(undefined);
|
||||
}
|
||||
}, [isLoading]);
|
||||
|
||||
return isLoading;
|
||||
}
|
||||
|
||||
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
// Render this on root
|
||||
function DashboardRouterTopProgressBarInner() {
|
||||
const isLoading = useRouterLoadingStatus();
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
const progressStartedRef = useRef(false);
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
if (!isLoading) {
|
||||
setProgress(0);
|
||||
progressStartedRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (progressStartedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
let isMounted = true;
|
||||
|
||||
progressStartedRef.current = true;
|
||||
|
||||
async function updateProgressBar(progress: number, delay: number) {
|
||||
if (!isMounted) {
|
||||
if (!isLoading) {
|
||||
setProgress(100);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setProgress(progress);
|
||||
await wait(delay);
|
||||
|
||||
// increase progress by 10% of the remaining progress
|
||||
// increase the delay by 5% to slow down number of updates for slower progress
|
||||
updateProgressBar(progress + (100 - progress) / 10, delay * 1.05);
|
||||
}
|
||||
|
||||
async function startEffect() {
|
||||
// if the loading state remains for atleast 500ms start the progress bar
|
||||
await wait(500);
|
||||
if (isMounted) {
|
||||
updateProgressBar(0, 100);
|
||||
}
|
||||
}
|
||||
|
||||
startEffect();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [isLoading]);
|
||||
|
||||
const width = isLoading ? progress : 100;
|
||||
return (
|
||||
<span
|
||||
className="block fixed top-0 h-[2px] bg-blue-500"
|
||||
style={{
|
||||
opacity: isLoading ? "100" : "0",
|
||||
width: `${width}%`,
|
||||
transition: width === 0 ? "none" : "width 0.2s ease, opacity 0.3s ease",
|
||||
zIndex: "100000000",
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// need to wrap with suspense because of useSearchParams usage
|
||||
export function DashboardRouterTopProgressBar() {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<DashboardRouterTopProgressBarInner />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
export type Store<T> = {
|
||||
getValue(): T;
|
||||
setValue(newValue: T): void;
|
||||
subscribe(listener: () => void): () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a reactive value store
|
||||
* @param initialValue - The initial value to store
|
||||
* @example
|
||||
* ```ts
|
||||
* const store = createStore(0);
|
||||
* ```
|
||||
* @returns A store object
|
||||
* @internal
|
||||
*/
|
||||
export function createStore<T>(initialValue: T): Store<T> {
|
||||
type Listener = () => void;
|
||||
const listeners = new Set<Listener>();
|
||||
|
||||
let value = initialValue;
|
||||
|
||||
const notify = () => {
|
||||
for (const listener of listeners) {
|
||||
listener();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
getValue() {
|
||||
return value;
|
||||
},
|
||||
setValue(newValue: T) {
|
||||
if (newValue === value) {
|
||||
return;
|
||||
}
|
||||
value = newValue;
|
||||
notify();
|
||||
},
|
||||
subscribe(listener: Listener) {
|
||||
listeners.add(listener);
|
||||
return () => {
|
||||
listeners.delete(listener);
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -94,19 +94,22 @@
|
||||
/* shadcn charts */
|
||||
@layer base {
|
||||
:root {
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
/* below is ai generated based on the initial */
|
||||
--chart-6: 200 65% 50%;
|
||||
--chart-7: 120 70% 45%;
|
||||
--chart-8: 45 75% 55%;
|
||||
--chart-9: 260 60% 60%;
|
||||
--chart-10: 0 70% 50%;
|
||||
--chart-11: 180 60% 50%;
|
||||
--chart-12: 100 65% 55%;
|
||||
--chart-13: 360 70% 55%;
|
||||
--chart-14: 240 65% 50%;
|
||||
--chart-15: 80 60% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -333,6 +333,7 @@ export type Transaction = {
|
||||
gasPrice?: string | null;
|
||||
maxFeePerGas?: string | null;
|
||||
maxPriorityFeePerGas?: string | null;
|
||||
effectiveGasPrice?: string | null;
|
||||
transactionType?: number | null;
|
||||
transactionHash?: string | null;
|
||||
queuedAt?: string | null;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { isLoginRequired } from "@/constants/auth";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { EnsureLoginResponse } from "app/api/auth/ensure-login/route";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useRef } from "react";
|
||||
import {
|
||||
useActiveAccount,
|
||||
@@ -20,7 +21,7 @@ export function useLoggedInUser(): {
|
||||
isLoggedIn: boolean;
|
||||
user: { address: string; jwt?: string } | null;
|
||||
} {
|
||||
const router = useRouter();
|
||||
const router = useDashboardRouter();
|
||||
const pathname = usePathname();
|
||||
const connectedAddress = useActiveAccount()?.address;
|
||||
const connectionStatus = useActiveWalletConnectionStatus();
|
||||
|
||||
@@ -1,38 +1,27 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useSigner } from "@thirdweb-dev/react";
|
||||
import { addContractToMultiChainRegistry } from "components/contract-components/utils";
|
||||
import { MULTICHAIN_REGISTRY_CONTRACT } from "constants/contracts";
|
||||
import {
|
||||
addContractToMultiChainRegistry,
|
||||
getGaslessPolygonSDK,
|
||||
} from "components/contract-components/utils";
|
||||
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 { useSupportedChainsRecord } from "hooks/chains/configureChains";
|
||||
import { defineDashboardChain } from "lib/defineDashboardChain";
|
||||
import { thirdwebClient } from "lib/thirdweb-client";
|
||||
import { useMemo } from "react";
|
||||
import { getContract } from "thirdweb";
|
||||
import { polygon } from "thirdweb/chains";
|
||||
import { sendAndConfirmTransaction } from "thirdweb";
|
||||
import { remove } from "thirdweb/extensions/thirdweb";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import invariant from "tiny-invariant";
|
||||
|
||||
const MULTICHAIN_REGISTRY_ADDRESS =
|
||||
"0xcdAD8FA86e18538aC207872E8ff3536501431B73";
|
||||
|
||||
const registryContract = getContract({
|
||||
client: thirdwebClient,
|
||||
address: MULTICHAIN_REGISTRY_ADDRESS,
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
chain: defineDashboardChain(polygon.id, undefined),
|
||||
});
|
||||
|
||||
function useMultiChainRegContractList(walletAddress?: string) {
|
||||
return useQuery(
|
||||
["dashboard-registry", walletAddress, "multichain-contract-list"],
|
||||
async () => {
|
||||
invariant(walletAddress, "walletAddress is required");
|
||||
const contracts = await getAllMultichainRegistry({
|
||||
contract: registryContract,
|
||||
contract: MULTICHAIN_REGISTRY_CONTRACT,
|
||||
address: walletAddress,
|
||||
});
|
||||
|
||||
@@ -97,28 +86,30 @@ type RemoveContractParams = {
|
||||
};
|
||||
|
||||
export function useRemoveContractMutation() {
|
||||
const walletAddress = useActiveAccount()?.address;
|
||||
const signer = useSigner();
|
||||
const { chainIdToChainRecord } = useAllChainsData();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const account = useActiveAccount();
|
||||
return useMutation(
|
||||
async (data: RemoveContractParams) => {
|
||||
invariant(
|
||||
walletAddress,
|
||||
"cannot add a contract without a wallet address",
|
||||
);
|
||||
invariant(chainIdToChainRecord, "chains not initialzed yet");
|
||||
invariant(signer, "no wallet connected");
|
||||
invariant(data.chainId, "chainId not provided");
|
||||
|
||||
invariant(account, "No wallet connected");
|
||||
const { contractAddress, chainId } = data;
|
||||
|
||||
const gaslessPolygonSDK = getGaslessPolygonSDK(signer);
|
||||
return await gaslessPolygonSDK.multiChainRegistry?.removeContract({
|
||||
address: contractAddress,
|
||||
chainId,
|
||||
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,
|
||||
},
|
||||
});
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { Abi } from "abitype";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { resolveContractAbi } from "thirdweb/contract";
|
||||
import invariant from "tiny-invariant";
|
||||
|
||||
export function useResolveContractAbi(contract?: ThirdwebContract) {
|
||||
return useQuery({
|
||||
queryKey: [
|
||||
"full-contract-abi",
|
||||
contract?.chain.id || "",
|
||||
contract?.address || "",
|
||||
],
|
||||
queryFn: async () => {
|
||||
invariant(contract, "contract is required");
|
||||
const abi = await resolveContractAbi<Abi>(contract);
|
||||
return abi;
|
||||
},
|
||||
enabled: !!contract,
|
||||
});
|
||||
}
|
||||
@@ -1,18 +1,36 @@
|
||||
import { useContract, useContractMetadata, useSDK } from "@thirdweb-dev/react";
|
||||
import type { Vote, VoteType } from "@thirdweb-dev/sdk";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import { thirdwebClient } from "@/constants/client";
|
||||
import { type ThirdwebContract, getContract, toTokens } from "thirdweb";
|
||||
import {
|
||||
balanceOf,
|
||||
decimals,
|
||||
delegate,
|
||||
delegates,
|
||||
} from "thirdweb/extensions/erc20";
|
||||
import {
|
||||
type VoteType,
|
||||
canExecute,
|
||||
castVoteWithReason,
|
||||
executeProposal,
|
||||
getAll,
|
||||
hasVoted,
|
||||
propose,
|
||||
token,
|
||||
} from "thirdweb/extensions/vote";
|
||||
import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react";
|
||||
import invariant from "tiny-invariant";
|
||||
import type { RequiredParam } from "utils/types";
|
||||
import { voteKeys } from "../cache-keys";
|
||||
import {
|
||||
useMutationWithInvalidate,
|
||||
useQueryWithNetwork,
|
||||
} from "./query/useQueryWithNetwork";
|
||||
|
||||
export function useVoteProposalList(contract?: Vote) {
|
||||
export function useVoteProposalList(contract?: ThirdwebContract) {
|
||||
return useQueryWithNetwork(
|
||||
voteKeys.proposals(contract?.getAddress()),
|
||||
async () => await contract?.getAll(),
|
||||
voteKeys.proposals(contract?.address || ""),
|
||||
async () => {
|
||||
invariant(contract, "contract is required");
|
||||
return await getAll({ contract });
|
||||
},
|
||||
{
|
||||
enabled: !!contract,
|
||||
},
|
||||
@@ -20,17 +38,20 @@ export function useVoteProposalList(contract?: Vote) {
|
||||
}
|
||||
|
||||
export function useHasVotedOnProposal(
|
||||
contract: RequiredParam<Vote>,
|
||||
proposalId: string,
|
||||
contract: ThirdwebContract,
|
||||
proposalId: bigint,
|
||||
) {
|
||||
const address = useActiveAccount()?.address;
|
||||
const userAddress = useActiveAccount()?.address;
|
||||
return useQueryWithNetwork(
|
||||
voteKeys.userHasVotedOnProposal(
|
||||
proposalId,
|
||||
contract?.getAddress(),
|
||||
address,
|
||||
proposalId.toString(),
|
||||
contract.address,
|
||||
userAddress,
|
||||
),
|
||||
async () => await contract?.hasVoted(proposalId, address),
|
||||
async () => {
|
||||
invariant(userAddress, "address is required");
|
||||
return await hasVoted({ contract, proposalId, account: userAddress });
|
||||
},
|
||||
{
|
||||
enabled: !!contract,
|
||||
},
|
||||
@@ -38,65 +59,83 @@ export function useHasVotedOnProposal(
|
||||
}
|
||||
|
||||
export function useCanExecuteProposal(
|
||||
contract: RequiredParam<Vote>,
|
||||
proposalId: string,
|
||||
contract: ThirdwebContract,
|
||||
proposalId: bigint,
|
||||
) {
|
||||
return useQueryWithNetwork(
|
||||
voteKeys.canExecuteProposal(proposalId, contract?.getAddress()),
|
||||
async () => await contract?.canExecute(proposalId),
|
||||
voteKeys.canExecuteProposal(proposalId.toString(), contract.address),
|
||||
async () => await canExecute({ contract, proposalId }),
|
||||
{
|
||||
enabled: !!contract,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function useTokensDelegated(contract?: Vote) {
|
||||
const sdk = useSDK();
|
||||
const address = useActiveAccount()?.address;
|
||||
export function useTokensDelegated(contract: ThirdwebContract) {
|
||||
const userAddress = useActiveAccount()?.address;
|
||||
|
||||
return useQueryWithNetwork(
|
||||
voteKeys.delegation(contract?.getAddress(), address),
|
||||
voteKeys.delegation(contract.address, userAddress),
|
||||
async () => {
|
||||
invariant(address, "address is required");
|
||||
invariant(contract, "vote contract is required");
|
||||
|
||||
const metadata = await contract?.metadata.get();
|
||||
const tokenAddress = metadata?.voting_token_address;
|
||||
const tokenContract = await sdk?.getToken(tokenAddress);
|
||||
const delegation = await tokenContract?.getDelegationOf(address);
|
||||
return delegation?.toLowerCase() === address.toLowerCase();
|
||||
invariant(userAddress, "wallet address is required");
|
||||
const tokenAddress = await token({ contract });
|
||||
if (!tokenAddress) {
|
||||
throw new Error("Expected a delegated token address");
|
||||
}
|
||||
const tokenContract = getContract({
|
||||
address: tokenAddress,
|
||||
chain: contract.chain,
|
||||
client: thirdwebClient,
|
||||
});
|
||||
const delegatedAddress = await delegates({
|
||||
contract: tokenContract,
|
||||
account: userAddress,
|
||||
});
|
||||
return delegatedAddress?.toLowerCase() === userAddress.toLowerCase();
|
||||
},
|
||||
{
|
||||
enabled: !!contract && !!address,
|
||||
enabled: !!contract && !!userAddress,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function useVoteTokenBalances(contract?: Vote, addresses?: string[]) {
|
||||
const { data } = useContractMetadata(contract);
|
||||
const tokenContract = useContract(
|
||||
data?.voting_token_address,
|
||||
"token",
|
||||
).contract;
|
||||
|
||||
export function useVoteTokenBalances(
|
||||
contract: ThirdwebContract,
|
||||
addresses: string[],
|
||||
) {
|
||||
return useQueryWithNetwork(
|
||||
voteKeys.balances(contract?.getAddress(), addresses),
|
||||
async () => {
|
||||
invariant(data, "contract metadata is required");
|
||||
invariant(tokenContract, "voting contract is required");
|
||||
invariant(addresses, "addresses are required");
|
||||
|
||||
const balances = addresses.map(async (address) => {
|
||||
voteKeys.balances(contract.address, addresses),
|
||||
async (): Promise<
|
||||
Array<{ address: string; balance: string; decimals: number }>
|
||||
> => {
|
||||
invariant(addresses.length, "addresses are required");
|
||||
const tokenAddress = await token({ contract });
|
||||
if (!tokenAddress) {
|
||||
throw new Error("Expected a delegated token address");
|
||||
}
|
||||
const tokenContract = getContract({
|
||||
address: tokenAddress,
|
||||
chain: contract.chain,
|
||||
client: thirdwebClient,
|
||||
});
|
||||
const _decimals = await decimals({ contract: tokenContract });
|
||||
const balanceData = await Promise.all(
|
||||
addresses.map((address) =>
|
||||
balanceOf({ contract: tokenContract, address }),
|
||||
),
|
||||
);
|
||||
const balances = addresses.map((address, index) => {
|
||||
const balance = balanceData[index] || 0n;
|
||||
return {
|
||||
address,
|
||||
balance: (await tokenContract.balanceOf(address)).displayValue,
|
||||
balance: toTokens(balance, _decimals),
|
||||
decimals: _decimals,
|
||||
};
|
||||
});
|
||||
|
||||
return await Promise.all(balances);
|
||||
},
|
||||
{
|
||||
enabled: !!data && !!contract && !!addresses?.length,
|
||||
enabled: addresses.length > 0,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -105,94 +144,143 @@ export interface IProposalInput {
|
||||
description: string;
|
||||
}
|
||||
|
||||
export function useProposalCreateMutation(contractAddress?: string) {
|
||||
const voteContract = useContract(contractAddress, "vote").contract;
|
||||
/**
|
||||
* This hook is a simplified version meant to use on the Dashboard
|
||||
* since _currently_ the dashboard only supports creating proposals with descriptions
|
||||
*
|
||||
* todo: make an extension in the SDK that is more robust and user-friendly ?
|
||||
*/
|
||||
export function useProposalCreateMutation(contract: ThirdwebContract) {
|
||||
const { mutateAsync } = useSendAndConfirmTransaction();
|
||||
return useMutationWithInvalidate(
|
||||
(proposal: IProposalInput) => {
|
||||
invariant(voteContract, "contract is required");
|
||||
async (proposal: IProposalInput) => {
|
||||
const { description } = proposal;
|
||||
return voteContract.propose(description);
|
||||
const transaction = propose({
|
||||
contract,
|
||||
description,
|
||||
targets: [contract.address],
|
||||
values: [0n],
|
||||
calldatas: ["0x"],
|
||||
});
|
||||
return await mutateAsync(transaction);
|
||||
},
|
||||
{
|
||||
onSuccess: (_data, _options, _variables, invalidate) => {
|
||||
return invalidate([voteKeys.proposals(contractAddress)]);
|
||||
return invalidate([voteKeys.proposals(contract.address)]);
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function useDelegateMutation(contract?: Vote) {
|
||||
const sdk = useSDK();
|
||||
const address = useActiveAccount()?.address;
|
||||
|
||||
const contractAddress = contract?.getAddress();
|
||||
|
||||
export function useDelegateMutation(contract: ThirdwebContract) {
|
||||
const userAddress = useActiveAccount()?.address;
|
||||
const { mutateAsync } = useSendAndConfirmTransaction();
|
||||
return useMutationWithInvalidate(
|
||||
async () => {
|
||||
invariant(address, "address is required");
|
||||
invariant(contractAddress, "contract address is required");
|
||||
invariant(contract, "vote contract is required");
|
||||
|
||||
const metadata = await contract?.metadata.get();
|
||||
const tokenAddress = metadata?.voting_token_address;
|
||||
const tokenContract = await sdk?.getToken(tokenAddress);
|
||||
return tokenContract?.delegateTo(address);
|
||||
invariant(userAddress, "address is required");
|
||||
const tokenAddress = await token({ contract });
|
||||
if (!tokenAddress) {
|
||||
throw new Error("Expected a delegated token address");
|
||||
}
|
||||
const tokenContract = getContract({
|
||||
address: tokenAddress,
|
||||
chain: contract.chain,
|
||||
client: thirdwebClient,
|
||||
});
|
||||
const transaction = delegate({
|
||||
contract: tokenContract,
|
||||
delegatee: userAddress,
|
||||
});
|
||||
return await mutateAsync(transaction);
|
||||
},
|
||||
{
|
||||
onSuccess: (_data, _options, _variables, invalidate) => {
|
||||
return invalidate([voteKeys.delegation(contractAddress, address)]);
|
||||
return invalidate([voteKeys.delegation(contract.address, userAddress)]);
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the decimals of the voting erc20 token
|
||||
*/
|
||||
export function useVotingTokenDecimals(contract: ThirdwebContract) {
|
||||
return useMutationWithInvalidate(async () => {
|
||||
const tokenAddress = await token({ contract });
|
||||
if (!tokenAddress) {
|
||||
throw new Error("Expected a delegated token address");
|
||||
}
|
||||
const tokenContract = getContract({
|
||||
address: tokenAddress,
|
||||
chain: contract.chain,
|
||||
client: thirdwebClient,
|
||||
});
|
||||
const _decimals = await decimals({ contract: tokenContract });
|
||||
return _decimals;
|
||||
});
|
||||
}
|
||||
|
||||
interface IVoteCast {
|
||||
voteType: VoteType;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export function useCastVoteMutation(
|
||||
contract: RequiredParam<Vote>,
|
||||
proposalId: string,
|
||||
contract: ThirdwebContract,
|
||||
proposalId: bigint,
|
||||
) {
|
||||
const address = useActiveAccount()?.address;
|
||||
const contractAddress = contract?.getAddress();
|
||||
|
||||
const contractAddress = contract.address;
|
||||
const { mutateAsync } = useSendAndConfirmTransaction();
|
||||
return useMutationWithInvalidate(
|
||||
async (vote: IVoteCast) => {
|
||||
async (voteItem: IVoteCast) => {
|
||||
invariant(contract, "contract is required");
|
||||
invariant(address, "address is required");
|
||||
const { voteType, reason } = vote;
|
||||
return contract.vote(proposalId, voteType, reason);
|
||||
const { voteType, reason } = voteItem;
|
||||
const transaction = castVoteWithReason({
|
||||
contract,
|
||||
proposalId,
|
||||
support: voteType,
|
||||
reason: reason ?? "",
|
||||
});
|
||||
return await mutateAsync(transaction);
|
||||
},
|
||||
{
|
||||
onSuccess: (_data, _options, _variables, invalidate) => {
|
||||
return invalidate([
|
||||
voteKeys.proposals(contractAddress),
|
||||
voteKeys.userHasVotedOnProposal(proposalId, contractAddress, address),
|
||||
voteKeys.canExecuteProposal(proposalId, contractAddress),
|
||||
voteKeys.userHasVotedOnProposal(
|
||||
proposalId.toString(),
|
||||
contractAddress,
|
||||
address,
|
||||
),
|
||||
voteKeys.canExecuteProposal(proposalId.toString(), contractAddress),
|
||||
]);
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This hook is a simplified version for the Dashboard
|
||||
* It doesn't pass any extra info to the execute function
|
||||
*/
|
||||
export function useExecuteProposalMutation(
|
||||
contract: RequiredParam<Vote>,
|
||||
proposalId: string,
|
||||
contract: ThirdwebContract,
|
||||
proposalId: bigint,
|
||||
) {
|
||||
const contractAddress = contract?.getAddress();
|
||||
|
||||
const contractAddress = contract.address;
|
||||
const mutation = useSendAndConfirmTransaction();
|
||||
return useMutationWithInvalidate(
|
||||
async () => {
|
||||
invariant(contract, "contract is required");
|
||||
return contract.execute(proposalId);
|
||||
const transaction = executeProposal({ contract, proposalId });
|
||||
return await mutation.mutateAsync(transaction);
|
||||
},
|
||||
{
|
||||
onSuccess: (_data, _options, _variables, invalidate) => {
|
||||
return invalidate([
|
||||
voteKeys.proposals(contractAddress),
|
||||
voteKeys.canExecuteProposal(proposalId, contractAddress),
|
||||
voteKeys.canExecuteProposal(proposalId.toString(), contractAddress),
|
||||
]);
|
||||
},
|
||||
},
|
||||
|
||||
+1
-1
@@ -2,13 +2,13 @@
|
||||
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { thirdwebClient } from "@/constants/client";
|
||||
import { THIRDWEB_ENGINE_FAUCET_WALLET } from "@/constants/env";
|
||||
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { CanClaimResponseType } from "app/api/testnet-faucet/can-claim/CanClaimResponseType";
|
||||
import { mapV4ChainToV5Chain } from "contexts/map-chains";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { thirdwebClient } from "lib/thirdweb-client";
|
||||
import { toast } from "sonner";
|
||||
import { toUnits } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
|
||||
+2
-2
@@ -3,9 +3,9 @@
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { thirdwebClient } from "@/constants/client";
|
||||
import { isProd } from "@/constants/env";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { isProd } from "constants/rpc";
|
||||
import { thirdwebClient } from "lib/thirdweb-client";
|
||||
import { CircleCheck, XIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { hostnameEndsWith } from "utils/url";
|
||||
|
||||
+1
-14
@@ -1,10 +1,6 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import {
|
||||
DASHBOARD_THIRDWEB_SECRET_KEY,
|
||||
IPFS_GATEWAY_URL,
|
||||
} from "@/constants/env";
|
||||
import { thirdwebClient } from "@/constants/client";
|
||||
import { ImageResponse } from "next/og";
|
||||
import { createThirdwebClient } from "thirdweb";
|
||||
import { download } from "thirdweb/storage";
|
||||
import { fetchChain } from "utils/fetchChain";
|
||||
|
||||
@@ -66,15 +62,6 @@ const TWLogo: React.FC = () => (
|
||||
export default async function Image({
|
||||
params,
|
||||
}: { params: { chain_id: string } }) {
|
||||
const thirdwebClient = createThirdwebClient({
|
||||
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
|
||||
config: {
|
||||
storage: {
|
||||
gatewayUrl: IPFS_GATEWAY_URL,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const chain = await fetchChain(params.chain_id);
|
||||
if (!chain) {
|
||||
return new Response("Chain not found", { status: 400 });
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { IPFS_GATEWAY_URL } from "@/constants/env";
|
||||
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "constants/rpc";
|
||||
import { ZERO_ADDRESS, createThirdwebClient, toTokens } from "thirdweb";
|
||||
import { thirdwebClient } from "@/constants/client";
|
||||
import { ZERO_ADDRESS, toTokens } from "thirdweb";
|
||||
import {
|
||||
eth_getBlockByHash,
|
||||
eth_getTransactionByHash,
|
||||
@@ -16,14 +15,6 @@ import { getChain } from "../../../utils";
|
||||
export default async function Page(props: {
|
||||
params: { chain_id: string; txHash: `0x${string}` };
|
||||
}) {
|
||||
const thirdwebClient = createThirdwebClient({
|
||||
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
|
||||
config: {
|
||||
storage: {
|
||||
gatewayUrl: IPFS_GATEWAY_URL,
|
||||
},
|
||||
},
|
||||
});
|
||||
const chain = await getChain(props.params.chain_id);
|
||||
|
||||
const rpcRequest = getRpcClient({
|
||||
|
||||
@@ -10,8 +10,9 @@ import {
|
||||
} from "@/components/ui/popover";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { ChevronDownIcon, Filter, XIcon } from "lucide-react";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
import type React from "react";
|
||||
import { type PropsWithChildren, useCallback, useMemo } from "react";
|
||||
import { products } from "../../../components/server/products";
|
||||
@@ -73,7 +74,7 @@ type FilterResetButtonProps = {
|
||||
const FilterResetButton: React.FC<
|
||||
PropsWithChildren<FilterResetButtonProps>
|
||||
> = ({ filters, children }) => {
|
||||
const router = useRouter();
|
||||
const router = useDashboardRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const mutableSearchParams = useMemo(() => {
|
||||
@@ -124,7 +125,7 @@ type ChainTypeFilterProps = { sectionOnly?: boolean };
|
||||
export const ChainTypeFilter: React.FC<ChainTypeFilterProps> = ({
|
||||
sectionOnly,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const router = useDashboardRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
@@ -228,7 +229,7 @@ type ChainOptionsFilterProps = { sectionOnly?: boolean };
|
||||
export const ChainOptionsFilter: React.FC<ChainOptionsFilterProps> = ({
|
||||
sectionOnly,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const router = useDashboardRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const mutableSearchParams = useMemo(() => {
|
||||
@@ -332,7 +333,7 @@ type ChainServiceFilterProps = { sectionOnly?: boolean };
|
||||
export const ChainServiceFilter: React.FC<ChainServiceFilterProps> = ({
|
||||
sectionOnly,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const router = useDashboardRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const mutableSearchParams = useMemo(() => {
|
||||
|
||||
+3
-2
@@ -1,7 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { PaginationButtons } from "@/components/pagination-buttons";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
import { useCallback } from "react";
|
||||
|
||||
type ChainlistPaginationProps = {
|
||||
@@ -15,7 +16,7 @@ export const ChainlistPagination: React.FC<ChainlistPaginationProps> = ({
|
||||
}) => {
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const router = useDashboardRouter();
|
||||
|
||||
const createPageURL = useCallback(
|
||||
(pageNumber: number) => {
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { SearchIcon, XCircleIcon } from "lucide-react";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
import { useRef } from "react";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
|
||||
@@ -15,7 +16,7 @@ function cleanUrl(url: string) {
|
||||
}
|
||||
|
||||
export const SearchInput: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const router = useDashboardRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { Grid2X2Icon, ListIcon } from "lucide-react";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
import { useCallback } from "react";
|
||||
|
||||
type hainListViewProps = {
|
||||
@@ -12,7 +13,7 @@ type hainListViewProps = {
|
||||
export const ChainListView: React.FC<hainListViewProps> = ({ activeView }) => {
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const router = useDashboardRouter();
|
||||
|
||||
const createPageURL = useCallback(
|
||||
(view: "grid" | "table") => {
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import "server-only";
|
||||
|
||||
import {
|
||||
DASHBOARD_THIRDWEB_SECRET_KEY,
|
||||
IPFS_GATEWAY_URL,
|
||||
} from "@/constants/env";
|
||||
import { thirdwebClient } from "@/constants/client";
|
||||
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/env";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { createThirdwebClient } from "thirdweb";
|
||||
import { resolveScheme } from "thirdweb/storage";
|
||||
|
||||
const fallbackChainIcon =
|
||||
@@ -17,16 +14,8 @@ export async function ChainIcon(props: {
|
||||
className?: string;
|
||||
}) {
|
||||
if (props.iconUrl) {
|
||||
const client = createThirdwebClient({
|
||||
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
|
||||
config: {
|
||||
storage: {
|
||||
gatewayUrl: IPFS_GATEWAY_URL,
|
||||
},
|
||||
},
|
||||
});
|
||||
const resolved = resolveScheme({
|
||||
client,
|
||||
client: thirdwebClient,
|
||||
uri: props.iconUrl,
|
||||
});
|
||||
const res = await fetch(resolved, {
|
||||
|
||||
+3
@@ -51,3 +51,6 @@ export default async function Layout({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// because cookies() is used
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
+2
-2
@@ -15,11 +15,11 @@ import { ImageUpload } from "@/components/ui/image-upload";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { RadioGroup, RadioGroupItemButton } from "@/components/ui/radio-group";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { AccountStatus, useAccount } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -49,7 +49,7 @@ export function CreateEcosystemForm() {
|
||||
>();
|
||||
const { data: billingAccountInfo } = useAccount();
|
||||
|
||||
const router = useRouter();
|
||||
const router = useDashboardRouter();
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
|
||||
@@ -113,3 +113,6 @@ export default async function Page() {
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
// because cookies() is used
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -79,3 +79,6 @@ export default async function DashboardConnectPayPage() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// because cookies() is used
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
+1
-1
@@ -9,8 +9,8 @@ import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { thirdwebClient } from "@/constants/client";
|
||||
import type { Abi, AbiFunction } from "abitype";
|
||||
import { thirdwebClient } from "lib/thirdweb-client";
|
||||
import { useV5DashboardChain } from "lib/v5-adapter";
|
||||
import { ArrowDown } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
@@ -6,10 +6,11 @@ import {
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
} from "@/components/ui/pagination";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
|
||||
export function TablePagination() {
|
||||
const router = useRouter();
|
||||
const router = useDashboardRouter();
|
||||
const path = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const page = Number(searchParams?.get("page") || 1);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"use client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { usePathname } from "next/navigation";
|
||||
import type { SortBy, TimeRange } from "../../../../lib/search";
|
||||
|
||||
export type SortingHeaderProps = {
|
||||
@@ -13,7 +14,7 @@ export type SortingHeaderProps = {
|
||||
};
|
||||
|
||||
export function SortingHeader(props: SortingHeaderProps) {
|
||||
const router = useRouter();
|
||||
const router = useDashboardRouter();
|
||||
const path = usePathname();
|
||||
const enableSorting = !!props.searchParams;
|
||||
const { timeRange, page, sortBy } = props.searchParams || {};
|
||||
|
||||
+3
-2
@@ -1,10 +1,11 @@
|
||||
"use client";
|
||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import type { TimeRange } from "lib/search";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
|
||||
export function TimeRangeSwitcher() {
|
||||
const router = useRouter();
|
||||
const router = useDashboardRouter();
|
||||
const path = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const currentSort = searchParams?.get("sortBy") || "transactionCount";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import "@/styles/globals.css";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { DashboardRouterTopProgressBar } from "@/lib/DashboardRouter";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Metadata } from "next";
|
||||
import PlausibleProvider from "next-plausible";
|
||||
@@ -70,6 +71,7 @@ export default function RootLayout({
|
||||
<PostHogPageView />
|
||||
<AppRouterProviders>{children}</AppRouterProviders>
|
||||
<Toaster richColors />
|
||||
<DashboardRouterTopProgressBar />
|
||||
<NextTopLoader
|
||||
color="hsl(var(--primary))"
|
||||
height={2}
|
||||
|
||||
@@ -129,13 +129,11 @@ export const BarChart = <
|
||||
tickFormatter={(payload) =>
|
||||
index.format
|
||||
? index.format(payload)
|
||||
: index.type === "date"
|
||||
? new Date(payload).toLocaleDateString(undefined, {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "2-digit",
|
||||
})
|
||||
: payload
|
||||
: new Date(payload).toLocaleDateString(undefined, {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "2-digit",
|
||||
})
|
||||
}
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isProd } from "@/constants/env";
|
||||
import {
|
||||
type EVMContractInfo,
|
||||
EVMContractInfoProvider,
|
||||
@@ -20,7 +21,6 @@ import { AppShell, type AppShellProps } from "components/layout/app-shell";
|
||||
import { Onboarding as OnboardingModal } from "components/onboarding";
|
||||
import { OpCreditsGrantedModalWrapper } from "components/onboarding/OpCreditsGrantedModalWrapper";
|
||||
import { PosthogIdentifier } from "components/wallets/PosthogIdentifier";
|
||||
import { isProd } from "constants/rpc";
|
||||
import { AllChainsProvider } from "contexts/all-chains";
|
||||
import { ChainsProvider } from "contexts/configured-chains";
|
||||
import { ErrorProvider } from "contexts/error-handler";
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import { thirdwebClient } from "@/constants/client";
|
||||
import {
|
||||
DASHBOARD_THIRDWEB_CLIENT_ID,
|
||||
DASHBOARD_THIRDWEB_SECRET_KEY,
|
||||
} from "@/constants/env";
|
||||
import {
|
||||
type EVMContractInfo,
|
||||
useEVMContractInfo,
|
||||
} from "@3rdweb-sdk/react/hooks/useActiveChainId";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { ThirdwebSDKProvider } from "@thirdweb-dev/react";
|
||||
import {
|
||||
DASHBOARD_THIRDWEB_CLIENT_ID,
|
||||
DASHBOARD_THIRDWEB_SECRET_KEY,
|
||||
} from "constants/rpc";
|
||||
import type { Signer } from "ethers";
|
||||
import { useSupportedChains } from "hooks/chains/configureChains";
|
||||
import { useNativeColorMode } from "hooks/useNativeColorMode";
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { thirdwebClient } from "@/constants/client";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useDashboardEVMChainId } from "@3rdweb-sdk/react";
|
||||
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
|
||||
import {
|
||||
Box,
|
||||
@@ -25,7 +26,6 @@ import {
|
||||
} from "@chakra-ui/react";
|
||||
import { AiOutlineWarning } from "@react-icons/all-files/ai/AiOutlineWarning";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useSDK, useSDKChainId } from "@thirdweb-dev/react";
|
||||
import { FaucetButton } from "app/(dashboard)/(chain)/[chain_id]/(chainPage)/components/client/FaucetButton";
|
||||
import { GiftIcon } from "app/(dashboard)/(chain)/[chain_id]/(chainPage)/components/icons/GiftIcon";
|
||||
import type {
|
||||
@@ -33,6 +33,7 @@ import type {
|
||||
ChainServices,
|
||||
} from "app/(dashboard)/(chain)/types/chain";
|
||||
import { getSDKTheme } from "app/components/sdk-component-theme";
|
||||
import { LOCAL_NODE_PKEY } from "constants/misc";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useSupportedChain } from "hooks/chains/configureChains";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
@@ -41,6 +42,7 @@ import Link from "next/link";
|
||||
import { forwardRef, useCallback, useMemo, useRef, useState } from "react";
|
||||
import { VscDebugDisconnect } from "react-icons/vsc";
|
||||
import { toast } from "sonner";
|
||||
import { prepareTransaction, sendTransaction, toWei } from "thirdweb";
|
||||
import { type Chain, type ChainMetadata, localhost } from "thirdweb/chains";
|
||||
import {
|
||||
PayEmbed,
|
||||
@@ -51,6 +53,7 @@ import {
|
||||
useSwitchActiveWalletChain,
|
||||
useWalletBalance,
|
||||
} from "thirdweb/react";
|
||||
import { privateKeyToAccount } from "thirdweb/wallets";
|
||||
import { Button, type ButtonProps, Card, Heading, Text } from "tw-components";
|
||||
import { THIRDWEB_API_HOST } from "../../constants/urls";
|
||||
import { useV5DashboardChain } from "../../lib/v5-adapter";
|
||||
@@ -62,7 +65,7 @@ const GAS_FREE_CHAINS = [
|
||||
|
||||
function useNetworkMismatchAdapter() {
|
||||
const walletChainId = useActiveWalletChain()?.id;
|
||||
const v4SDKChainId = useSDKChainId();
|
||||
const v4SDKChainId = useDashboardEVMChainId();
|
||||
if (!walletChainId || !v4SDKChainId) {
|
||||
// simply not ready yet, assume false
|
||||
return false;
|
||||
@@ -397,7 +400,7 @@ const MismatchNotice: React.FC<{
|
||||
onClose: (hasSwitched: boolean) => void;
|
||||
}> = ({ initialFocusRef, onClose }) => {
|
||||
const connectedChainId = useActiveWalletChain()?.id;
|
||||
const desiredChainId = useSDKChainId();
|
||||
const desiredChainId = useDashboardEVMChainId();
|
||||
const switchNetwork = useSwitchActiveWalletChain();
|
||||
const connectionStatus = useActiveWalletConnectionStatus();
|
||||
const activeWallet = useActiveWallet();
|
||||
@@ -485,16 +488,30 @@ const MismatchNotice: React.FC<{
|
||||
};
|
||||
|
||||
const GetLocalHostTestnetFunds: React.FC = () => {
|
||||
const sdk = useSDK();
|
||||
const address = useActiveAccount()?.address;
|
||||
const requestFunds = async () => {
|
||||
if (sdk) {
|
||||
try {
|
||||
await sdk.wallet.requestFunds(10);
|
||||
toast.success("Successfully got funds from localhost");
|
||||
} catch (e) {
|
||||
toast.error("Failed to get funds from localhost");
|
||||
}
|
||||
if (!address) {
|
||||
return toast.error("No active account detected");
|
||||
}
|
||||
const faucet = privateKeyToAccount({
|
||||
privateKey: LOCAL_NODE_PKEY,
|
||||
client: thirdwebClient,
|
||||
});
|
||||
const transaction = prepareTransaction({
|
||||
to: address,
|
||||
chain: localhost,
|
||||
client: thirdwebClient,
|
||||
value: toWei("10"),
|
||||
});
|
||||
const promise = sendTransaction({
|
||||
account: faucet,
|
||||
transaction,
|
||||
});
|
||||
toast.promise(promise, {
|
||||
error: "Failed to get funds from localhost",
|
||||
success: "Successfully got funds from localhost",
|
||||
loading: "Requesting funds",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Alert,
|
||||
AlertIcon,
|
||||
Flex,
|
||||
FormControl,
|
||||
Input,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
} from "@chakra-ui/react";
|
||||
import { ChainIcon } from "components/icons/ChainIcon";
|
||||
import type { StoredChain } from "contexts/configured-chains";
|
||||
@@ -16,7 +14,7 @@ import { useSupportedChainsNameRecord } from "hooks/chains/configureChains";
|
||||
import { useRemoveChainModification } from "hooks/chains/useModifyChain";
|
||||
import { getDashboardChainRpc } from "lib/rpc";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Button, FormErrorMessage, FormLabel, Text } from "tw-components";
|
||||
import { FormErrorMessage, FormLabel } from "tw-components";
|
||||
import { ChainIdInput } from "./Form/ChainIdInput";
|
||||
import { ConfirmationPopover } from "./Form/ConfirmationPopover";
|
||||
import { IconUpload } from "./Form/IconUpload";
|
||||
@@ -217,11 +215,7 @@ export const ConfigureNetworkForm: React.FC<NetworkConfigFormProps> = ({
|
||||
|
||||
const submitBtn = (
|
||||
<Button
|
||||
background="bgBlack"
|
||||
color="bgWhite"
|
||||
_hover={{
|
||||
background: "bgBlack",
|
||||
}}
|
||||
variant="primary"
|
||||
type={disableSubmit ? "submit" : overwritingChain ? "button" : "submit"}
|
||||
disabled={disableSubmit}
|
||||
>
|
||||
@@ -243,9 +237,6 @@ export const ConfigureNetworkForm: React.FC<NetworkConfigFormProps> = ({
|
||||
<Input
|
||||
autoComplete="off"
|
||||
placeholder="e.g. My Network"
|
||||
_placeholder={{
|
||||
fontWeight: 500,
|
||||
}}
|
||||
type="text"
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
@@ -272,9 +263,9 @@ export const ConfigureNetworkForm: React.FC<NetworkConfigFormProps> = ({
|
||||
{/* Slug URL */}
|
||||
<NetworkIDInput form={form} disabled={!isFullyEditable} />
|
||||
|
||||
<Flex direction="column" gap={6} mt={6}>
|
||||
<div className="flex flex-col gap-6 mt-6">
|
||||
{/* Chain ID + Currency Symbol */}
|
||||
<SimpleGrid columns={{ md: 2, base: 1 }} gap={4}>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<ChainIdInput form={form} disabled={!isFullyEditable} />
|
||||
|
||||
{/* Currency Symbol */}
|
||||
@@ -284,33 +275,29 @@ export const ConfigureNetworkForm: React.FC<NetworkConfigFormProps> = ({
|
||||
disabled={!isFullyEditable}
|
||||
placeholder="e.g. ETH"
|
||||
autoComplete="off"
|
||||
_placeholder={{
|
||||
fontWeight: 500,
|
||||
}}
|
||||
type="text"
|
||||
{...form.register("currencySymbol", { required: true })}
|
||||
/>
|
||||
</FormControl>
|
||||
</SimpleGrid>
|
||||
</div>
|
||||
|
||||
<SimpleGrid columns={{ md: 2, base: 1 }} gap={4}>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* Testnet / Mainnet */}
|
||||
<FormControl>
|
||||
<FormLabel display="flex">
|
||||
<FormLabel className="!flex gap-1 items-center">
|
||||
Network type
|
||||
<TooltipBox
|
||||
content={
|
||||
<>
|
||||
<Text color="heading" display="inline-block" mb={2}>
|
||||
{" "}
|
||||
<span className="mb-2 block">
|
||||
The network type indicates whether it is intended for
|
||||
production or testing.
|
||||
</Text>
|
||||
</span>
|
||||
|
||||
<Text color="heading">
|
||||
<span className="block">
|
||||
It{`'`}s only used for displaying network type on the
|
||||
dashboard and does not affect functionality.
|
||||
</Text>
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
@@ -324,10 +311,10 @@ export const ConfigureNetworkForm: React.FC<NetworkConfigFormProps> = ({
|
||||
}}
|
||||
value={form.watch("type")}
|
||||
>
|
||||
<Stack direction="row" gap={4} mt={3}>
|
||||
<div className="flex gap-4 mt-3">
|
||||
<Radio value="mainnet">Mainnet</Radio>
|
||||
<Radio value="testnet">Testnet</Radio>
|
||||
</Stack>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
|
||||
@@ -335,19 +322,19 @@ export const ConfigureNetworkForm: React.FC<NetworkConfigFormProps> = ({
|
||||
<FormControl isInvalid={!!form.formState.errors.icon}>
|
||||
<FormLabel>Icon</FormLabel>
|
||||
|
||||
<Flex gap={3} alignItems="center">
|
||||
<div className="flex gap-1 items-center">
|
||||
<ChainIcon size={24} ipfsSrc={form.watch("icon")} />
|
||||
<IconUpload
|
||||
onUpload={(uri) => {
|
||||
form.setValue("icon", uri, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</div>
|
||||
</FormControl>
|
||||
</SimpleGrid>
|
||||
</div>
|
||||
|
||||
{editingChain?.status === "deprecated" && (
|
||||
<SimpleGrid columns={{ md: 2, base: 1 }} gap={4}>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* Active / Deprecated */}
|
||||
<FormControl>
|
||||
<FormLabel display="flex">Network status</FormLabel>
|
||||
@@ -360,13 +347,13 @@ export const ConfigureNetworkForm: React.FC<NetworkConfigFormProps> = ({
|
||||
}}
|
||||
value={form.watch("status")}
|
||||
>
|
||||
<Stack direction="row" gap={4} mt={3}>
|
||||
<div className="flex mt-3 gap-4">
|
||||
<Radio value="active">Live</Radio>
|
||||
<Radio value="deprecated">Deprecated</Radio>
|
||||
</Stack>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</SimpleGrid>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* RPC URL */}
|
||||
@@ -382,11 +369,7 @@ export const ConfigureNetworkForm: React.FC<NetworkConfigFormProps> = ({
|
||||
)}
|
||||
|
||||
{/* Buttons */}
|
||||
<Flex
|
||||
gap={4}
|
||||
direction={{ base: "column", md: "row" }}
|
||||
justifyContent={{ base: "center", md: "flex-end" }}
|
||||
>
|
||||
<div className="flex gap-4 flex-col md:flex-row justify-end">
|
||||
{/* Add / Update Button */}
|
||||
{overwritingChain && !disableSubmit ? (
|
||||
<ConfirmationPopover
|
||||
@@ -417,8 +400,8 @@ export const ConfigureNetworkForm: React.FC<NetworkConfigFormProps> = ({
|
||||
Reset
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalOverlay,
|
||||
} from "@chakra-ui/react";
|
||||
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
||||
import type { StoredChain } from "contexts/configured-chains";
|
||||
import { useSetEditChain } from "hooks/networkConfigModal";
|
||||
import { ConfigureNetworks } from "./ConfigureNetworks";
|
||||
@@ -24,31 +18,26 @@ export const ConfigureNetworkModal: React.FC<AddNetworkModalProps> = (
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={true} onClose={onModalClose} isCentered>
|
||||
<ModalOverlay />
|
||||
<ModalContent
|
||||
borderRadius="xl"
|
||||
overflow="hidden"
|
||||
w="500px"
|
||||
mt={{ base: 8, md: 16 }}
|
||||
mb={{ base: 0, md: 16 }}
|
||||
maxW={{ base: "100vw", md: "calc(100vw - 40px)" }}
|
||||
<Dialog
|
||||
open={true}
|
||||
onOpenChange={(v) => {
|
||||
if (!v) {
|
||||
onModalClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent
|
||||
className="p-0 z-[10001] max-w-[480px]"
|
||||
dialogOverlayClassName="z-[10000]"
|
||||
>
|
||||
<ModalCloseButton />
|
||||
<ModalBody
|
||||
p={0}
|
||||
_dark={{ background: "backgroundBody" }}
|
||||
_light={{ background: "white" }}
|
||||
>
|
||||
<ConfigureNetworks
|
||||
onNetworkAdded={(chain) => {
|
||||
props.onNetworkAdded?.(chain);
|
||||
onModalClose();
|
||||
}}
|
||||
onNetworkConfigured={onModalClose}
|
||||
/>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<ConfigureNetworks
|
||||
onNetworkAdded={(chain) => {
|
||||
props.onNetworkAdded?.(chain);
|
||||
onModalClose();
|
||||
}}
|
||||
onNetworkConfigured={onModalClose}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { Box, useToast } from "@chakra-ui/react";
|
||||
import type { StoredChain } from "contexts/configured-chains";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useAddRecentlyUsedChainId } from "hooks/chains/recentlyUsedChains";
|
||||
import { useModifyChain } from "hooks/chains/useModifyChain";
|
||||
import { useEditChain } from "hooks/networkConfigModal";
|
||||
import { Heading } from "tw-components";
|
||||
import { toast } from "sonner";
|
||||
import { ConfigureNetworkForm } from "./ConfigureNetworkForm";
|
||||
|
||||
function useChainConfigTrack() {
|
||||
@@ -31,17 +30,6 @@ export const ConfigureNetworks: React.FC<ConfigureNetworksProps> = (props) => {
|
||||
const modifyChain = useModifyChain();
|
||||
const editChain = useEditChain();
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
const successToast = (message: string) => {
|
||||
toast({
|
||||
title: message,
|
||||
status: "success",
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = (chain: StoredChain) => {
|
||||
modifyChain(chain);
|
||||
addRecentlyUsedChainId(chain.chainId);
|
||||
@@ -52,44 +40,29 @@ export const ConfigureNetworks: React.FC<ConfigureNetworksProps> = (props) => {
|
||||
}
|
||||
|
||||
trackChainConfig("add", chain);
|
||||
successToast("Network Added Successfully");
|
||||
toast.success("Network Added Successfully");
|
||||
} else {
|
||||
if (props.onNetworkConfigured) {
|
||||
props.onNetworkConfigured(chain);
|
||||
}
|
||||
|
||||
trackChainConfig("update", chain);
|
||||
successToast("Network Updated Successfully");
|
||||
toast.success("Network Updated Successfully");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
p={{ base: 4, md: 8 }}
|
||||
minH="600px"
|
||||
borderTop={{ md: "none", base: "1px solid" }}
|
||||
marginTop={{ base: 6, md: 0 }}
|
||||
borderColor="inputBg"
|
||||
>
|
||||
<Heading
|
||||
as={"h3"}
|
||||
size="label.xl"
|
||||
mb={8}
|
||||
display="flex"
|
||||
gap={2}
|
||||
alignItems="center"
|
||||
>
|
||||
<div className="p-6">
|
||||
<h3 className="text-2xl font-semibold tracking-tight mb-5">
|
||||
{editChain ? "Edit Network" : "Add Custom Network"}
|
||||
</Heading>
|
||||
</h3>
|
||||
|
||||
{/* Modify the given chain */}
|
||||
{editChain && (
|
||||
<Box mt={9}>
|
||||
<ConfigureNetworkForm
|
||||
editingChain={editChain}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
</Box>
|
||||
<ConfigureNetworkForm
|
||||
editingChain={editChain}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Custom chain */}
|
||||
@@ -100,6 +73,6 @@ export const ConfigureNetworks: React.FC<ConfigureNetworksProps> = (props) => {
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { FormControl, Input } from "@chakra-ui/react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { FormControl } from "@chakra-ui/react";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import { FormLabel } from "tw-components";
|
||||
import type { NetworkConfigFormData } from "../ConfigureNetworkForm";
|
||||
|
||||
export const ChainIdInput: React.FC<{
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
form: UseFormReturn<NetworkConfigFormData, any>;
|
||||
form: UseFormReturn<NetworkConfigFormData>;
|
||||
disabled: boolean;
|
||||
}> = ({ form, disabled }) => {
|
||||
return (
|
||||
@@ -18,9 +18,6 @@ export const ChainIdInput: React.FC<{
|
||||
disabled={disabled}
|
||||
placeholder="e.g. 152"
|
||||
autoComplete="off"
|
||||
_placeholder={{
|
||||
fontWeight: 500,
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
// prevent typing e, +, -
|
||||
if (e.key === "e" || e.key === "+" || e.key === "-") {
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useDashboardStorageUpload } from "@3rdweb-sdk/react/hooks/useDashboardStorageUpload";
|
||||
import { Icon } from "@chakra-ui/react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { FileInput } from "components/shared/FileInput";
|
||||
import { PINNED_FILES_QUERY_KEY_ROOT } from "components/storage/your-files";
|
||||
import { useErrorHandler } from "contexts/error-handler";
|
||||
import { FiUpload } from "react-icons/fi";
|
||||
import { Button } from "tw-components";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export const IconUpload: React.FC<{ onUpload: (url: string) => void }> = ({
|
||||
onUpload,
|
||||
}) => {
|
||||
const errorHandler = useErrorHandler();
|
||||
const storageUpload = useDashboardStorageUpload();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const handleIconUpload = (file: File) => {
|
||||
// if file size is larger than 5000kB, show error
|
||||
if (file.size > 5000 * 1024) {
|
||||
errorHandler.onError("Icon image can not be larger than 5MB");
|
||||
toast.error("Icon image can not be larger than 5MB");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -28,21 +27,20 @@ export const IconUpload: React.FC<{ onUpload: (url: string) => void }> = ({
|
||||
queryClient.invalidateQueries([PINNED_FILES_QUERY_KEY_ROOT]);
|
||||
},
|
||||
onError(error) {
|
||||
errorHandler.onError(error, "Failed to upload file");
|
||||
console.error(error);
|
||||
toast.error("Failed to upload file");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<FileInput setValue={handleIconUpload} accept={{ "image/*": [] }}>
|
||||
<Button
|
||||
bg="inputBg"
|
||||
size="sm"
|
||||
variant="solid"
|
||||
aria-label="Upload to IPFS"
|
||||
rightIcon={<Icon as={FiUpload} />}
|
||||
isLoading={storageUpload.isLoading}
|
||||
>
|
||||
<Button variant="ghost" className="gap-2 px-2" size="sm">
|
||||
{storageUpload.isLoading ? (
|
||||
<Spinner className="size-4" />
|
||||
) : (
|
||||
<FiUpload className="size-4" />
|
||||
)}
|
||||
Upload Icon
|
||||
</Button>
|
||||
</FileInput>
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
import { FormControl, Input } from "@chakra-ui/react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { FormControl } from "@chakra-ui/react";
|
||||
import { useAllChainsData } from "hooks/chains/allChains";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import {
|
||||
CodeBlock,
|
||||
FormErrorMessage,
|
||||
FormLabel,
|
||||
Heading,
|
||||
Text,
|
||||
} from "tw-components";
|
||||
import { FormErrorMessage, FormLabel } from "tw-components";
|
||||
import type { NetworkConfigFormData } from "../ConfigureNetworkForm";
|
||||
import { TooltipBox } from "./TooltipBox";
|
||||
|
||||
export const NetworkIDInput: React.FC<{
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
form: UseFormReturn<NetworkConfigFormData, any>;
|
||||
form: UseFormReturn<NetworkConfigFormData>;
|
||||
disabled?: boolean;
|
||||
}> = ({ form, disabled }) => {
|
||||
const slug = form.watch("slug");
|
||||
@@ -25,23 +19,16 @@ export const NetworkIDInput: React.FC<{
|
||||
mt={6}
|
||||
isInvalid={form.formState.errors.slug?.type === "taken"}
|
||||
>
|
||||
<FormLabel display="flex">
|
||||
<FormLabel className="!flex items-center gap-1">
|
||||
Network ID
|
||||
<TooltipBox
|
||||
content={
|
||||
<>
|
||||
<Text color="heading" mb={4}>
|
||||
Network ID is used to identify the network in the URL{" "}
|
||||
</Text>
|
||||
<Heading fontSize="14px" mb={3}>
|
||||
Example
|
||||
</Heading>
|
||||
<CodeBlock
|
||||
fontSize="14px"
|
||||
code="thirdweb.com/<network-id>/<contract-address>"
|
||||
language="bash"
|
||||
canCopy={false}
|
||||
/>
|
||||
Network ID is used to identify the network in the URL
|
||||
<p className="font-semibold mt-2 mb-1">Example</p>
|
||||
<p className="text-link-foreground text-xs">
|
||||
{"thirdweb.com/<network-id>/<contract-address>"}
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
@@ -50,9 +37,6 @@ export const NetworkIDInput: React.FC<{
|
||||
disabled={disabled}
|
||||
autoComplete="off"
|
||||
placeholder="e.g. ethereum"
|
||||
_placeholder={{
|
||||
fontWeight: 500,
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
// only allow alphanumeric characters and dashes
|
||||
if (!/^[a-z0-9-]*$/i.test(e.key)) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Alert, AlertIcon, FormControl, Input } from "@chakra-ui/react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Alert, AlertIcon, FormControl } from "@chakra-ui/react";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import { IoWarning } from "react-icons/io5";
|
||||
import { FormErrorMessage, FormLabel } from "tw-components";
|
||||
@@ -28,15 +29,7 @@ export const RpcInput: React.FC<{
|
||||
isDisabled={form.watch("status") !== "active"}
|
||||
>
|
||||
<FormLabel>RPC URL</FormLabel>
|
||||
<Input
|
||||
autoComplete="off"
|
||||
placeholder="https://"
|
||||
type="url"
|
||||
_placeholder={{
|
||||
fontWeight: 500,
|
||||
}}
|
||||
{...reg}
|
||||
/>
|
||||
<Input autoComplete="off" placeholder="https://" type="url" {...reg} />
|
||||
|
||||
<FormErrorMessage fontSize="12px">Invalid RPC URL</FormErrorMessage>
|
||||
|
||||
|
||||
@@ -1,34 +1,12 @@
|
||||
import { Flex, Icon, Tooltip } from "@chakra-ui/react";
|
||||
import { BsQuestionCircle } from "react-icons/bs";
|
||||
import { Card } from "tw-components";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { CircleHelpIcon } from "lucide-react";
|
||||
|
||||
export const TooltipBox: React.FC<{
|
||||
content: React.ReactNode;
|
||||
iconColor?: string;
|
||||
}> = ({ content, iconColor = "accent.600" }) => {
|
||||
}> = ({ content }) => {
|
||||
return (
|
||||
<Tooltip
|
||||
placement="top-start"
|
||||
borderRadius="md"
|
||||
bg="transparent"
|
||||
boxShadow="none"
|
||||
p={4}
|
||||
minW={{ md: "450px" }}
|
||||
label={
|
||||
<Card py={2} px={4} bgColor="backgroundCardHighlight">
|
||||
{content}
|
||||
</Card>
|
||||
}
|
||||
>
|
||||
<Flex alignItems="center" justifyContent="center">
|
||||
<Icon
|
||||
ml={2}
|
||||
mr={1}
|
||||
as={BsQuestionCircle}
|
||||
color={iconColor}
|
||||
boxSize={4}
|
||||
/>
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
<ToolTipLabel label={<div>{content}</div>}>
|
||||
<CircleHelpIcon className="size-4 text-muted-foreground" />
|
||||
</ToolTipLabel>
|
||||
);
|
||||
};
|
||||
|
||||
+14
-10
@@ -21,7 +21,7 @@ import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useSupportedChain } from "hooks/chains/configureChains";
|
||||
import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
import { replaceTemplateValues } from "lib/deployment/template-values";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import { ExternalLinkIcon, InfoIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useMemo } from "react";
|
||||
@@ -349,15 +349,6 @@ const CustomContractForm: React.FC<CustomContractFormProps> = ({
|
||||
(isErc721SharedMetadadata ? 1 : 0);
|
||||
|
||||
const isCreate2Deployment = form.watch("deployDeterministic");
|
||||
|
||||
if (fullPublishMetadata.isLoading) {
|
||||
return (
|
||||
<div className="min-h-[200px] flex items-center justify-center">
|
||||
<Spinner className="size-10" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const advancedParams = Object.keys(formDeployParams)
|
||||
.map((paramKey) => {
|
||||
const deployParam = deployParams.find((p) => p.name === paramKey);
|
||||
@@ -382,6 +373,19 @@ const CustomContractForm: React.FC<CustomContractFormProps> = ({
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
{fullPublishMetadata.isLoading && (
|
||||
<div className="min-h-[200px] flex items-center justify-center">
|
||||
<Spinner className="size-10" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{fullPublishMetadata.isError && (
|
||||
<div className="bg-destructive p-4 border border-destructive-text/30 rounded-lg text-destructive-text flex gap-2 items-center">
|
||||
<InfoIcon className="size-4" />
|
||||
Failed to fetch Publish metadata
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Flex
|
||||
flexGrow={1}
|
||||
minH="full"
|
||||
|
||||
@@ -171,7 +171,7 @@ export const PublishedContractTable: ComponentWithChildren<
|
||||
data: rows,
|
||||
});
|
||||
return (
|
||||
<TableContainer>
|
||||
<TableContainer className="relative">
|
||||
{isFetching && (
|
||||
<Spinner
|
||||
color="primary"
|
||||
|
||||
@@ -56,7 +56,7 @@ export const DeployableContractTable: ComponentWithChildren<
|
||||
data: contractIds.map((contractId) => ({ contractId })),
|
||||
});
|
||||
return (
|
||||
<TableContainer>
|
||||
<TableContainer className="relative">
|
||||
{isFetching && (
|
||||
<Spinner
|
||||
color="primary"
|
||||
|
||||
@@ -750,6 +750,20 @@ export function useCustomContractDeployMutation(options: {
|
||||
message,
|
||||
);
|
||||
}
|
||||
case "eth_sendTransaction": {
|
||||
const tx = params?.[0];
|
||||
if (!tx) {
|
||||
throw new Error("No transaction provided");
|
||||
}
|
||||
return signer.sendTransaction(tx);
|
||||
}
|
||||
case "personal_sign": {
|
||||
const data = params?.[0];
|
||||
if (!data) {
|
||||
throw new Error("Nothing to sign");
|
||||
}
|
||||
return signer.signMessage(data);
|
||||
}
|
||||
default: {
|
||||
return (signer as providers.JsonRpcSigner)?.provider?.send(
|
||||
method,
|
||||
|
||||
@@ -59,7 +59,7 @@ export interface ExtendedPublishedContract extends PublishedContractType {
|
||||
name: string;
|
||||
displayName?: string;
|
||||
description: string;
|
||||
version: string;
|
||||
version: string | undefined;
|
||||
publisher: string;
|
||||
tags?: string[];
|
||||
logo?: string;
|
||||
@@ -146,7 +146,7 @@ export const PublishedContract: React.FC<PublishedContractProps> = ({
|
||||
PublishedContractOG.toUrl({
|
||||
name: publishedContractName,
|
||||
description: contract.description,
|
||||
version: contract.version,
|
||||
version: contract.version || "latest",
|
||||
publisher: publisherEnsOrAddress,
|
||||
extension: extensionNames,
|
||||
license: licenses,
|
||||
|
||||
+62
-61
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import {
|
||||
Box,
|
||||
type BoxProps,
|
||||
@@ -35,8 +34,7 @@ export const MarkdownRenderer: React.FC<
|
||||
{...restProps}
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
h1: (props: any) => (
|
||||
h1: (props) => (
|
||||
<Heading
|
||||
as="h2"
|
||||
size="title.lg"
|
||||
@@ -44,11 +42,11 @@ export const MarkdownRenderer: React.FC<
|
||||
borderBottomColor="borderColor"
|
||||
{...commonHeadingProps}
|
||||
mb={4}
|
||||
{...props}
|
||||
{...cleanedProps(props)}
|
||||
/>
|
||||
),
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
h2: (props: any) => (
|
||||
|
||||
h2: (props) => (
|
||||
<Heading
|
||||
as="h3"
|
||||
size="title.md"
|
||||
@@ -57,51 +55,51 @@ export const MarkdownRenderer: React.FC<
|
||||
{...commonHeadingProps}
|
||||
mt={8}
|
||||
mb={4}
|
||||
{...props}
|
||||
{...cleanedProps(props)}
|
||||
/>
|
||||
),
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
h3: (props: any) => (
|
||||
|
||||
h3: (props) => (
|
||||
<Heading
|
||||
as="h4"
|
||||
size="title.sm"
|
||||
{...commonHeadingProps}
|
||||
{...props}
|
||||
{...cleanedProps(props)}
|
||||
mt={4}
|
||||
/>
|
||||
),
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
h4: (props: any) => (
|
||||
|
||||
h4: (props) => (
|
||||
<Heading
|
||||
as="h5"
|
||||
size="subtitle.md"
|
||||
{...commonHeadingProps}
|
||||
{...props}
|
||||
{...cleanedProps(props)}
|
||||
mt={4}
|
||||
/>
|
||||
),
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
h5: (props: any) => (
|
||||
|
||||
h5: (props) => (
|
||||
<Heading
|
||||
as="h6"
|
||||
size="subtitle.sm"
|
||||
{...commonHeadingProps}
|
||||
{...props}
|
||||
{...cleanedProps(props)}
|
||||
mt={4}
|
||||
/>
|
||||
),
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
h6: (props: any) => (
|
||||
|
||||
h6: (props) => (
|
||||
<Heading
|
||||
as="p"
|
||||
size="label.md"
|
||||
{...commonHeadingProps}
|
||||
{...props}
|
||||
{...cleanedProps(props)}
|
||||
mt={4}
|
||||
/>
|
||||
),
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
a: (props: any) => (
|
||||
|
||||
a: (props) => (
|
||||
<Link
|
||||
_dark={{
|
||||
color: "blue.400",
|
||||
@@ -116,13 +114,12 @@ export const MarkdownRenderer: React.FC<
|
||||
},
|
||||
}}
|
||||
isExternal
|
||||
{...props}
|
||||
{...cleanedProps(props)}
|
||||
mt={4}
|
||||
/>
|
||||
),
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
code: ({ inline, ...props }: any) => {
|
||||
code: ({ ...props }) => {
|
||||
if (props?.className) {
|
||||
const language = props.className.replace("language-", "");
|
||||
return (
|
||||
@@ -130,7 +127,7 @@ export const MarkdownRenderer: React.FC<
|
||||
code={onlyText(props.children).trim()}
|
||||
language={language}
|
||||
mb={4}
|
||||
{...props}
|
||||
{...cleanedProps(props)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -145,16 +142,21 @@ export const MarkdownRenderer: React.FC<
|
||||
_dark={{ bg: "whiteAlpha.100" }}
|
||||
borderRadius="md"
|
||||
fontFamily="mono"
|
||||
{...props}
|
||||
{...cleanedProps(props)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
p: (props: any) => (
|
||||
<Text size="body.md" mb={4} {...props} lineHeight={1.5} />
|
||||
|
||||
p: (props) => (
|
||||
<Text
|
||||
size="body.md"
|
||||
mb={4}
|
||||
{...cleanedProps(props)}
|
||||
lineHeight={1.5}
|
||||
/>
|
||||
),
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
table: (props: any) => (
|
||||
|
||||
table: (props) => (
|
||||
<Box
|
||||
maxW="100%"
|
||||
overflowX="auto"
|
||||
@@ -164,53 +166,44 @@ export const MarkdownRenderer: React.FC<
|
||||
mb={4}
|
||||
borderTopRadius="lg"
|
||||
>
|
||||
<Table {...props} />
|
||||
<Table {...cleanedProps(props)} />
|
||||
</Box>
|
||||
),
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
th: ({ children: c, ...props }: any) => (
|
||||
<Th
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
{...(props as unknown as any)}
|
||||
textAlign="left!important"
|
||||
border="none"
|
||||
>
|
||||
<Text as="label" size="label.sm" color="faded">
|
||||
|
||||
th: ({ children: c, ...props }) => (
|
||||
<Th {...cleanedProps(props)} textAlign="left" border="none">
|
||||
<Text as="span" size="label.sm" color="faded">
|
||||
{c}
|
||||
</Text>
|
||||
</Th>
|
||||
),
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
td: (props: any) => (
|
||||
|
||||
td: (props) => (
|
||||
<Td
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
{...(props as unknown as any)}
|
||||
{...cleanedProps(props)}
|
||||
borderColor="borderColor"
|
||||
textAlign="left!important"
|
||||
textAlign="left"
|
||||
borderBottomWidth={"inherit"}
|
||||
/>
|
||||
),
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
thead: (props: any) => <Thead {...props} />,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
tbody: (props: any) => <Tbody {...props} />,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
tr: (props: any) => (
|
||||
thead: (props) => <Thead {...cleanedProps(props)} />,
|
||||
tbody: (props) => <Tbody {...cleanedProps(props)} />,
|
||||
tr: (props) => (
|
||||
<Tr
|
||||
{...props}
|
||||
{...cleanedProps(props)}
|
||||
transition="all 0.1s"
|
||||
borderBottomWidth={1}
|
||||
_last={{ borderBottomWidth: 0 }}
|
||||
/>
|
||||
),
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
ul: ({ ordered, ...props }: any) => <UnorderedList {...props} mb={4} />,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
ol: ({ ordered, ...props }: any) => <OrderedList {...props} mb={4} />,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
li: ({ children: c, ordered, ...props }: any) => (
|
||||
<ListItem {...props}>
|
||||
<Text>{c}</Text>
|
||||
ul: (props) => {
|
||||
return <UnorderedList {...cleanedProps(props)} mb={4} />;
|
||||
},
|
||||
ol: (props) => <OrderedList {...cleanedProps(props)} mb={4} />,
|
||||
li: ({ children: c, ...props }) => (
|
||||
<ListItem {...cleanedProps(props)}>
|
||||
{/* hydration error - can not render p > p, so use span instead - remove extra span when we move to tailwind */}
|
||||
<Text as="span">{c}</Text>
|
||||
</ListItem>
|
||||
),
|
||||
}}
|
||||
@@ -219,3 +212,11 @@ export const MarkdownRenderer: React.FC<
|
||||
</ChakraReactMarkdown>
|
||||
);
|
||||
};
|
||||
|
||||
function cleanedProps<T extends object & { node?: unknown }>(
|
||||
props: T,
|
||||
): Omit<T, "node"> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { node, ...rest } = props;
|
||||
return rest;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
} from "@chakra-ui/react";
|
||||
import type { Abi } from "@thirdweb-dev/sdk";
|
||||
import type { Abi } from "abitype";
|
||||
import { CodeBlock, Heading } from "tw-components";
|
||||
import type { SourceFile } from "../types";
|
||||
|
||||
@@ -20,6 +20,34 @@ export const SourcesAccordion: React.FC<SourcesAccordionProps> = ({
|
||||
}) => {
|
||||
return (
|
||||
<Accordion allowMultiple defaultIndex={[]}>
|
||||
{/* ABI Accordion is put at the top for better UX */}
|
||||
{abi && (
|
||||
<AccordionItem
|
||||
gap={4}
|
||||
flexDirection="column"
|
||||
borderColor="borderColor"
|
||||
_first={{ borderTopWidth: 0 }}
|
||||
_last={{ borderBottomWidth: 0 }}
|
||||
>
|
||||
{({ isExpanded }) => (
|
||||
<>
|
||||
<AccordionButton justifyContent="space-between" py={2}>
|
||||
<Heading size="label.md">ABI</Heading>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel>
|
||||
{isExpanded && (
|
||||
<CodeBlock
|
||||
code={JSON.stringify(abi, null, 2)}
|
||||
language="json"
|
||||
overflow="auto"
|
||||
/>
|
||||
)}
|
||||
</AccordionPanel>
|
||||
</>
|
||||
)}
|
||||
</AccordionItem>
|
||||
)}
|
||||
{sources.map((signature) => (
|
||||
<AccordionItem
|
||||
gap={4}
|
||||
@@ -48,33 +76,6 @@ export const SourcesAccordion: React.FC<SourcesAccordionProps> = ({
|
||||
)}
|
||||
</AccordionItem>
|
||||
))}
|
||||
{abi && (
|
||||
<AccordionItem
|
||||
gap={4}
|
||||
flexDirection="column"
|
||||
borderColor="borderColor"
|
||||
_first={{ borderTopWidth: 0 }}
|
||||
_last={{ borderBottomWidth: 0 }}
|
||||
>
|
||||
{({ isExpanded }) => (
|
||||
<>
|
||||
<AccordionButton justifyContent="space-between" py={2}>
|
||||
<Heading size="label.md">ABI</Heading>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel>
|
||||
{isExpanded && (
|
||||
<CodeBlock
|
||||
code={JSON.stringify(abi, null, 2)}
|
||||
language="json"
|
||||
overflow="auto"
|
||||
/>
|
||||
)}
|
||||
</AccordionPanel>
|
||||
</>
|
||||
)}
|
||||
</AccordionItem>
|
||||
)}
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Flex } from "@chakra-ui/react";
|
||||
import type { Abi } from "@thirdweb-dev/sdk";
|
||||
import type { Abi } from "abitype";
|
||||
import { Link, Text } from "tw-components";
|
||||
import type { SourceFile } from "../types";
|
||||
import { SourcesAccordion } from "./sources-accordion";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { thirdwebClient } from "@/constants/client";
|
||||
import { useDashboardContractMetadata } from "@3rdweb-sdk/react/hooks/useDashboardContractMetadata";
|
||||
import { Skeleton } from "@chakra-ui/react";
|
||||
import type { BasicContract } from "contract-ui/types/types";
|
||||
import { useChainSlug } from "hooks/chains/chainSlug";
|
||||
import { thirdwebClient } from "lib/thirdweb-client";
|
||||
import { useV5DashboardChain } from "lib/v5-adapter";
|
||||
import { memo } from "react";
|
||||
import { getContract } from "thirdweb";
|
||||
|
||||
@@ -416,6 +416,7 @@ const ContractTable: ComponentWithChildren<ContractTableProps> = ({
|
||||
overflowX={{ base: "auto", md: "initial" }}
|
||||
// to avoid clipping the network selector menu on mobile
|
||||
minH={{ base: "600px", md: "initial" }}
|
||||
className="relative"
|
||||
>
|
||||
{isFetching && (
|
||||
<Spinner
|
||||
|
||||
@@ -6,16 +6,12 @@ import {
|
||||
detectFeatures as detectFeaturesFromSdk,
|
||||
isExtensionEnabled,
|
||||
} from "@thirdweb-dev/sdk";
|
||||
import type { Signer } from "ethers";
|
||||
import { getDashboardChainRpc } from "lib/rpc";
|
||||
import { getThirdwebSDK } from "lib/sdk";
|
||||
import { thirdwebClient } from "lib/thirdweb-client";
|
||||
import { MULTICHAIN_REGISTRY_CONTRACT } from "constants/contracts";
|
||||
import {
|
||||
getContract,
|
||||
prepareContractCall,
|
||||
sendAndConfirmTransaction,
|
||||
} from "thirdweb";
|
||||
import { polygon } from "thirdweb/chains";
|
||||
DASHBOARD_ENGINE_RELAYER_URL,
|
||||
DASHBOARD_FORWARDER_ADDRESS,
|
||||
} from "constants/misc";
|
||||
import { prepareContractCall, sendAndConfirmTransaction } from "thirdweb";
|
||||
import type { Account } from "thirdweb/wallets";
|
||||
|
||||
export function detectFeatures<TContract extends ValidContractInstance | null>(
|
||||
@@ -43,39 +39,13 @@ export function detectFeatures<TContract extends ValidContractInstance | null>(
|
||||
);
|
||||
}
|
||||
|
||||
export function getGaslessPolygonSDK(signer?: Signer) {
|
||||
const polygonSDK = getThirdwebSDK(
|
||||
polygon.id,
|
||||
getDashboardChainRpc(polygon.id, undefined),
|
||||
{
|
||||
gasless: {
|
||||
engine: {
|
||||
relayerUrl:
|
||||
"https://checkout.engine.thirdweb.com/relayer/0c2bdd3a-307f-4243-b6e5-5ba495222d2b",
|
||||
relayerForwarderAddress: "0x409d530a6961297ece29121dbee2c917c3398659",
|
||||
},
|
||||
experimentalChainlessSupport: true,
|
||||
},
|
||||
},
|
||||
signer,
|
||||
);
|
||||
|
||||
return polygonSDK;
|
||||
}
|
||||
|
||||
const contractAddress = "0xcdAD8FA86e18538aC207872E8ff3536501431B73";
|
||||
const registry = getContract({
|
||||
chain: polygon,
|
||||
client: thirdwebClient,
|
||||
address: contractAddress,
|
||||
});
|
||||
export async function addContractToMultiChainRegistry(
|
||||
contractData: AddContractInput,
|
||||
account: Account,
|
||||
gasOverride?: bigint,
|
||||
) {
|
||||
const transaction = prepareContractCall({
|
||||
contract: registry,
|
||||
contract: MULTICHAIN_REGISTRY_CONTRACT,
|
||||
method: "function add(address, address, uint256, string)",
|
||||
params: [
|
||||
account.address,
|
||||
@@ -94,9 +64,8 @@ export async function addContractToMultiChainRegistry(
|
||||
gasless: {
|
||||
experimentalChainlessSupport: true,
|
||||
provider: "engine",
|
||||
relayerUrl:
|
||||
"https://checkout.engine.thirdweb.com/relayer/0c2bdd3a-307f-4243-b6e5-5ba495222d2b",
|
||||
relayerForwarderAddress: "0x409d530a6961297ece29121dbee2c917c3398659",
|
||||
relayerUrl: DASHBOARD_ENGINE_RELAYER_URL,
|
||||
relayerForwarderAddress: DASHBOARD_FORWARDER_ADDRESS,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { thirdwebClient } from "@/constants/client";
|
||||
import {
|
||||
Box,
|
||||
Divider,
|
||||
@@ -29,8 +30,10 @@ import {
|
||||
import { useContractEnabledExtensions } from "components/contract-components/hooks";
|
||||
import { MarkdownRenderer } from "components/contract-components/published-contract/markdown-renderer";
|
||||
import { camelToTitle } from "contract-ui/components/solidity-inputs/helpers";
|
||||
import { useV5DashboardChain } from "lib/v5-adapter";
|
||||
import { useRouter } from "next/router";
|
||||
import { type Dispatch, type SetStateAction, useMemo, useState } from "react";
|
||||
import { getContract } from "thirdweb";
|
||||
import { Badge, Button, Card, Heading, Text } from "tw-components";
|
||||
import {
|
||||
COMMANDS,
|
||||
@@ -66,6 +69,16 @@ const ContractFunction: React.FC<ContractFunctionProps> = ({
|
||||
return undefined;
|
||||
}, [enabledExtensions]);
|
||||
|
||||
const chain = useV5DashboardChain(contract?.chainId);
|
||||
const contractV5 =
|
||||
chain && contract
|
||||
? getContract({
|
||||
address: contract.getAddress(),
|
||||
chain,
|
||||
client: thirdwebClient,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
if (!fn) {
|
||||
return null;
|
||||
}
|
||||
@@ -164,10 +177,10 @@ const ContractFunction: React.FC<ContractFunctionProps> = ({
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{contract && isFunction && (
|
||||
{contractV5 && isFunction && (
|
||||
<InteractiveAbiFunction
|
||||
key={JSON.stringify(fn)}
|
||||
contract={contract}
|
||||
contract={contractV5}
|
||||
abiFunction={fn}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -104,6 +104,7 @@ export const ContractFunctionsOverview: React.FC<ContractFunctionsOverview> = ({
|
||||
)}
|
||||
{(sources || abi) && (
|
||||
<TabPanel>
|
||||
{/* @ts-expect-error This won't cause any error and will auto-resolve itself once we have fully migrated to v5 */}
|
||||
<SourcesPanel sources={sources} abi={abi} />
|
||||
</TabPanel>
|
||||
)}
|
||||
|
||||
@@ -8,17 +8,24 @@ import {
|
||||
Input,
|
||||
} from "@chakra-ui/react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useContractWrite } from "@thirdweb-dev/react";
|
||||
import type { AbiFunction, SmartContract } from "@thirdweb-dev/sdk";
|
||||
import type { AbiFunction } from "@thirdweb-dev/sdk";
|
||||
import { TransactionButton } from "components/buttons/TransactionButton";
|
||||
import { SolidityInput } from "contract-ui/components/solidity-inputs";
|
||||
import { camelToTitle } from "contract-ui/components/solidity-inputs/helpers";
|
||||
import { BigNumber, type CallOverrides, utils } from "ethers";
|
||||
import { replaceIpfsUrl } from "lib/sdk";
|
||||
import { useEffect, useId, useMemo } from "react";
|
||||
import { FormProvider, useFieldArray, useForm } from "react-hook-form";
|
||||
import { FiPlay } from "react-icons/fi";
|
||||
import invariant from "tiny-invariant";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
type ThirdwebContract,
|
||||
prepareContractCall,
|
||||
readContract,
|
||||
resolveMethod,
|
||||
toWei,
|
||||
} from "thirdweb";
|
||||
import { useSendAndConfirmTransaction } from "thirdweb/react";
|
||||
import { parseAbiParams, stringify } from "thirdweb/utils";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
@@ -37,9 +44,8 @@ function formatResponseData(data: unknown): string {
|
||||
if (typeof data === "string") {
|
||||
return data;
|
||||
}
|
||||
if (BigNumber.isBigNumber(data)) {
|
||||
// biome-ignore lint/style/noParameterAssign: FIXME
|
||||
data = data.toString();
|
||||
if (typeof data === "bigint") {
|
||||
return data.toString();
|
||||
}
|
||||
|
||||
if (typeof data === "object") {
|
||||
@@ -56,7 +62,7 @@ function formatResponseData(data: unknown): string {
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.stringify(data, null, 2);
|
||||
return stringify(data, null, 2);
|
||||
}
|
||||
|
||||
function formatError(error: Error): string {
|
||||
@@ -87,7 +93,6 @@ function formatContractCall(
|
||||
}[]
|
||||
| undefined;
|
||||
}[],
|
||||
value?: BigNumber,
|
||||
) {
|
||||
const parsedParams = params
|
||||
.map((p) => (p.type === "bool" ? p.value !== "false" : p.value))
|
||||
@@ -104,31 +109,23 @@ function formatContractCall(
|
||||
return p;
|
||||
}
|
||||
});
|
||||
|
||||
if (value) {
|
||||
parsedParams.push({
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
return parsedParams;
|
||||
}
|
||||
|
||||
interface InteractiveAbiFunctionProps {
|
||||
abiFunction?: AbiFunction;
|
||||
contract: SmartContract;
|
||||
abiFunction: AbiFunction;
|
||||
contract: ThirdwebContract;
|
||||
}
|
||||
|
||||
function useDelayedRead(
|
||||
contract: Required<SmartContract> | undefined,
|
||||
functionName?: string,
|
||||
) {
|
||||
function useAsyncRead(contract: ThirdwebContract, functionName: string) {
|
||||
return useMutation(
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
async ({ args, overrides }: { args: any[]; overrides?: CallOverrides }) => {
|
||||
invariant(contract, "Contract is required");
|
||||
invariant(functionName, "functionName is required");
|
||||
return contract?.call(functionName, args, overrides);
|
||||
async ({ args, types }: { args: unknown[]; types: string[] }) => {
|
||||
const params = parseAbiParams(types, args);
|
||||
return readContract({
|
||||
contract,
|
||||
method: resolveMethod(functionName),
|
||||
params,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -141,7 +138,7 @@ export const InteractiveAbiFunction: React.FC<InteractiveAbiFunctionProps> = ({
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
params:
|
||||
abiFunction?.inputs.map((i) => ({
|
||||
abiFunction.inputs.map((i) => ({
|
||||
key: i.name || "key",
|
||||
value: "",
|
||||
type: i.type,
|
||||
@@ -166,15 +163,20 @@ export const InteractiveAbiFunction: React.FC<InteractiveAbiFunctionProps> = ({
|
||||
mutate,
|
||||
data,
|
||||
error: mutationError,
|
||||
isLoading: mutationLoading,
|
||||
} = useContractWrite(contract, abiFunction?.name);
|
||||
isPending: mutationLoading,
|
||||
} = useSendAndConfirmTransaction();
|
||||
|
||||
const {
|
||||
mutate: readFn,
|
||||
data: readData,
|
||||
isLoading: readLoading,
|
||||
error: readError,
|
||||
} = useDelayedRead(contract, abiFunction?.name);
|
||||
} = useAsyncRead(contract, abiFunction.name);
|
||||
|
||||
const formattedReadData: string = useMemo(
|
||||
() => (readData ? formatResponseData(readData) : ""),
|
||||
[readData],
|
||||
);
|
||||
|
||||
const error = isView ? readError : mutationError;
|
||||
|
||||
@@ -183,12 +185,15 @@ export const InteractiveAbiFunction: React.FC<InteractiveAbiFunctionProps> = ({
|
||||
useEffect(() => {
|
||||
if (
|
||||
form.watch("params").length === 0 &&
|
||||
(abiFunction?.stateMutability === "view" ||
|
||||
abiFunction?.stateMutability === "pure")
|
||||
(abiFunction.stateMutability === "view" ||
|
||||
abiFunction.stateMutability === "pure")
|
||||
) {
|
||||
readFn({ args: [] });
|
||||
readFn({
|
||||
args: [],
|
||||
types: [],
|
||||
});
|
||||
}
|
||||
}, [readFn, abiFunction?.stateMutability, form]);
|
||||
}, [abiFunction, form, readFn]);
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
@@ -217,19 +222,24 @@ export const InteractiveAbiFunction: React.FC<InteractiveAbiFunctionProps> = ({
|
||||
const formatted = formatContractCall(d.params);
|
||||
if (
|
||||
contract &&
|
||||
(abiFunction?.stateMutability === "view" ||
|
||||
abiFunction?.stateMutability === "pure")
|
||||
(abiFunction.stateMutability === "view" ||
|
||||
abiFunction.stateMutability === "pure")
|
||||
) {
|
||||
readFn({
|
||||
args: formatted,
|
||||
});
|
||||
const types = abiFunction.inputs.map((o) => o.type);
|
||||
readFn({ args: formatted, types });
|
||||
} else {
|
||||
mutate({
|
||||
args: formatted,
|
||||
overrides: d.value
|
||||
? { value: utils.parseEther(d.value) }
|
||||
: undefined,
|
||||
if (!abiFunction.name) {
|
||||
return toast.error("Cannot detect function name");
|
||||
}
|
||||
const types = abiFunction.inputs.map((o) => o.type);
|
||||
const params = parseAbiParams(types, formatted);
|
||||
const transaction = prepareContractCall({
|
||||
contract,
|
||||
method: resolveMethod(abiFunction.name),
|
||||
params,
|
||||
value: d.value ? toWei(d.value) : undefined,
|
||||
});
|
||||
mutate(transaction);
|
||||
}
|
||||
}
|
||||
})}
|
||||
@@ -258,7 +268,7 @@ export const InteractiveAbiFunction: React.FC<InteractiveAbiFunctionProps> = ({
|
||||
solidityType={item.type}
|
||||
solidityComponents={item.components}
|
||||
{...form.register(`params.${index}.value`)}
|
||||
functionName={abiFunction?.name}
|
||||
functionName={abiFunction.name}
|
||||
/>
|
||||
<FormErrorMessage>
|
||||
{
|
||||
@@ -274,7 +284,7 @@ export const InteractiveAbiFunction: React.FC<InteractiveAbiFunctionProps> = ({
|
||||
</>
|
||||
)}
|
||||
|
||||
{abiFunction?.stateMutability === "payable" && (
|
||||
{abiFunction.stateMutability === "payable" && (
|
||||
<>
|
||||
<Divider mb="8px" />
|
||||
<FormControl gap={0.5}>
|
||||
@@ -318,19 +328,18 @@ export const InteractiveAbiFunction: React.FC<InteractiveAbiFunctionProps> = ({
|
||||
language="json"
|
||||
code={formatResponseData(data || readData)}
|
||||
/>
|
||||
{typeof readData === "string" &&
|
||||
readData?.startsWith("ipfs://") && (
|
||||
<Text size="label.sm">
|
||||
<TrackedLink
|
||||
href={replaceIpfsUrl(readData)}
|
||||
isExternal
|
||||
category="contract-explorer"
|
||||
label="open-in-gateway"
|
||||
>
|
||||
Open in gateway
|
||||
</TrackedLink>
|
||||
</Text>
|
||||
)}
|
||||
{formattedReadData.startsWith("ipfs://") && (
|
||||
<Text size="label.sm">
|
||||
<TrackedLink
|
||||
href={replaceIpfsUrl(formattedReadData)}
|
||||
isExternal
|
||||
category="contract-explorer"
|
||||
label="open-in-gateway"
|
||||
>
|
||||
Open in gateway
|
||||
</TrackedLink>
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
</Flex>
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
Stack,
|
||||
Tooltip,
|
||||
} from "@chakra-ui/react";
|
||||
import type { OptionalPropertiesInput } from "@thirdweb-dev/sdk";
|
||||
import { FileInput } from "components/shared/FileInput";
|
||||
import { useEffect } from "react";
|
||||
import {
|
||||
@@ -27,10 +26,13 @@ import {
|
||||
} from "react-hook-form";
|
||||
import { FiPlus, FiSlash, FiTrash, FiUpload, FiX } from "react-icons/fi";
|
||||
import { Button, FormErrorMessage, FormLabel } from "tw-components";
|
||||
import type { z } from "zod";
|
||||
|
||||
type OptionalPropertiesInput = {
|
||||
[key: string]: string | number;
|
||||
};
|
||||
|
||||
interface IPropertyFieldValues extends FieldValues {
|
||||
attributes?: z.input<typeof OptionalPropertiesInput>;
|
||||
attributes?: OptionalPropertiesInput;
|
||||
}
|
||||
|
||||
interface IPropertiesFormControlProps<
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useRouter } from "next/router";
|
||||
import type { EngineInstance } from "../../@3rdweb-sdk/react/hooks/useEngine";
|
||||
import { PageId } from "../../page-id";
|
||||
import type { ThirdwebNextPage } from "../../utils/types";
|
||||
import { AppLayout } from "../app-layouts/app";
|
||||
import {
|
||||
type ActivePage,
|
||||
EnginePageLayout,
|
||||
PageLoading,
|
||||
} from "./EnginePageLayout";
|
||||
|
||||
export function createEnginePage(
|
||||
activePage: ActivePage,
|
||||
PageContent: React.FC<{ instance: EngineInstance }>,
|
||||
): ThirdwebNextPage {
|
||||
const Page: ThirdwebNextPage = () => {
|
||||
const router = useRouter();
|
||||
const engineId = router.query.engineId;
|
||||
|
||||
if (typeof engineId !== "string") {
|
||||
return <PageLoading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<EnginePageLayout
|
||||
activePage={activePage}
|
||||
engineId={engineId}
|
||||
content={PageContent}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Page.pageId = PageId.EngineManage;
|
||||
|
||||
Page.getLayout = (page, props) => (
|
||||
<AppLayout {...props} hasSidebar={true}>
|
||||
{page}
|
||||
</AppLayout>
|
||||
);
|
||||
|
||||
return Page;
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { useAccount } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { ArrowLeftIcon, CircleAlertIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import invariant from "tiny-invariant";
|
||||
import { CopyTextButton } from "../../@/components/ui/CopyTextButton";
|
||||
import { Button } from "../../@/components/ui/button";
|
||||
import { Separator } from "../../@/components/ui/separator";
|
||||
import { useDashboardRouter } from "../../@/lib/DashboardRouter";
|
||||
import {
|
||||
type EngineInstance,
|
||||
useEngineInstances,
|
||||
} from "../../@3rdweb-sdk/react/hooks/useEngine";
|
||||
import { SidebarNav } from "../../core-ui/sidebar/nav";
|
||||
import type { Route } from "../../core-ui/sidebar/types";
|
||||
import { useTrack } from "../../hooks/analytics/useTrack";
|
||||
import { EngineVersionBadge } from "./badges/version";
|
||||
import { useHasEnginePermission } from "./useHasEnginePermission";
|
||||
|
||||
export type ActivePage =
|
||||
| "overview"
|
||||
| "explorer"
|
||||
| "relayers"
|
||||
| "contract-subscriptions"
|
||||
| "admins"
|
||||
| "access-tokens"
|
||||
| "webhooks"
|
||||
| "configuration"
|
||||
| "metrics";
|
||||
|
||||
const NEXT_PUBLIC_DEMO_ENGINE_URL = process.env.NEXT_PUBLIC_DEMO_ENGINE_URL;
|
||||
|
||||
export function EnginePageLayout(props: {
|
||||
engineId: string;
|
||||
activePage: ActivePage;
|
||||
content: React.FC<{ instance: EngineInstance }>;
|
||||
}) {
|
||||
const { data } = useAccount();
|
||||
|
||||
if (props.engineId === "sandbox" && data) {
|
||||
invariant(
|
||||
NEXT_PUBLIC_DEMO_ENGINE_URL,
|
||||
"missing NEXT_PUBLIC_DEMO_ENGINE_URL",
|
||||
);
|
||||
|
||||
const sandboxEngine: EngineInstance = {
|
||||
id: "sandbox",
|
||||
url: NEXT_PUBLIC_DEMO_ENGINE_URL,
|
||||
name: "Demo Engine",
|
||||
status: "active",
|
||||
lastAccessedAt: new Date().toISOString(),
|
||||
accountId: data?.id,
|
||||
};
|
||||
|
||||
return (
|
||||
<RenderInstance
|
||||
instance={sandboxEngine}
|
||||
activePage={props.activePage}
|
||||
content={props.content}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <QueryAndRenderInstance {...props} />;
|
||||
}
|
||||
|
||||
function QueryAndRenderInstance(props: {
|
||||
activePage: ActivePage;
|
||||
engineId: string;
|
||||
content: React.FC<{ instance: EngineInstance }>;
|
||||
}) {
|
||||
const instancesQuery = useEngineInstances();
|
||||
const instance = instancesQuery.data?.find((x) => x.id === props.engineId);
|
||||
|
||||
if (instancesQuery.isLoading) {
|
||||
return <PageLoading />;
|
||||
}
|
||||
|
||||
if (!instance) {
|
||||
return <EngineErrorPage>Engine Instance Not Found</EngineErrorPage>;
|
||||
}
|
||||
|
||||
return (
|
||||
<EnsurePermissionAndRenderInstance
|
||||
instance={instance}
|
||||
activePage={props.activePage}
|
||||
content={props.content}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function EnsurePermissionAndRenderInstance(props: {
|
||||
activePage: ActivePage;
|
||||
content: React.FC<{ instance: EngineInstance }>;
|
||||
instance: EngineInstance;
|
||||
}) {
|
||||
const permissionQuery = useHasEnginePermission({
|
||||
instanceUrl: props.instance.url,
|
||||
});
|
||||
|
||||
if (permissionQuery.isLoading) {
|
||||
return <PageLoading />;
|
||||
}
|
||||
|
||||
if (permissionQuery.error instanceof Error) {
|
||||
if (permissionQuery.error.message.includes("Failed to fetch")) {
|
||||
return (
|
||||
<EngineErrorPage>
|
||||
<p>Unable to connect to Engine</p>
|
||||
|
||||
<p>Ensure that your Engine is publicly accessible</p>
|
||||
</EngineErrorPage>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EngineErrorPage>
|
||||
<p>There was an unexpected error reaching your Engine instance</p>
|
||||
<p>Try again or contact us if this issue persists.</p>
|
||||
</EngineErrorPage>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
permissionQuery.data &&
|
||||
permissionQuery.data.hasPermission === false &&
|
||||
permissionQuery.data.reason === "Unauthorized"
|
||||
) {
|
||||
return (
|
||||
<div>
|
||||
You are not an admin for this Engine instance. Contact the owner to add
|
||||
your wallet as an admin
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<RenderInstance
|
||||
instance={props.instance}
|
||||
activePage={props.activePage}
|
||||
content={props.content}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function RenderInstance(props: {
|
||||
instance: EngineInstance;
|
||||
activePage: ActivePage;
|
||||
content: React.FC<{ instance: EngineInstance }>;
|
||||
}) {
|
||||
const { instance, activePage } = props;
|
||||
const router = useDashboardRouter();
|
||||
const trackEvent = useTrack();
|
||||
|
||||
const handleClick = useCallback(
|
||||
(id: ActivePage) => {
|
||||
trackEvent({
|
||||
category: "engine",
|
||||
action: "navigate-tab",
|
||||
label: id,
|
||||
url: instance.url,
|
||||
});
|
||||
if (id === "overview") {
|
||||
router.push(`/dashboard/engine/${instance.id}`);
|
||||
} else {
|
||||
router.push(`/dashboard/engine/${instance.id}/${id}`);
|
||||
}
|
||||
},
|
||||
[instance.url, instance.id, trackEvent, router],
|
||||
);
|
||||
|
||||
const links = useMemo(
|
||||
() =>
|
||||
[
|
||||
{
|
||||
path: `/dashboard/engine/${instance.id}`,
|
||||
title: "Overview",
|
||||
name: "overview",
|
||||
onClick: () => handleClick("overview"),
|
||||
},
|
||||
{
|
||||
path: `/dashboard/engine/${instance.id}/explorer`,
|
||||
title: "Explorer",
|
||||
name: "explorer",
|
||||
onClick: () => handleClick("explorer"),
|
||||
},
|
||||
{
|
||||
path: `/dashboard/engine/${instance.id}/relayers`,
|
||||
title: "Relayers",
|
||||
name: "relayers",
|
||||
onClick: () => handleClick("relayers"),
|
||||
},
|
||||
{
|
||||
path: `/dashboard/engine/${instance.id}/contract-subscriptions`,
|
||||
title: "Contract Subscriptions",
|
||||
name: "contract-subscriptions",
|
||||
onClick: () => handleClick("contract-subscriptions"),
|
||||
},
|
||||
{
|
||||
path: `/dashboard/engine/${instance.id}/admins`,
|
||||
title: "Admins",
|
||||
name: "admins",
|
||||
onClick: () => handleClick("admins"),
|
||||
},
|
||||
{
|
||||
path: `/dashboard/engine/${instance.id}/access-tokens`,
|
||||
title: "Access Tokens",
|
||||
name: "access-tokens",
|
||||
onClick: () => handleClick("access-tokens"),
|
||||
},
|
||||
{
|
||||
path: `/dashboard/engine/${instance.id}/webhooks`,
|
||||
title: "Webhooks",
|
||||
name: "webhooks",
|
||||
onClick: () => handleClick("webhooks"),
|
||||
},
|
||||
{
|
||||
path: `/dashboard/engine/${instance.id}/configuration`,
|
||||
title: "Configuration",
|
||||
name: "configuration",
|
||||
onClick: () => handleClick("configuration"),
|
||||
},
|
||||
{
|
||||
path: `/dashboard/engine/${instance.id}/metrics`,
|
||||
title: "Metrics",
|
||||
name: "metrics",
|
||||
onClick: () => handleClick("metrics"),
|
||||
},
|
||||
] satisfies Route[],
|
||||
[handleClick, instance.id],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidebarNav links={links} activePage={activePage} title="Engine" />
|
||||
|
||||
<div className="flex">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="px-2 py-1 -translate-x-2 flex items-center gap-2 text-muted-foreground hover:text-foreground h-auto"
|
||||
>
|
||||
<Link
|
||||
href="/dashboard/engine"
|
||||
aria-label="Go Back"
|
||||
className="flex items-center gap-2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<ArrowLeftIcon className="size-4" /> Back
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="h-5" />
|
||||
|
||||
<div className="flex flex-col lg:flex-row lg:items-center gap-4 lg:justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl md:text-5xl font-semibold tracking-tighter">
|
||||
{instance.name}
|
||||
</h1>
|
||||
|
||||
<div className="h-1" />
|
||||
|
||||
<div className="flex gap-3 items-center">
|
||||
{!instance.name.startsWith("https://") && (
|
||||
<CopyTextButton
|
||||
copyIconPosition="right"
|
||||
textToShow={instance.url}
|
||||
textToCopy={instance.url}
|
||||
tooltip="Copy Engine URL"
|
||||
variant="ghost"
|
||||
className="px-2 py-1 -translate-x-2 h-auto text-muted-foreground"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<EngineVersionBadge instance={instance} />
|
||||
</div>
|
||||
|
||||
<div className="h-5" />
|
||||
<Separator />
|
||||
<div className="h-10 " />
|
||||
|
||||
<props.content instance={instance} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function EngineErrorPage(props: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<Link
|
||||
href="/dashboard/engine"
|
||||
className="gap-2 flex items-center text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<ArrowLeftIcon className="size-5" />
|
||||
Back
|
||||
</Link>
|
||||
|
||||
<div className="border rounded-lg border-border mt-5">
|
||||
<MessageContainer>
|
||||
<div className="flex flex-col gap-4 items-center">
|
||||
<CircleAlertIcon className="size-16 text-destructive-text" />
|
||||
<p className="text-secondary-foreground text-center">
|
||||
{props.children}
|
||||
</p>
|
||||
</div>
|
||||
</MessageContainer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MessageContainer(props: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="h-[300px] lg:h-[400px] flex items-center justify-center">
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PageLoading() {
|
||||
return (
|
||||
<MessageContainer>
|
||||
<Spinner className="size-10" />
|
||||
</MessageContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { SidebarNav } from "../../core-ui/sidebar/nav";
|
||||
|
||||
export function EngineSidebar() {
|
||||
return (
|
||||
<SidebarNav
|
||||
title="Engine"
|
||||
links={[
|
||||
{
|
||||
name: "overview",
|
||||
path: "/dashboard/engine",
|
||||
title: "Overview",
|
||||
},
|
||||
{
|
||||
name: "create",
|
||||
path: "/dashboard/engine/create",
|
||||
title: "Create",
|
||||
},
|
||||
{
|
||||
name: "import",
|
||||
path: "/dashboard/engine/import",
|
||||
title: "Import",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,27 +1,25 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import {
|
||||
type EngineInstance,
|
||||
useEngineLatestVersion,
|
||||
useEngineSystemHealth,
|
||||
useEngineUpdateVersion,
|
||||
} from "@3rdweb-sdk/react/hooks/useEngine";
|
||||
import { CircleArrowDownIcon, CloudDownloadIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import {
|
||||
Flex,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
Tag,
|
||||
TagLabel,
|
||||
TagLeftIcon,
|
||||
Tooltip,
|
||||
type UseDisclosureReturn,
|
||||
useDisclosure,
|
||||
} from "@chakra-ui/react";
|
||||
import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
import { FaArrowCircleUp } from "react-icons/fa";
|
||||
import { Button, Text, TrackedLink } from "tw-components";
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { TrackedLinkTW } from "@/components/ui/tracked-link";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export const EngineVersionBadge = ({
|
||||
instance,
|
||||
@@ -30,7 +28,7 @@ export const EngineVersionBadge = ({
|
||||
}) => {
|
||||
const healthQuery = useEngineSystemHealth(instance.url);
|
||||
const latestVersionQuery = useEngineLatestVersion();
|
||||
const disclosure = useDisclosure();
|
||||
const [isModalOpen, setModalOpen] = useState(false);
|
||||
|
||||
const current = healthQuery.data?.engineVersion ?? "...";
|
||||
const latest = latestVersionQuery.data ?? "...";
|
||||
@@ -38,112 +36,141 @@ export const EngineVersionBadge = ({
|
||||
|
||||
if (!isStale) {
|
||||
return (
|
||||
<Tag size="sm">
|
||||
<TagLabel fontSize="small">{current}</TagLabel>
|
||||
</Tag>
|
||||
<ToolTipLabel label="Latest Version">
|
||||
<Button variant="outline" asChild className="hover:bg-transparent">
|
||||
<div>{current}</div>
|
||||
</Button>
|
||||
</ToolTipLabel>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip label="Update to the latest version">
|
||||
<Button variant="unstyled">
|
||||
<Tag
|
||||
onClick={disclosure.onOpen}
|
||||
size="sm"
|
||||
colorScheme="blue"
|
||||
variant="outline"
|
||||
>
|
||||
<TagLeftIcon as={FaArrowCircleUp} boxSize={3} />
|
||||
<TagLabel fontSize="small">{current}</TagLabel>
|
||||
</Tag>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<ToolTipLabel
|
||||
label="New version is available"
|
||||
leftIcon={
|
||||
<CircleArrowDownIcon className="size-4 text-link-foreground" />
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="relative"
|
||||
onClick={() => setModalOpen(true)}
|
||||
>
|
||||
{current}
|
||||
|
||||
{disclosure.isOpen && (
|
||||
<UpdateVersionModal
|
||||
disclosure={disclosure}
|
||||
latest={latest ?? ""}
|
||||
instance={instance}
|
||||
/>
|
||||
)}
|
||||
{/* Notification Dot */}
|
||||
<span className="absolute -top-1 -right-1">
|
||||
<PulseDot />
|
||||
</span>
|
||||
</Button>
|
||||
</ToolTipLabel>
|
||||
|
||||
<UpdateVersionModal
|
||||
open={isModalOpen}
|
||||
onOpenChange={setModalOpen}
|
||||
latest={latest ?? ""}
|
||||
instance={instance}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const UpdateVersionModal = ({
|
||||
disclosure,
|
||||
latest,
|
||||
instance,
|
||||
}: {
|
||||
disclosure: UseDisclosureReturn;
|
||||
const UpdateVersionModal = (props: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
latest: string;
|
||||
instance: EngineInstance;
|
||||
}) => {
|
||||
const { mutate } = useEngineUpdateVersion();
|
||||
const { onSuccess, onError } = useTxNotifications(
|
||||
"Submitted a request to update your Engine instance. Please allow 1-2 business days for this process.",
|
||||
"Unexpected error updating your Engine instance.",
|
||||
);
|
||||
const { open, onOpenChange, latest, instance } = props;
|
||||
const updateEngine = useEngineUpdateVersion();
|
||||
|
||||
if (!instance.cloudDeployedAt) {
|
||||
// For self-hosted, show a prompt to the Github release page.
|
||||
return (
|
||||
<Modal isOpen={disclosure.isOpen} onClose={disclosure.onClose}>
|
||||
<ModalContent>
|
||||
<ModalCloseButton />
|
||||
|
||||
<ModalHeader>Update your self-hosted Engine to {latest}</ModalHeader>
|
||||
<ModalBody>
|
||||
<Text>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent
|
||||
className="z-[10001] max-w-[400px]"
|
||||
dialogOverlayClassName="z-[10000]"
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl font-semibold tracking-tight pr-4 mb-6">
|
||||
Update your self-hosted Engine to {latest}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
View the changelog in the{" "}
|
||||
<TrackedLink
|
||||
<TrackedLinkTW
|
||||
href="https://github.com/thirdweb-dev/engine/releases"
|
||||
category="engine"
|
||||
label="clicked-engine-releases"
|
||||
isExternal
|
||||
color="blue.500"
|
||||
target="_blank"
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
>
|
||||
Engine Github repository
|
||||
</TrackedLink>
|
||||
.
|
||||
</Text>
|
||||
</ModalBody>
|
||||
<ModalFooter as={Flex} gap={3}>
|
||||
<Button type="button" onClick={disclosure.onClose} variant="ghost">
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</TrackedLinkTW>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
const onClick = () => {
|
||||
const onClick = async () => {
|
||||
try {
|
||||
mutate({ engineId: instance.id });
|
||||
onSuccess();
|
||||
} catch (e) {
|
||||
onError(e);
|
||||
const promise = updateEngine.mutateAsync({ engineId: instance.id });
|
||||
toast.promise(promise, {
|
||||
success:
|
||||
"Submitted a request to update your Engine instance. Please allow 1-2 business days for this process.",
|
||||
error: "Unexpected error updating your Engine instance.",
|
||||
});
|
||||
await promise;
|
||||
} finally {
|
||||
onOpenChange(false);
|
||||
}
|
||||
disclosure.onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={disclosure.isOpen} onClose={disclosure.onClose}>
|
||||
<ModalContent>
|
||||
<ModalCloseButton />
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent
|
||||
className="z-[10001] max-w-[400px]"
|
||||
dialogOverlayClassName="z-[10000]"
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Update Engine to {latest}?</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<ModalHeader>Update Engine to {latest}?</ModalHeader>
|
||||
<ModalFooter as={Flex} gap={3}>
|
||||
<Button type="button" onClick={disclosure.onClose} variant="ghost">
|
||||
<DialogFooter className="mt-5">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => onOpenChange(false)}
|
||||
variant="outline"
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<Button type="submit" onClick={onClick} colorScheme="blue">
|
||||
<Button
|
||||
type="submit"
|
||||
onClick={onClick}
|
||||
variant="primary"
|
||||
className="gap-2"
|
||||
>
|
||||
{updateEngine.isLoading ? (
|
||||
<Spinner className="size-4" />
|
||||
) : (
|
||||
<CloudDownloadIcon className="size-4" />
|
||||
)}
|
||||
Update
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
function PulseDot() {
|
||||
return (
|
||||
<span className="relative flex size-2">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-sky-400 opacity-75" />
|
||||
<span className="relative inline-flex rounded-full size-2 bg-primary" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
+50
-13
@@ -1,7 +1,9 @@
|
||||
import { thirdwebClient } from "@/constants/client";
|
||||
import {
|
||||
type AddContractSubscriptionInput,
|
||||
useEngineAddContractSubscription,
|
||||
} from "@3rdweb-sdk/react/hooks/useEngine";
|
||||
import { useResolveContractAbi } from "@3rdweb-sdk/react/hooks/useResolveContractAbi";
|
||||
import {
|
||||
CheckboxGroup,
|
||||
Collapse,
|
||||
@@ -25,11 +27,12 @@ import {
|
||||
} from "@chakra-ui/react";
|
||||
import { NetworkDropdown } from "components/contract-components/contract-publish-form/NetworkDropdown";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useContractAbiItems } from "hooks/useContractAbiItems";
|
||||
import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
import { useV5DashboardChain } from "lib/v5-adapter";
|
||||
import { type Dispatch, type SetStateAction, useMemo, useState } from "react";
|
||||
import { type UseFormReturn, useForm } from "react-hook-form";
|
||||
import { AiOutlinePlusCircle } from "react-icons/ai";
|
||||
import { getContract } from "thirdweb";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
@@ -421,32 +424,66 @@ const FilterSelector = ({
|
||||
filter: string[];
|
||||
setFilter: (value: string[]) => void;
|
||||
}) => {
|
||||
const abiItemsQuery = useContractAbiItems(
|
||||
form.getValues("chainId"),
|
||||
form.getValues("contractAddress") as `0x${string}`,
|
||||
);
|
||||
const chain = useV5DashboardChain(form.getValues("chainId"));
|
||||
const contract = chain
|
||||
? getContract({
|
||||
address: form.getValues("contractAddress"),
|
||||
chain,
|
||||
client: thirdwebClient,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const abiQuery = useResolveContractAbi(contract);
|
||||
|
||||
const abiItems = useMemo(() => {
|
||||
if (!abiQuery.data) {
|
||||
return {
|
||||
readFunctions: [],
|
||||
writeFunctions: [],
|
||||
events: [],
|
||||
};
|
||||
}
|
||||
const readFunctions: string[] = [];
|
||||
const writeFunctions: string[] = [];
|
||||
const events: string[] = [];
|
||||
for (const abiItem of abiQuery.data) {
|
||||
if (abiItem.type === "function") {
|
||||
if (
|
||||
abiItem.stateMutability === "pure" ||
|
||||
abiItem.stateMutability === "view"
|
||||
) {
|
||||
readFunctions.push(abiItem.name);
|
||||
} else {
|
||||
writeFunctions.push(abiItem.name);
|
||||
}
|
||||
} else if (abiItem.type === "event") {
|
||||
events.push(abiItem.name);
|
||||
}
|
||||
}
|
||||
return {
|
||||
readFunctions: [...new Set(readFunctions)].sort(),
|
||||
writeFunctions: [...new Set(writeFunctions)].sort(),
|
||||
events: [...new Set(events)].sort(),
|
||||
};
|
||||
}, [abiQuery.data]);
|
||||
|
||||
const filterNames = useMemo(() => {
|
||||
switch (abiItemType) {
|
||||
case "function": {
|
||||
return abiItemsQuery.data.writeFunctions;
|
||||
return abiItems.writeFunctions;
|
||||
}
|
||||
case "event": {
|
||||
return abiItemsQuery.data.events;
|
||||
return abiItems.events;
|
||||
}
|
||||
default: {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}, [
|
||||
abiItemType,
|
||||
abiItemsQuery.data.events,
|
||||
abiItemsQuery.data.writeFunctions,
|
||||
]);
|
||||
}, [abiItemType, abiItems.events, abiItems.writeFunctions]);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
{abiItemsQuery.isLoading ? (
|
||||
{abiQuery.isLoading ? (
|
||||
<Spinner size="sm" />
|
||||
) : filterNames.length === 0 ? (
|
||||
<Text>
|
||||
|
||||
+1
-1
@@ -1,3 +1,4 @@
|
||||
import { thirdwebClient } from "@/constants/client";
|
||||
import {
|
||||
type EngineContractSubscription,
|
||||
useEngineRemoveContractSubscription,
|
||||
@@ -26,7 +27,6 @@ import { format } from "date-fns";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useAllChainsData } from "hooks/chains/allChains";
|
||||
import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
import { thirdwebClient } from "lib/thirdweb-client";
|
||||
import { useV5DashboardChain } from "lib/v5-adapter";
|
||||
import { useState } from "react";
|
||||
import { FiInfo, FiTrash } from "react-icons/fi";
|
||||
|
||||
@@ -1,257 +0,0 @@
|
||||
import { AccountStatus, useAccount } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import type { EngineTier } from "@3rdweb-sdk/react/hooks/useEngine";
|
||||
import {
|
||||
Flex,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
SimpleGrid,
|
||||
type UseDisclosureReturn,
|
||||
useDisclosure,
|
||||
useToast,
|
||||
} from "@chakra-ui/react";
|
||||
import { ChakraNextImage } from "components/Image";
|
||||
import { OnboardingModal } from "components/onboarding/Modal";
|
||||
import { THIRDWEB_API_HOST } from "constants/urls";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useSingleQueryParam } from "hooks/useQueryParam";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { Button, Heading, Text } from "tw-components";
|
||||
import { LazyOnboardingBilling } from "../onboarding/LazyOnboardingBilling";
|
||||
import { EngineTierCard, MONTHLY_PRICE_USD } from "./tier-card";
|
||||
|
||||
interface CreateEngineInstanceButtonProps {
|
||||
ctaText: string;
|
||||
refetch: () => void;
|
||||
}
|
||||
|
||||
export const CreateEngineInstanceButton = ({
|
||||
ctaText,
|
||||
refetch,
|
||||
}: CreateEngineInstanceButtonProps) => {
|
||||
const showModalOnLoad = useSingleQueryParam("requestCloudHosted");
|
||||
const disclosure = useDisclosure({
|
||||
defaultIsOpen: showModalOnLoad !== undefined,
|
||||
});
|
||||
const paymentDisclosure = useDisclosure();
|
||||
const trackEvent = useTrack();
|
||||
const toast = useToast();
|
||||
const accountQuery = useAccount();
|
||||
const [engineTier, setEngineTier] = useState<EngineTier>("STARTER");
|
||||
|
||||
const addCloudHostedEngine = async (tier?: EngineTier) => {
|
||||
trackEvent({
|
||||
category: "engine",
|
||||
action: "click",
|
||||
label: "clicked-cloud-hosted",
|
||||
tier: tier ?? engineTier,
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${THIRDWEB_API_HOST}/v1/engine/add-cloud-hosted`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tier: tier ?? engineTier,
|
||||
}),
|
||||
},
|
||||
);
|
||||
if (!res.ok) {
|
||||
if (res.status === 409) {
|
||||
toast({
|
||||
status: "warning",
|
||||
description:
|
||||
"There is a pending Engine deployment. Please contact [email protected] if this takes longer than 2 hours.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
const error =
|
||||
json.error?.message ??
|
||||
"Unexpected error. Please try again or visit https://thirdweb.com/support.";
|
||||
throw error;
|
||||
}
|
||||
|
||||
toast({
|
||||
status: "success",
|
||||
description: "Thank you! Your Engine deployment will begin shortly.",
|
||||
});
|
||||
|
||||
// Refresh the engine list to show a pending cloud-hosted Engine instance.
|
||||
refetch();
|
||||
} catch (e) {
|
||||
toast({
|
||||
status: "error",
|
||||
description: `${e}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
trackEvent({
|
||||
category: "engine",
|
||||
action: "click",
|
||||
label: "add-engine-instance",
|
||||
});
|
||||
disclosure.onOpen();
|
||||
}}
|
||||
colorScheme="blue"
|
||||
px={6}
|
||||
>
|
||||
{ctaText}
|
||||
</Button>
|
||||
|
||||
{disclosure.isOpen && (
|
||||
<CreateCloudHostedEngineModal
|
||||
disclosure={disclosure}
|
||||
onClickAddToPlan={async (tier: EngineTier) => {
|
||||
setEngineTier(tier);
|
||||
disclosure.onClose();
|
||||
|
||||
if (accountQuery.data?.status === AccountStatus.ValidPayment) {
|
||||
await addCloudHostedEngine(tier);
|
||||
} else {
|
||||
trackEvent({
|
||||
category: "engine",
|
||||
action: "click",
|
||||
label: "clicked-add-payment-method",
|
||||
});
|
||||
// Switch the Cloud-hosted Engine modal with the Add Payment modal.
|
||||
paymentDisclosure.onOpen();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<OnboardingModal
|
||||
isOpen={paymentDisclosure.isOpen}
|
||||
onClose={paymentDisclosure.onClose}
|
||||
>
|
||||
<LazyOnboardingBilling
|
||||
onSave={async () => {
|
||||
await addCloudHostedEngine();
|
||||
paymentDisclosure.onClose();
|
||||
}}
|
||||
onCancel={paymentDisclosure.onClose}
|
||||
/>
|
||||
</OnboardingModal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const CreateCloudHostedEngineModal = ({
|
||||
disclosure,
|
||||
onClickAddToPlan,
|
||||
}: {
|
||||
disclosure: UseDisclosureReturn;
|
||||
onClickAddToPlan: (tier: EngineTier) => void;
|
||||
}) => {
|
||||
const trackEvent = useTrack();
|
||||
const router = useRouter();
|
||||
const [modalState, setModalState] = useState<
|
||||
"selectTier" | "confirmStandard" | "confirmPremium"
|
||||
>("selectTier");
|
||||
|
||||
if (modalState === "confirmStandard" || modalState === "confirmPremium") {
|
||||
const tier: EngineTier =
|
||||
modalState === "confirmStandard" ? "STARTER" : "PREMIUM";
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={disclosure.isOpen}
|
||||
onClose={disclosure.onClose}
|
||||
isCentered
|
||||
size="lg"
|
||||
>
|
||||
<ModalOverlay />
|
||||
<ModalContent p={4} pt={8}>
|
||||
<ModalCloseButton />
|
||||
<ModalBody as={Flex} flexDir="column" gap={6}>
|
||||
<ChakraNextImage
|
||||
alt="Engine hero image"
|
||||
src={require("../../../public/assets/engine/empty-state-header.png")}
|
||||
w="fit-content"
|
||||
/>
|
||||
<Heading size="title.sm">
|
||||
Are you sure you want to deploy a{" "}
|
||||
{tier === "STARTER" ? "Standard" : "Premium"} Engine?
|
||||
</Heading>
|
||||
<Text>
|
||||
You will be charged ${MONTHLY_PRICE_USD[tier]} per month for the
|
||||
subscription.
|
||||
</Text>
|
||||
</ModalBody>
|
||||
<ModalFooter as={Flex} alignItems="flex-end" gap={3}>
|
||||
<Button onClick={() => setModalState("selectTier")} variant="ghost">
|
||||
Back
|
||||
</Button>
|
||||
<Button onClick={() => onClickAddToPlan(tier)} colorScheme="blue">
|
||||
Deploy now
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={disclosure.isOpen}
|
||||
onClose={disclosure.onClose}
|
||||
isCentered
|
||||
size="6xl"
|
||||
>
|
||||
<ModalOverlay />
|
||||
<ModalContent p={4} pb={8}>
|
||||
<ModalCloseButton />
|
||||
<ModalHeader>
|
||||
<Heading size="title.sm">Choose an Engine deployment</Heading>
|
||||
</ModalHeader>
|
||||
<ModalBody as={Flex} flexDir="column" gap={4}>
|
||||
<Text>
|
||||
Host Engine on thirdweb with no setup or maintenance required.
|
||||
</Text>
|
||||
<SimpleGrid columns={{ base: 1, lg: 3 }} gap={6}>
|
||||
<EngineTierCard
|
||||
tier="STARTER"
|
||||
onClick={() => setModalState("confirmStandard")}
|
||||
/>
|
||||
|
||||
<EngineTierCard
|
||||
tier="PREMIUM"
|
||||
isPrimaryCta
|
||||
onClick={() => setModalState("confirmPremium")}
|
||||
/>
|
||||
|
||||
<EngineTierCard
|
||||
tier="ENTERPRISE"
|
||||
previousTier="Premium Engine"
|
||||
onClick={() => {
|
||||
trackEvent({
|
||||
category: "engine",
|
||||
action: "click",
|
||||
label: "clicked-cloud-hosted",
|
||||
tier: "ENTERPRISE",
|
||||
});
|
||||
router.push("/contact-us");
|
||||
}}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,13 @@
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import {
|
||||
type EditEngineInstanceInput,
|
||||
type EngineInstance,
|
||||
@@ -7,52 +17,38 @@ import {
|
||||
useEngineRemoveCloudHosted,
|
||||
useEngineRemoveFromDashboard,
|
||||
} from "@3rdweb-sdk/react/hooks/useEngine";
|
||||
import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser";
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
Flex,
|
||||
FormControl,
|
||||
HStack,
|
||||
Input,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Stack,
|
||||
Textarea,
|
||||
Tooltip,
|
||||
type UseDisclosureReturn,
|
||||
useDisclosure,
|
||||
useToast,
|
||||
} from "@chakra-ui/react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { FormControl, Radio, RadioGroup, Stack } from "@chakra-ui/react";
|
||||
import { DialogDescription } from "@radix-ui/react-dialog";
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
import { TWTable } from "components/shared/TWTable";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import {
|
||||
type Dispatch,
|
||||
type ReactNode,
|
||||
type SetStateAction,
|
||||
useState,
|
||||
} from "react";
|
||||
CircleAlertIcon,
|
||||
SendIcon,
|
||||
Trash2Icon,
|
||||
TriangleAlertIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { type ReactNode, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { BiPencil } from "react-icons/bi";
|
||||
import { FiArrowRight, FiTrash } from "react-icons/fi";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import { Badge, Button, FormLabel, Heading, Text } from "tw-components";
|
||||
import { FiTrash } from "react-icons/fi";
|
||||
import { toast } from "sonner";
|
||||
import { FormLabel } from "tw-components";
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
} from "../../@/components/ui/alert";
|
||||
import { Button } from "../../@/components/ui/button";
|
||||
import { Input } from "../../@/components/ui/input";
|
||||
import { Textarea } from "../../@/components/ui/textarea";
|
||||
|
||||
interface EngineInstancesTableProps {
|
||||
instances: EngineInstance[];
|
||||
isLoading: boolean;
|
||||
isFetched: boolean;
|
||||
refetch: ReturnType<typeof useEngineInstances>["refetch"];
|
||||
setConnectedInstance: Dispatch<SetStateAction<EngineInstance | undefined>>;
|
||||
}
|
||||
|
||||
export const EngineInstancesTable: React.FC<EngineInstancesTableProps> = ({
|
||||
@@ -60,86 +56,15 @@ export const EngineInstancesTable: React.FC<EngineInstancesTableProps> = ({
|
||||
isLoading,
|
||||
isFetched,
|
||||
refetch,
|
||||
setConnectedInstance,
|
||||
}) => {
|
||||
const editDisclosure = useDisclosure();
|
||||
const removeDisclosure = useDisclosure();
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
const [isRemoveModalOpen, setIsRemoveModalOpen] = useState(false);
|
||||
const trackEvent = useTrack();
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const address = useActiveAccount()?.address;
|
||||
const toast = useToast();
|
||||
|
||||
const [instanceToUpdate, setInstanceToUpdate] = useState<
|
||||
EngineInstance | undefined
|
||||
>();
|
||||
|
||||
const { mutate: connectToInstance } = useMutation({
|
||||
mutationFn: async (instance: EngineInstance) => {
|
||||
// Make an authed request.
|
||||
// If it fails to fetch, the server is unreachable.
|
||||
// If it returns a 401, the user is not a valid admin.
|
||||
const res = await fetch(`${instance.url}auth/permissions/get-all`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
if (res.ok) {
|
||||
return instance;
|
||||
}
|
||||
if (res.status === 401) {
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
throw new Error(`Unexpected status code ${res.status}`);
|
||||
},
|
||||
onSuccess: (instance) => {
|
||||
trackEvent({
|
||||
category: "engine",
|
||||
action: "connect",
|
||||
label: "success",
|
||||
});
|
||||
setConnectedInstance(instance);
|
||||
},
|
||||
onError: (error, instance) => {
|
||||
const e = error as Error;
|
||||
if (e?.message === "Failed to fetch") {
|
||||
trackEvent({
|
||||
category: "engine",
|
||||
action: "connect",
|
||||
label: "unreachable",
|
||||
});
|
||||
toast({
|
||||
status: "error",
|
||||
description: `Unable to connect to ${instance.url}. Ensure that your Engine is publicly accessible.`,
|
||||
duration: null,
|
||||
isClosable: true,
|
||||
});
|
||||
} else if (e?.message.indexOf("Unauthorized") > -1) {
|
||||
trackEvent({
|
||||
category: "engine",
|
||||
action: "connect",
|
||||
label: "unauthorized",
|
||||
});
|
||||
toast({
|
||||
status: "error",
|
||||
title: "Unauthorized",
|
||||
description: `You are not an admin for this Engine instance. Contact the owner to add your wallet as an admin: ${address}`,
|
||||
duration: null,
|
||||
isClosable: true,
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
status: "error",
|
||||
description:
|
||||
"There was an unexpected error reaching your Engine instance. Try again or contact us if this issue persists.",
|
||||
duration: null,
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const columnHelper = createColumnHelper<EngineInstance>();
|
||||
const columns = [
|
||||
columnHelper.accessor("id", {
|
||||
@@ -150,83 +75,62 @@ export const EngineInstancesTable: React.FC<EngineInstancesTableProps> = ({
|
||||
let badge: ReactNode | undefined;
|
||||
if (status === "requested") {
|
||||
badge = (
|
||||
<Tooltip label="Deployment will begin shortly.">
|
||||
<Badge
|
||||
borderRadius="full"
|
||||
size="label.sm"
|
||||
variant="subtle"
|
||||
px={3}
|
||||
py={1.5}
|
||||
colorScheme="yellow"
|
||||
>
|
||||
Pending
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
<ToolTipLabel label="Deployment will begin shortly.">
|
||||
<div>
|
||||
<Badge variant="outline" className="gap-1.5">
|
||||
<Spinner className="size-3" />
|
||||
Pending
|
||||
</Badge>
|
||||
</div>
|
||||
</ToolTipLabel>
|
||||
);
|
||||
} else if (status === "deploying") {
|
||||
badge = (
|
||||
<Tooltip label="This step may take up to 30 minutes.">
|
||||
<Badge
|
||||
borderRadius="full"
|
||||
size="label.sm"
|
||||
variant="subtle"
|
||||
px={3}
|
||||
py={1.5}
|
||||
colorScheme="green"
|
||||
>
|
||||
Deploying
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
<ToolTipLabel label="This step may take up to 30 minutes.">
|
||||
<div>
|
||||
<Badge variant="default" className="gap-1.5">
|
||||
<Spinner className="size-3" />
|
||||
Deploying
|
||||
</Badge>
|
||||
</div>
|
||||
</ToolTipLabel>
|
||||
);
|
||||
} else if (status === "paymentFailed") {
|
||||
badge = (
|
||||
<Tooltip label="There was an error charging your payment method. Please contact [email protected].">
|
||||
<Badge
|
||||
borderRadius="full"
|
||||
size="label.sm"
|
||||
variant="subtle"
|
||||
px={3}
|
||||
py={1.5}
|
||||
colorScheme="red"
|
||||
>
|
||||
Payment Failed
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
<ToolTipLabel
|
||||
label={
|
||||
"There was an error charging your payment method. Please contact [email protected]."
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<Badge variant="destructive" className="gap-1.5">
|
||||
<CircleAlertIcon className="size-3" />
|
||||
Payment Failed
|
||||
</Badge>
|
||||
</div>
|
||||
</ToolTipLabel>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack py={2}>
|
||||
<div className="py-3">
|
||||
{badge ? (
|
||||
<HStack spacing={4}>
|
||||
<Text fontWeight="600" size="body.lg">
|
||||
{name}
|
||||
</Text>
|
||||
<div className="flex items-center gap-4 py-2">
|
||||
<p className="font-semibold text-lg"> {name} </p>
|
||||
{badge}
|
||||
</HStack>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const instance = instances.find((i) => i.id === id);
|
||||
if (instance) {
|
||||
connectToInstance(instance);
|
||||
}
|
||||
}}
|
||||
variant="link"
|
||||
colorScheme="blue"
|
||||
w="fit-content"
|
||||
rightIcon={<FiArrowRight />}
|
||||
justifyContent="flex-start"
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<Link
|
||||
href={`/dashboard/engine/${id}`}
|
||||
className="text-foreground flex text-lg font-semibold items-center before:absolute before:inset-0 before:bg-transparent"
|
||||
>
|
||||
{name}
|
||||
</Button>
|
||||
<Text size="body.sm" color="gray.700">
|
||||
{url}
|
||||
</Text>
|
||||
</>
|
||||
</Link>
|
||||
<p className="text-muted-foreground text-sm">{url}</p>
|
||||
</div>
|
||||
)}
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}),
|
||||
@@ -251,7 +155,7 @@ export const EngineInstancesTable: React.FC<EngineInstancesTableProps> = ({
|
||||
label: "open-modal",
|
||||
});
|
||||
setInstanceToUpdate(instance);
|
||||
editDisclosure.onOpen();
|
||||
setIsEditModalOpen(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -264,24 +168,28 @@ export const EngineInstancesTable: React.FC<EngineInstancesTableProps> = ({
|
||||
label: "open-modal",
|
||||
});
|
||||
setInstanceToUpdate(instance);
|
||||
removeDisclosure.onOpen();
|
||||
setIsRemoveModalOpen(true);
|
||||
},
|
||||
isDestructive: true,
|
||||
},
|
||||
]}
|
||||
bodyRowClassName="hover:bg-secondary relative"
|
||||
/>
|
||||
|
||||
{editDisclosure.isOpen && instanceToUpdate && (
|
||||
{instanceToUpdate && (
|
||||
<EditModal
|
||||
instance={instanceToUpdate}
|
||||
disclosure={editDisclosure}
|
||||
open={isEditModalOpen}
|
||||
onOpenChange={setIsEditModalOpen}
|
||||
refetch={refetch}
|
||||
/>
|
||||
)}
|
||||
{removeDisclosure.isOpen && instanceToUpdate && (
|
||||
|
||||
{instanceToUpdate && (
|
||||
<RemoveModal
|
||||
instance={instanceToUpdate}
|
||||
disclosure={removeDisclosure}
|
||||
onOpenChange={setIsRemoveModalOpen}
|
||||
open={isRemoveModalOpen}
|
||||
refetch={refetch}
|
||||
/>
|
||||
)}
|
||||
@@ -289,18 +197,14 @@ export const EngineInstancesTable: React.FC<EngineInstancesTableProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const EditModal = ({
|
||||
instance,
|
||||
disclosure,
|
||||
refetch,
|
||||
}: {
|
||||
disclosure: UseDisclosureReturn;
|
||||
const EditModal = (props: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
instance: EngineInstance;
|
||||
refetch: () => void;
|
||||
}) => {
|
||||
const toast = useToast();
|
||||
const { mutate: editInstance } = useEngineEditInstance();
|
||||
const { onClose } = disclosure;
|
||||
const editInstance = useEngineEditInstance();
|
||||
const { onOpenChange, instance, open, refetch } = props;
|
||||
|
||||
const form = useForm<EditEngineInstanceInput>({
|
||||
defaultValues: {
|
||||
@@ -308,242 +212,298 @@ const EditModal = ({
|
||||
name: instance.name,
|
||||
url: instance.url,
|
||||
},
|
||||
reValidateMode: "onChange",
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal isOpen onClose={onClose} isCentered size="lg">
|
||||
<ModalOverlay />
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="z-[10001]" dialogOverlayClassName="z-[10000]">
|
||||
<DialogHeader className="mb-3">
|
||||
<DialogTitle className="text-2xl font-semibold tracking-tight">
|
||||
Edit Engine Instance
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<ModalContent>
|
||||
<form
|
||||
onSubmit={form.handleSubmit((data) =>
|
||||
editInstance(data, {
|
||||
editInstance.mutate(data, {
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
status: "success",
|
||||
description: "Successfully updated this Engine.",
|
||||
});
|
||||
toast.success("Engine updated successfully");
|
||||
refetch();
|
||||
onClose();
|
||||
onOpenChange(false);
|
||||
},
|
||||
onError: () => {
|
||||
toast({
|
||||
status: "error",
|
||||
description: "Error updating this Engine.",
|
||||
});
|
||||
toast.error("Failed to update Engine");
|
||||
},
|
||||
}),
|
||||
)}
|
||||
>
|
||||
<ModalHeader>Edit Engine Instance</ModalHeader>
|
||||
<div className="flex flex-col gap-6">
|
||||
<FormControl isRequired>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Enter a descriptive label"
|
||||
autoFocus
|
||||
{...form.register("name", {
|
||||
required: true,
|
||||
})}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl isRequired>
|
||||
<FormLabel>URL</FormLabel>
|
||||
<Input
|
||||
type="url"
|
||||
placeholder="Enter your Engine URL"
|
||||
{...form.register("url", {
|
||||
required: true,
|
||||
})}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
|
||||
<ModalBody>
|
||||
<Stack spacing={4}>
|
||||
<FormControl>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Enter a descriptive label"
|
||||
autoFocus
|
||||
{...form.register("name")}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl isRequired>
|
||||
<FormLabel>URL</FormLabel>
|
||||
<Input
|
||||
type="url"
|
||||
placeholder="Enter your Engine URL"
|
||||
{...form.register("url")}
|
||||
/>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter as={Flex} gap={3}>
|
||||
<Button onClick={onClose} variant="ghost">
|
||||
<DialogFooter className="mt-10 gap-2">
|
||||
<Button onClick={() => onOpenChange(false)} variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" colorScheme="blue">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
className="gap-2"
|
||||
disabled={!form.formState.isDirty}
|
||||
>
|
||||
{editInstance.isLoading ? (
|
||||
<Spinner className="size-4" />
|
||||
) : (
|
||||
<SendIcon className="size-4" />
|
||||
)}
|
||||
Update
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const RemoveModal = ({
|
||||
instance,
|
||||
disclosure,
|
||||
refetch,
|
||||
}: {
|
||||
const RemoveModal = (props: {
|
||||
instance: EngineInstance;
|
||||
disclosure: UseDisclosureReturn;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
refetch: () => void;
|
||||
}) => {
|
||||
const toast = useToast();
|
||||
const { mutate: removeFromDashboard } = useEngineRemoveFromDashboard();
|
||||
const { mutate: removeCloudHosted } = useEngineRemoveCloudHosted();
|
||||
const { onClose } = disclosure;
|
||||
const { instance, open, onOpenChange, refetch } = props;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="z-[10001]" dialogOverlayClassName="z-[10000]">
|
||||
{instance.status === "paymentFailed" ||
|
||||
(instance.status === "active" && !instance.cloudDeployedAt) ? (
|
||||
<RemoveFromDashboardModalContent
|
||||
refetch={refetch}
|
||||
instance={instance}
|
||||
close={() => onOpenChange(false)}
|
||||
/>
|
||||
) : (
|
||||
<CancelSubscriptionModalContent
|
||||
refetch={refetch}
|
||||
instance={instance}
|
||||
close={() => onOpenChange(false)}
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
function RemoveFromDashboardModalContent(props: {
|
||||
refetch: () => void;
|
||||
instance: EngineInstance;
|
||||
close: () => void;
|
||||
}) {
|
||||
const { refetch, instance, close } = props;
|
||||
const removeFromDashboard = useEngineRemoveFromDashboard();
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl font-semibold tracking-tight mb-3">
|
||||
Remove Engine Instance
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-muted-foreground">
|
||||
<span className="block mb-2">
|
||||
Are you sure you want to remove{" "}
|
||||
<em className="not-italic font-semibold">"{instance.name}"</em> from
|
||||
your dashboard?
|
||||
</span>
|
||||
<span className="block">
|
||||
This action does not modify your Engine infrastructure. You can
|
||||
re-add it at any time.
|
||||
</span>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<DialogFooter className="mt-8 gap-1">
|
||||
<Button onClick={close} variant="outline">
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
removeFromDashboard.mutate(instance.id, {
|
||||
onSuccess: () => {
|
||||
toast.success("Removed an Engine instance from your dashboard");
|
||||
refetch();
|
||||
close();
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(
|
||||
"Error removing an Engine instance from your dashboard",
|
||||
);
|
||||
},
|
||||
});
|
||||
}}
|
||||
variant="destructive"
|
||||
className="gap-2"
|
||||
>
|
||||
{removeFromDashboard.isLoading ? (
|
||||
<Spinner className="size-4" />
|
||||
) : (
|
||||
<Trash2Icon className="size-4" />
|
||||
)}
|
||||
Remove
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function CancelSubscriptionModalContent(props: {
|
||||
refetch: () => void;
|
||||
instance: EngineInstance;
|
||||
close: () => void;
|
||||
}) {
|
||||
const { refetch, instance, close } = props;
|
||||
const removeCloudHosted = useEngineRemoveCloudHosted();
|
||||
|
||||
const form = useForm<RemoveCloudHostedInput>({
|
||||
defaultValues: {
|
||||
instanceId: instance.id,
|
||||
},
|
||||
reValidateMode: "onChange",
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal isOpen onClose={onClose} isCentered size="lg">
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
{instance.status === "paymentFailed" ||
|
||||
(instance.status === "active" && !instance.cloudDeployedAt) ? (
|
||||
<>
|
||||
<ModalHeader>Remove Engine Instance</ModalHeader>
|
||||
<div>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl font-semibold tracking-tight mb-1">
|
||||
Cancel Engine Subscription
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-muted-foreground">
|
||||
Complete this form to request to cancel your Engine subscription. This
|
||||
may take up to 2 business days.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<ModalBody as={Flex} flexDir="column" gap={2}>
|
||||
<Text>
|
||||
Are you sure you want to remove{" "}
|
||||
<strong>{instance?.name}</strong> from your dashboard?
|
||||
</Text>
|
||||
<Text>
|
||||
This action does not modify your Engine infrastructure. You can
|
||||
re-add it at any time.
|
||||
</Text>
|
||||
</ModalBody>
|
||||
<div className="h-3" />
|
||||
|
||||
<ModalFooter as={Flex} gap={3}>
|
||||
<Button onClick={onClose} variant="ghost">
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
removeFromDashboard(instance.id, {
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
status: "success",
|
||||
description:
|
||||
"Removed an Engine instance from your dashboard.",
|
||||
});
|
||||
refetch();
|
||||
onClose();
|
||||
},
|
||||
onError: () => {
|
||||
toast({
|
||||
status: "error",
|
||||
description:
|
||||
"Error removing an Engine instance from your dashboard.",
|
||||
});
|
||||
},
|
||||
});
|
||||
}}
|
||||
colorScheme="red"
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</>
|
||||
) : (
|
||||
<form
|
||||
onSubmit={form.handleSubmit((data) =>
|
||||
removeCloudHosted(data, {
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
status: "success",
|
||||
description:
|
||||
"Submitted a request to cancel your Engine subscription. This may take up to 2 business days.",
|
||||
});
|
||||
refetch();
|
||||
onClose();
|
||||
<Alert variant="destructive">
|
||||
<TriangleAlertIcon className="size-5 !text-destructive-text" />
|
||||
<AlertTitle>This action is irreversible!</AlertTitle>
|
||||
<AlertDescription>
|
||||
You will no longer be able to access this Engine's local backend
|
||||
wallets. <strong>Any remaining mainnet funds will be lost.</strong>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="h-5" />
|
||||
|
||||
<form
|
||||
onSubmit={form.handleSubmit((data) =>
|
||||
removeCloudHosted.mutate(data, {
|
||||
onSuccess: () => {
|
||||
toast.success(
|
||||
"Submitted a request to cancel your Engine subscription. This may take up to 2 business days.",
|
||||
{
|
||||
dismissible: true,
|
||||
duration: 10000,
|
||||
},
|
||||
onError: () => {
|
||||
toast({
|
||||
status: "error",
|
||||
description:
|
||||
"Error requesting to cancel your Engine subscription.",
|
||||
});
|
||||
},
|
||||
}),
|
||||
)}
|
||||
>
|
||||
<ModalHeader>Cancel Engine Subscription</ModalHeader>
|
||||
);
|
||||
|
||||
<ModalBody as={Stack} gap={4}>
|
||||
<Text>
|
||||
Complete this form to request to cancel your Engine
|
||||
subscription. This may take up to 2 business days.
|
||||
</Text>
|
||||
|
||||
{/* Form */}
|
||||
<FormControl>
|
||||
<FormLabel>
|
||||
Please share any feedback to help us improve
|
||||
</FormLabel>
|
||||
<RadioGroup>
|
||||
<Stack>
|
||||
<Radio
|
||||
value="USING_SELF_HOSTED"
|
||||
{...form.register("reason", { required: true })}
|
||||
>
|
||||
<Text>Migrating to self-hosted</Text>
|
||||
</Radio>
|
||||
<Radio
|
||||
value="TOO_EXPENSIVE"
|
||||
{...form.register("reason", { required: true })}
|
||||
>
|
||||
<Text>Too expensive</Text>
|
||||
</Radio>
|
||||
<Radio
|
||||
value="MISSING_FEATURES"
|
||||
{...form.register("reason", { required: true })}
|
||||
>
|
||||
<Text>Missing features</Text>
|
||||
</Radio>
|
||||
<Radio
|
||||
value="OTHER"
|
||||
{...form.register("reason", { required: true })}
|
||||
>
|
||||
<Text>Other</Text>
|
||||
</Radio>
|
||||
</Stack>
|
||||
</RadioGroup>
|
||||
<Textarea
|
||||
placeholder="Provide additional feedback"
|
||||
mt={2}
|
||||
{...form.register("feedback")}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<Alert status="warning" variant="left-accent">
|
||||
<Flex direction="column" gap={2}>
|
||||
<Heading as={AlertTitle} size="label.md">
|
||||
This action is irreversible!
|
||||
</Heading>
|
||||
<Text as={AlertDescription} size="body.md">
|
||||
You will no longer be able to access this Engine's
|
||||
local backend wallets.{" "}
|
||||
<strong>Any remaining mainnet funds will be lost.</strong>
|
||||
</Text>
|
||||
</Flex>
|
||||
</Alert>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter as={Flex} gap={3}>
|
||||
<Button onClick={onClose} variant="ghost">
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
colorScheme="primary"
|
||||
isDisabled={!form.formState.isValid}
|
||||
>
|
||||
Request to cancel
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
refetch();
|
||||
close();
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(
|
||||
"Error requesting to cancel your Engine subscription",
|
||||
);
|
||||
},
|
||||
}),
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
>
|
||||
{/* Form */}
|
||||
<FormControl isRequired>
|
||||
<FormLabel className="!text-base">
|
||||
Please share any feedback to help us improve
|
||||
</FormLabel>
|
||||
<RadioGroup>
|
||||
<Stack>
|
||||
<Radio
|
||||
value="USING_SELF_HOSTED"
|
||||
{...form.register("reason", { required: true })}
|
||||
>
|
||||
<span className="text-sm"> Migrating to self-hosted </span>
|
||||
</Radio>
|
||||
<Radio
|
||||
value="TOO_EXPENSIVE"
|
||||
{...form.register("reason", { required: true })}
|
||||
>
|
||||
<span className="text-sm"> Too expensive </span>
|
||||
</Radio>
|
||||
<Radio
|
||||
value="MISSING_FEATURES"
|
||||
{...form.register("reason", { required: true })}
|
||||
>
|
||||
<span className="text-sm"> Missing features </span>
|
||||
</Radio>
|
||||
<Radio
|
||||
value="OTHER"
|
||||
{...form.register("reason", { required: true })}
|
||||
>
|
||||
<span className="text-sm"> Other </span>
|
||||
</Radio>
|
||||
</Stack>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
|
||||
<div className="h-2" />
|
||||
|
||||
<Textarea
|
||||
className="mt-3"
|
||||
placeholder="Provide additional feedback"
|
||||
{...form.register("feedback")}
|
||||
/>
|
||||
|
||||
<div className="h-8" />
|
||||
|
||||
<DialogFooter className="gap-2">
|
||||
<Button onClick={close} variant="outline">
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="destructive"
|
||||
disabled={!form.formState.isValid}
|
||||
className="gap-2"
|
||||
>
|
||||
{removeCloudHosted.isLoading ? (
|
||||
<Spinner className="size-4" />
|
||||
) : (
|
||||
<SendIcon className="size-4" />
|
||||
)}
|
||||
Request to cancel
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,164 +1,188 @@
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { useAccount } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import {
|
||||
type EngineInstance,
|
||||
useEngineInstances,
|
||||
} from "@3rdweb-sdk/react/hooks/useEngine";
|
||||
import {
|
||||
Center,
|
||||
Divider,
|
||||
Flex,
|
||||
Icon,
|
||||
ListItem,
|
||||
Stack,
|
||||
UnorderedList,
|
||||
} from "@chakra-ui/react";
|
||||
import { ChakraNextImage } from "components/Image";
|
||||
import { useEngineInstances } from "@3rdweb-sdk/react/hooks/useEngine";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { VscBook } from "react-icons/vsc";
|
||||
import invariant from "tiny-invariant";
|
||||
import { Button, Card, Heading, Link, Text } from "tw-components";
|
||||
import { CreateEngineInstanceButton } from "./create-engine-instance";
|
||||
import {
|
||||
CloudDownloadIcon,
|
||||
ExternalLinkIcon,
|
||||
PlusIcon,
|
||||
RocketIcon,
|
||||
} from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { UnorderedList } from "../../@/components/ui/List/List";
|
||||
import { Button } from "../../@/components/ui/button";
|
||||
import { EngineInstancesTable } from "./engine-instances-table";
|
||||
import { ImportEngineInstanceButton } from "./import-engine-instance";
|
||||
|
||||
interface EngineInstancesListProps {
|
||||
setConnectedInstance: Dispatch<SetStateAction<EngineInstance | undefined>>;
|
||||
}
|
||||
|
||||
export const EngineInstancesList = ({
|
||||
setConnectedInstance,
|
||||
}: EngineInstancesListProps) => {
|
||||
invariant(
|
||||
process.env.NEXT_PUBLIC_DEMO_ENGINE_URL,
|
||||
"missing NEXT_PUBLIC_DEMO_ENGINE_URL",
|
||||
);
|
||||
const NEXT_PUBLIC_DEMO_ENGINE_URL = process.env.NEXT_PUBLIC_DEMO_ENGINE_URL;
|
||||
|
||||
const { data } = useAccount();
|
||||
export const EngineInstancesList = () => {
|
||||
const instancesQuery = useEngineInstances();
|
||||
const instances = instancesQuery.data ?? [];
|
||||
const trackEvent = useTrack();
|
||||
|
||||
if (instancesQuery.isLoading) {
|
||||
return (
|
||||
<div className="grid place-items-center">
|
||||
<div className="flex justify-center items-center h-[300px]">
|
||||
<Spinner className="size-14" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Stack spacing={8}>
|
||||
{instances.length === 0 ? (
|
||||
<Center>
|
||||
<Stack spacing={8}>
|
||||
<ChakraNextImage
|
||||
alt="Engine hero image"
|
||||
src={require("../../../public/assets/engine/empty-state-header.png")}
|
||||
maxW={650}
|
||||
/>
|
||||
<Heading size="title.lg" as="h1">
|
||||
Your scalable web3 backend server
|
||||
</Heading>
|
||||
|
||||
<UnorderedList color="gray.600">
|
||||
<ListItem>
|
||||
Read, write, and deploy contracts at production scale
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
Reliably parallelize and retry transactions with gas & nonce
|
||||
management
|
||||
</ListItem>
|
||||
<ListItem>Securely manage backend wallets</ListItem>
|
||||
<ListItem>
|
||||
Built-in support for account abstraction, relayers, and more
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
if (instances.length === 0) {
|
||||
return (
|
||||
<div className="max-w-[600px] mx-auto">
|
||||
<Image
|
||||
alt="Engine hero image"
|
||||
src={require("../../../public/assets/engine/empty-state-header.png")}
|
||||
/>
|
||||
|
||||
<Flex gap={4}>
|
||||
<CreateEngineInstanceButton
|
||||
ctaText="Get Started"
|
||||
refetch={instancesQuery.refetch}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (!data) return;
|
||||
<div className="h-6" />
|
||||
|
||||
trackEvent({
|
||||
category: "engine",
|
||||
action: "try-demo",
|
||||
label: "clicked-try-demo",
|
||||
});
|
||||
<h1 className="text-2xl md:text-3xl font-bold tracking-tighter">
|
||||
Your scalable web3 backend server
|
||||
</h1>
|
||||
|
||||
// Need to hardcode the demo engine instance details
|
||||
// but using the signed-in user account id
|
||||
setConnectedInstance({
|
||||
id: "sandbox",
|
||||
url: NEXT_PUBLIC_DEMO_ENGINE_URL,
|
||||
name: "Demo Engine",
|
||||
status: "active",
|
||||
lastAccessedAt: new Date().toISOString(),
|
||||
accountId: data.id,
|
||||
});
|
||||
}}
|
||||
variant="outline"
|
||||
px={6}
|
||||
>
|
||||
🚀 Try Demo
|
||||
</Button>
|
||||
</Flex>
|
||||
<Flex direction="column" gap={2}>
|
||||
<Divider my="2" />
|
||||
<Text>Already have an Engine Instance?</Text>
|
||||
<ImportEngineInstanceButton refetch={instancesQuery.refetch} />
|
||||
</Flex>
|
||||
</Stack>
|
||||
</Center>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex flex-col md:flex-row gap-4 justify-between md:items-center">
|
||||
<h1 className="text-4xl font-bold">Engine</h1>
|
||||
<div className="flex flex-row gap-2">
|
||||
<ImportEngineInstanceButton refetch={instancesQuery.refetch} />
|
||||
<CreateEngineInstanceButton
|
||||
ctaText="Create Engine Instance"
|
||||
refetch={instancesQuery.refetch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-3" />
|
||||
|
||||
<Stack spacing={4}>
|
||||
<EngineInstancesTable
|
||||
instances={instancesQuery.data ?? ([] as EngineInstance[])}
|
||||
isFetched={instancesQuery.isFetched}
|
||||
isLoading={instancesQuery.isLoading}
|
||||
refetch={instancesQuery.refetch}
|
||||
setConnectedInstance={setConnectedInstance}
|
||||
/>
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
<UnorderedList>
|
||||
<li>Read, write, and deploy contracts at production scale</li>
|
||||
<li>
|
||||
Reliably parallelize and retry transactions with gas & nonce
|
||||
management
|
||||
</li>
|
||||
<li>Securely manage backend wallets</li>
|
||||
<li>Built-in support for account abstraction, relayers, and more</li>
|
||||
</UnorderedList>
|
||||
|
||||
<Divider />
|
||||
<div className="h-6" />
|
||||
|
||||
<Card p={8}>
|
||||
<Stack>
|
||||
<Flex gap={2} align="center">
|
||||
<Icon as={VscBook} />
|
||||
<Heading size="title.xs">Learn more about Engine</Heading>
|
||||
</Flex>
|
||||
<Text>Dive into features and integration guides.</Text>
|
||||
<Link
|
||||
href="https://portal.thirdweb.com/engine"
|
||||
isExternal
|
||||
color="blue.500"
|
||||
fontSize="small"
|
||||
<div className="flex gap-3">
|
||||
<CreateEngineLink label="Get Started" />
|
||||
|
||||
<Button
|
||||
asChild
|
||||
onClick={() => {
|
||||
trackEvent({
|
||||
category: "engine",
|
||||
action: "try-demo",
|
||||
label: "clicked-try-demo",
|
||||
});
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
View Docs →
|
||||
</Link>
|
||||
</Stack>
|
||||
</Card>
|
||||
</Stack>
|
||||
<Link href={"/dashboard/engine/sandbox"} className="gap-2">
|
||||
Try Demo
|
||||
<RocketIcon className="size-4 text-muted-foreground" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="h-10" />
|
||||
|
||||
<div className="flex items-center gap-4 justify-between rounded-lg border border-border p-6 bg-secondary">
|
||||
<p className="text-lg font-semibold tracking-tight">
|
||||
Already have an Engine Instance?
|
||||
</p>
|
||||
|
||||
<ImportEngineLink label="Import" />
|
||||
</div>
|
||||
|
||||
<div className="h-20" />
|
||||
<LearnMoreCard />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col md:flex-row gap-4 justify-between md:items-center">
|
||||
<h1 className="text-4xl font-bold">Engine</h1>
|
||||
<div className="flex flex-row gap-2">
|
||||
<ImportEngineLink label="Import" />
|
||||
<CreateEngineLink label="Create Engine Instance" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-6" />
|
||||
|
||||
<EngineInstancesTable
|
||||
instances={instances}
|
||||
isFetched={instancesQuery.isFetched}
|
||||
isLoading={instancesQuery.isLoading}
|
||||
refetch={instancesQuery.refetch}
|
||||
/>
|
||||
|
||||
<div className="h-10" />
|
||||
<LearnMoreCard />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function LearnMoreCard() {
|
||||
return (
|
||||
<div className="p-4 border border-border rounded-lg relative hover:bg-secondary">
|
||||
<h3 className="font-semibold tracking-tight mb-1">
|
||||
<Link
|
||||
href="https://portal.thirdweb.com/engine"
|
||||
target="_blank"
|
||||
className="before:content before:absolute before:inset-0"
|
||||
>
|
||||
Learn more about Engine
|
||||
</Link>
|
||||
</h3>
|
||||
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Dive into features and integration guides.
|
||||
</p>
|
||||
|
||||
<ExternalLinkIcon className="size-4 text-muted-foreground absolute right-4 top-4" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CreateEngineLink(props: {
|
||||
label: string;
|
||||
}) {
|
||||
const trackEvent = useTrack();
|
||||
|
||||
return (
|
||||
<Button
|
||||
asChild
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
trackEvent({
|
||||
category: "engine",
|
||||
action: "click",
|
||||
label: "add-engine-instance",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Link href={"/dashboard/engine/create"} className="gap-2">
|
||||
<PlusIcon className="size-4" />
|
||||
{props.label}
|
||||
</Link>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function ImportEngineLink(props: {
|
||||
label: string;
|
||||
}) {
|
||||
const trackEvent = useTrack();
|
||||
|
||||
return (
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
trackEvent({
|
||||
category: "engine",
|
||||
action: "import",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Link href={"/dashboard/engine/import"} className="gap-2">
|
||||
<CloudDownloadIcon className="size-4" />
|
||||
{props.label}
|
||||
</Link>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
import type { EngineInstance } from "@3rdweb-sdk/react/hooks/useEngine";
|
||||
import { Flex, Stack } from "@chakra-ui/react";
|
||||
import { SidebarNav } from "core-ui/sidebar/nav";
|
||||
import type { Route } from "core-ui/sidebar/types";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import {
|
||||
type Dispatch,
|
||||
type SetStateAction,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { FiArrowLeft } from "react-icons/fi";
|
||||
import { Button, Heading, Text } from "tw-components";
|
||||
import { EngineVersionBadge } from "./badges/version";
|
||||
import { EngineConfiguration } from "./configuration/engine-configuration";
|
||||
import { EngineWebhooks } from "./configuration/engine-webhooks";
|
||||
import { EngineContractSubscriptions } from "./contract-subscription/engine-contract-subscription";
|
||||
import { EngineExplorer } from "./explorer/engine-explorer";
|
||||
import { EngineOverview } from "./overview/engine-overview";
|
||||
import { EngineAccessTokens } from "./permissions/engine-access-tokens";
|
||||
import { EngineAdmins } from "./permissions/engine-admins";
|
||||
import { EngineRelayer } from "./relayer/engine-relayer";
|
||||
import { EngineSystemMetrics } from "./system-metrics";
|
||||
|
||||
interface EngineNavigationProps {
|
||||
instance: EngineInstance;
|
||||
setConnectedInstance: Dispatch<SetStateAction<EngineInstance | undefined>>;
|
||||
}
|
||||
|
||||
export const EngineNavigation: React.FC<EngineNavigationProps> = ({
|
||||
instance,
|
||||
setConnectedInstance,
|
||||
}) => {
|
||||
const trackEvent = useTrack();
|
||||
|
||||
const onClickBack = () => {
|
||||
setConnectedInstance(undefined);
|
||||
};
|
||||
|
||||
const [activePage, setActivePage] = useState<string>("overview");
|
||||
|
||||
const handleClick = useCallback(
|
||||
(id: string) => {
|
||||
trackEvent({
|
||||
category: "engine",
|
||||
action: "navigate-tab",
|
||||
label: id,
|
||||
url: instance.url,
|
||||
});
|
||||
setActivePage(id);
|
||||
},
|
||||
[instance.url, trackEvent],
|
||||
);
|
||||
|
||||
const links = useMemo(
|
||||
() =>
|
||||
[
|
||||
{
|
||||
path: "/dashboard/engine/overview",
|
||||
title: "Overview",
|
||||
name: "overview",
|
||||
onClick: () => handleClick("overview"),
|
||||
},
|
||||
{
|
||||
path: "/dashboard/engine/explorer",
|
||||
title: "Explorer",
|
||||
name: "explorer",
|
||||
onClick: () => handleClick("explorer"),
|
||||
},
|
||||
{
|
||||
path: "/dashboard/engine/relayers",
|
||||
title: "Relayers",
|
||||
name: "relayers",
|
||||
onClick: () => handleClick("relayers"),
|
||||
},
|
||||
{
|
||||
path: "/dashboard/engine/contract-subscriptions",
|
||||
title: "Contract Subscriptions",
|
||||
name: "contract-subscriptions",
|
||||
onClick: () => handleClick("contract-subscriptions"),
|
||||
},
|
||||
{
|
||||
path: "/dashboard/engine/admins",
|
||||
title: "Admins",
|
||||
name: "admins",
|
||||
onClick: () => handleClick("admins"),
|
||||
},
|
||||
{
|
||||
path: "/dashboard/engine/access-tokens",
|
||||
title: "Access Tokens",
|
||||
name: "access-tokens",
|
||||
onClick: () => handleClick("access-tokens"),
|
||||
},
|
||||
{
|
||||
path: "/dashboard/engine/webhooks",
|
||||
title: "Webhooks",
|
||||
name: "webhooks",
|
||||
onClick: () => handleClick("webhooks"),
|
||||
},
|
||||
{
|
||||
path: "/dashboard/engine/configuration",
|
||||
title: "Configuration",
|
||||
name: "configuration",
|
||||
onClick: () => handleClick("configuration"),
|
||||
},
|
||||
{
|
||||
path: "/dashboard/engine/metrics",
|
||||
title: "Metrics",
|
||||
name: "metrics",
|
||||
onClick: () => handleClick("metrics"),
|
||||
},
|
||||
] satisfies Route[],
|
||||
[handleClick],
|
||||
);
|
||||
|
||||
const activeComponent = useMemo(() => {
|
||||
const tabs = {
|
||||
overview: <EngineOverview instanceUrl={instance.url} />,
|
||||
explorer: <EngineExplorer instanceUrl={instance.url} />,
|
||||
relayers: <EngineRelayer instanceUrl={instance.url} />,
|
||||
"contract-subscriptions": (
|
||||
<EngineContractSubscriptions instanceUrl={instance.url} />
|
||||
),
|
||||
admins: <EngineAdmins instanceUrl={instance.url} />,
|
||||
"access-tokens": <EngineAccessTokens instanceUrl={instance.url} />,
|
||||
webhooks: <EngineWebhooks instanceUrl={instance.url} />,
|
||||
configuration: <EngineConfiguration instance={instance} />,
|
||||
metrics: <EngineSystemMetrics instance={instance} />,
|
||||
};
|
||||
return tabs[activePage as keyof typeof tabs];
|
||||
}, [instance, activePage]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidebarNav links={links} activePage={activePage} title="Engine" />
|
||||
<Stack spacing={4}>
|
||||
<Button
|
||||
onClick={onClickBack}
|
||||
variant="link"
|
||||
leftIcon={<FiArrowLeft />}
|
||||
w="fit-content"
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<Stack>
|
||||
<Heading size="title.lg" as="h1" isTruncated>
|
||||
{instance.name}
|
||||
</Heading>
|
||||
|
||||
<Flex gap={3} alignItems="center">
|
||||
{!instance.name.startsWith("https://") && (
|
||||
<Text color="gray.600">{instance.url}</Text>
|
||||
)}
|
||||
<EngineVersionBadge instance={instance} />
|
||||
</Flex>
|
||||
</Stack>
|
||||
|
||||
{activeComponent}
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,186 +0,0 @@
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertIcon,
|
||||
Flex,
|
||||
FormControl,
|
||||
Input,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
Stack,
|
||||
type UseDisclosureReturn,
|
||||
useDisclosure,
|
||||
useToast,
|
||||
} from "@chakra-ui/react";
|
||||
import { THIRDWEB_API_HOST } from "constants/urls";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useSingleQueryParam } from "hooks/useQueryParam";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Button, FormLabel, Text, TrackedLink } from "tw-components";
|
||||
|
||||
interface ImportEngineInstanceButtonProps {
|
||||
refetch: () => void;
|
||||
}
|
||||
|
||||
export const ImportEngineInstanceButton = ({
|
||||
refetch,
|
||||
}: ImportEngineInstanceButtonProps) => {
|
||||
const hasImportUrl = !!useSingleQueryParam("importUrl");
|
||||
|
||||
const disclosure = useDisclosure({
|
||||
defaultIsOpen: !!hasImportUrl,
|
||||
});
|
||||
const trackEvent = useTrack();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
trackEvent({
|
||||
category: "engine",
|
||||
action: "import",
|
||||
label: "open-modal",
|
||||
});
|
||||
disclosure.onOpen();
|
||||
}}
|
||||
variant="outline"
|
||||
px={6}
|
||||
width="fit-content"
|
||||
>
|
||||
Import
|
||||
</Button>
|
||||
|
||||
{disclosure.isOpen && (
|
||||
<ModalImportEngine
|
||||
disclosure={disclosure}
|
||||
onSuccess={() => refetch()}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface ImportEngineInput {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
const ModalImportEngine = ({
|
||||
disclosure,
|
||||
onSuccess,
|
||||
}: {
|
||||
disclosure: UseDisclosureReturn;
|
||||
onSuccess: () => void;
|
||||
}) => {
|
||||
const toast = useToast();
|
||||
const defaultUrl = useSingleQueryParam("importUrl");
|
||||
|
||||
const form = useForm<ImportEngineInput>({
|
||||
defaultValues: {
|
||||
name: "My Engine Instance",
|
||||
url: defaultUrl ? decodeURIComponent(defaultUrl) : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async (data: ImportEngineInput) => {
|
||||
try {
|
||||
// Instance URLs should end with a /.
|
||||
const url = data.url.endsWith("/") ? data.url : `${data.url}/`;
|
||||
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/engine`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: data.name,
|
||||
url,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`Unexpected status ${res.status}`);
|
||||
}
|
||||
|
||||
disclosure.onClose();
|
||||
onSuccess();
|
||||
} catch (e) {
|
||||
toast({
|
||||
status: "error",
|
||||
description:
|
||||
"Error importing Engine. Please check if the details are correct.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={disclosure.isOpen}
|
||||
onClose={disclosure.onClose}
|
||||
isCentered
|
||||
size="lg"
|
||||
>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalCloseButton />
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<ModalHeader>Import Engine Instance</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Stack spacing={4}>
|
||||
<Text>
|
||||
Import an Engine instance hosted on your infrastructure.
|
||||
<br />
|
||||
<TrackedLink
|
||||
href="https://portal.thirdweb.com/infrastructure/engine/get-started"
|
||||
isExternal
|
||||
category="engine"
|
||||
label="clicked-self-host-instructions"
|
||||
color="blue.500"
|
||||
>
|
||||
Get help setting up Engine for free.
|
||||
</TrackedLink>
|
||||
</Text>
|
||||
<FormControl>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Enter a descriptive label"
|
||||
autoFocus
|
||||
{...form.register("name")}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl isRequired>
|
||||
<FormLabel>URL</FormLabel>
|
||||
<Input
|
||||
type="url"
|
||||
placeholder="Enter your Engine URL"
|
||||
{...form.register("url")}
|
||||
/>
|
||||
</FormControl>
|
||||
<Alert status="warning" borderRadius="m">
|
||||
<AlertIcon />
|
||||
<AlertDescription>
|
||||
Do not import a URL you do not recognize.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</Stack>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter as={Flex} gap={3}>
|
||||
<Button onClick={disclosure.onClose} variant="ghost">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" colorScheme="primary">
|
||||
Import
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -18,6 +18,7 @@ import { formatDistanceToNowStrict } from "date-fns/formatDistanceToNowStrict";
|
||||
import { useAllChainsData } from "hooks/chains/allChains";
|
||||
import { useState } from "react";
|
||||
import { FiArrowLeft, FiArrowRight, FiInfo } from "react-icons/fi";
|
||||
import { toTokens } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import {
|
||||
Badge,
|
||||
@@ -311,6 +312,8 @@ const TransactionDetailsDrawer = ({
|
||||
|
||||
const chain: ChainMetadata | undefined =
|
||||
chainIdToChainRecord[Number.parseInt(transaction.chainId)];
|
||||
const decimals = chain.nativeCurrency.decimals || 18;
|
||||
const symbol = chain.nativeCurrency.symbol || "ETH";
|
||||
const explorer = chain?.explorers?.[0];
|
||||
|
||||
const status = statusDetails[transaction.status as EngineStatus];
|
||||
@@ -320,12 +323,15 @@ const TransactionDetailsDrawer = ({
|
||||
: transaction.functionName ?? null;
|
||||
|
||||
let txFeeDisplay = "N/A";
|
||||
if (transaction.gasLimit && transaction.gasPrice) {
|
||||
const txFee =
|
||||
(Number.parseFloat(transaction.gasLimit) *
|
||||
Number.parseFloat(transaction.gasPrice)) /
|
||||
10 ** (chain?.nativeCurrency.decimals || 18);
|
||||
txFeeDisplay = `${txFee} ${chain?.nativeCurrency.symbol || "ETH"}`;
|
||||
if (transaction.gasLimit) {
|
||||
const gasPrice =
|
||||
transaction.effectiveGasPrice ||
|
||||
transaction.gasPrice ||
|
||||
transaction.maxFeePerGas;
|
||||
if (gasPrice) {
|
||||
const txFeeWei = BigInt(transaction.gasLimit) * BigInt(gasPrice);
|
||||
txFeeDisplay = `${toTokens(txFeeWei, decimals)} ${symbol}`;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -397,7 +403,9 @@ const TransactionDetailsDrawer = ({
|
||||
)}
|
||||
|
||||
<FormControl>
|
||||
<FormLabel>From Address</FormLabel>
|
||||
<FormLabel>
|
||||
{transaction.accountAddress ? "Signer Address" : "From Address"}
|
||||
</FormLabel>
|
||||
<LinkButton
|
||||
variant="ghost"
|
||||
isExternal
|
||||
@@ -412,8 +420,31 @@ const TransactionDetailsDrawer = ({
|
||||
</LinkButton>
|
||||
</FormControl>
|
||||
|
||||
{transaction.accountAddress && (
|
||||
<FormControl>
|
||||
<FormLabel>Account Address</FormLabel>
|
||||
<LinkButton
|
||||
variant="ghost"
|
||||
isExternal
|
||||
size="xs"
|
||||
href={
|
||||
explorer
|
||||
? `${explorer.url}/address/${transaction.accountAddress}`
|
||||
: "#"
|
||||
}
|
||||
>
|
||||
<Text fontFamily="mono">{transaction.accountAddress}</Text>
|
||||
</LinkButton>
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||
<FormControl>
|
||||
<FormLabel>To Address</FormLabel>
|
||||
{/* The "to" address is usually a contract except for native token transfers. */}
|
||||
<FormLabel>
|
||||
{functionCalled === "transfer"
|
||||
? "Recipient Address"
|
||||
: "Contract Address"}
|
||||
</FormLabel>
|
||||
<LinkButton
|
||||
variant="ghost"
|
||||
isExternal
|
||||
@@ -456,6 +487,24 @@ const TransactionDetailsDrawer = ({
|
||||
|
||||
{/* On-chain details */}
|
||||
|
||||
<FormControl>
|
||||
<Flex>
|
||||
<FormLabel>Value</FormLabel>
|
||||
<Tooltip
|
||||
label={`The amount of ${symbol} sent to the "To" .`}
|
||||
shouldWrapChildren
|
||||
>
|
||||
<FiInfo />
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
<Text>
|
||||
{transaction.value
|
||||
? toTokens(BigInt(transaction.value), decimals)
|
||||
: 0}{" "}
|
||||
{symbol}
|
||||
</Text>
|
||||
</FormControl>
|
||||
|
||||
{transaction.transactionHash && (
|
||||
<>
|
||||
<FormControl>
|
||||
@@ -528,6 +577,25 @@ const TransactionDetailsDrawer = ({
|
||||
<Text>{Number(transaction.gasPrice).toLocaleString()}</Text>
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||
{transaction.blockNumber && (
|
||||
<FormControl>
|
||||
<FormLabel>Block</FormLabel>
|
||||
<LinkButton
|
||||
variant="ghost"
|
||||
isExternal
|
||||
size="xs"
|
||||
href={
|
||||
explorer
|
||||
? `${explorer.url}/block/${transaction.blockNumber}`
|
||||
: "#"
|
||||
}
|
||||
maxW="100%"
|
||||
>
|
||||
<Text>{transaction.blockNumber}</Text>
|
||||
</LinkButton>
|
||||
</FormControl>
|
||||
)}
|
||||
</Stack>
|
||||
</Collapse>
|
||||
|
||||
|
||||
@@ -21,13 +21,12 @@ import {
|
||||
useDisclosure,
|
||||
} from "@chakra-ui/react";
|
||||
import { NetworkDropdown } from "components/contract-components/contract-publish-form/NetworkDropdown";
|
||||
import { isAddress } from "ethers/lib/utils";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useAllChainsData } from "hooks/chains/allChains";
|
||||
import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { AiOutlinePlusCircle } from "react-icons/ai";
|
||||
import { shortenAddress } from "thirdweb/utils";
|
||||
import { isAddress, shortenAddress } from "thirdweb/utils";
|
||||
import { Button, FormHelperText, FormLabel } from "tw-components";
|
||||
|
||||
interface AddRelayerButtonProps {
|
||||
|
||||
@@ -36,7 +36,7 @@ export const EngineSystemMetrics: React.FC<EngineStatusProps> = ({
|
||||
<Text>
|
||||
Upgrade to a{" "}
|
||||
<TrackedLink
|
||||
href="/dashboard/engine?requestCloudHosted"
|
||||
href="/dashboard/engine/create"
|
||||
isExternal
|
||||
color="blue.500"
|
||||
category="engine"
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useLoggedInUser } from "../../@3rdweb-sdk/react/hooks/useLoggedInUser";
|
||||
|
||||
// If it fails to fetch, the server is unreachable.
|
||||
// If it returns a 401, the user is not a valid admin.
|
||||
|
||||
export function useHasEnginePermission(props: {
|
||||
instanceUrl: string;
|
||||
}) {
|
||||
const instanceUrl = props.instanceUrl;
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
|
||||
return useQuery({
|
||||
queryKey: ["auth/permissions/get-all", instanceUrl, token],
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`${instanceUrl}auth/permissions/get-all`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
return {
|
||||
hasPermission: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (res.status === 401) {
|
||||
return {
|
||||
hasPermission: false,
|
||||
reason: "Unauthorized",
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error("Unexpected status code");
|
||||
},
|
||||
retry: false,
|
||||
});
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { GasEstimate } from "@3rdweb-sdk/react/hooks/useGas";
|
||||
import { Box, type BoxProps, Flex, Icon, Tooltip } from "@chakra-ui/react";
|
||||
import { AiOutlineInfoCircle } from "@react-icons/all-files/ai/AiOutlineInfoCircle";
|
||||
import { utils } from "ethers";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { FiExternalLink } from "react-icons/fi";
|
||||
import { toTokens } from "thirdweb/utils";
|
||||
import { Heading, Link, Text } from "tw-components";
|
||||
import { contractSlugMapping } from "./contractSlugMapping";
|
||||
import { functionNameMapping } from "./functionNameMapping";
|
||||
@@ -31,18 +31,15 @@ export const GasEstimatorBox: React.FC<GasEstimatorBoxProps> = ({
|
||||
const formatPrice = (price: number | undefined) => {
|
||||
if (price && ethOrUsd === "eth") {
|
||||
return `~${Number(
|
||||
utils.formatUnits(
|
||||
`${((gasEstimate?.gasPrice as number) || 30) * price}`,
|
||||
"gwei",
|
||||
),
|
||||
toTokens(BigInt(((gasEstimate?.gasPrice as number) || 30) * price), 9),
|
||||
).toFixed(4)} ETH`;
|
||||
}
|
||||
if (price && ethOrUsd === "usd") {
|
||||
return `~$${(
|
||||
Number(
|
||||
utils.formatUnits(
|
||||
`${((gasEstimate?.gasPrice as number) || 30) * price}`,
|
||||
"gwei",
|
||||
toTokens(
|
||||
BigInt(((gasEstimate?.gasPrice as number) || 30) * price),
|
||||
9,
|
||||
),
|
||||
) * (gasEstimate?.ethPrice || 3400)
|
||||
).toFixed(2)}`;
|
||||
|
||||
@@ -91,7 +91,7 @@ export const PublishWithVersionPage: React.FC<PublishWithVersionPageProps> = ({
|
||||
<SelectContent>
|
||||
{availableVersions.map((v, idx) => {
|
||||
return (
|
||||
<SelectItem value={v} key={v}>
|
||||
<SelectItem value={v || "latest"} key={v}>
|
||||
{v}
|
||||
{idx === 0 && (
|
||||
<span className="text-muted-foreground"> (latest) </span>
|
||||
|
||||
@@ -103,7 +103,7 @@ export const CustomChainRenderer = ({
|
||||
{!disableChainConfig && storedChain && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="ml-auto p-2 leading-4 opacity-0 group-hover:opacity-100 hover:bg-transparent transition-opacity"
|
||||
className="ml-auto p-2 leading-4 md:opacity-0 group-hover:opacity-100 hover:bg-transparent transition-opacity"
|
||||
aria-label="Configure Network"
|
||||
onClick={() => {
|
||||
setEditChain(storedChain);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isAddress } from "ethers/lib/utils";
|
||||
import { isAddress } from "thirdweb/utils";
|
||||
import { RE_BUNDLE_ID, RE_DOMAIN } from "utils/regex";
|
||||
import { validStrList } from "utils/validations";
|
||||
import { z } from "zod";
|
||||
@@ -11,7 +11,8 @@ const nameValidation = z
|
||||
const domainsValidation = z.string().refine(
|
||||
(str) =>
|
||||
validStrList(str, (domain) => {
|
||||
return domain.split(":")[0] === "localhost" || RE_DOMAIN.test(domain);
|
||||
// This isn't the strictest validation, but users keep running into cases where the validation is TOO strict
|
||||
return domain.includes(":") || RE_DOMAIN.test(domain);
|
||||
}),
|
||||
{
|
||||
message: "Some of the domains are invalid",
|
||||
|
||||
@@ -6,9 +6,9 @@ import { useDisclosure, useToast } from "@chakra-ui/react";
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
import { TWTable } from "components/shared/TWTable";
|
||||
import { format } from "date-fns/format";
|
||||
import { isAddress } from "ethers/lib/utils";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useState } from "react";
|
||||
import { isAddress } from "thirdweb/utils";
|
||||
import { Button, Text } from "tw-components";
|
||||
import type { ComponentWithChildren } from "types/component-with-children";
|
||||
import { shortenString } from "utils/usedapp-external";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useDashboardEVMChainId } from "@3rdweb-sdk/react";
|
||||
import { Flex, Input, Select, type SelectProps } from "@chakra-ui/react";
|
||||
import { useSDKChainId } from "@thirdweb-dev/react";
|
||||
import { CURRENCIES, type CurrencyMetadata } from "constants/currencies";
|
||||
import { useSupportedChainsRecord } from "hooks/chains/configureChains";
|
||||
import { useMemo, useState } from "react";
|
||||
@@ -25,7 +25,7 @@ export const CurrencySelector: React.FC<CurrencySelectorProps> = ({
|
||||
defaultCurrencies = [],
|
||||
...props
|
||||
}) => {
|
||||
const chainId = useSDKChainId();
|
||||
const chainId = useDashboardEVMChainId();
|
||||
const configuredChainsRecord = useSupportedChainsRecord();
|
||||
const chain = chainId ? configuredChainsRecord[chainId] : undefined;
|
||||
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
ButtonGroup,
|
||||
Center,
|
||||
Divider,
|
||||
Flex,
|
||||
Icon,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuList,
|
||||
Spinner,
|
||||
Table,
|
||||
Tbody,
|
||||
@@ -29,7 +33,7 @@ import { type SetStateAction, useMemo, useState } from "react";
|
||||
import { FaEllipsisVertical } from "react-icons/fa6";
|
||||
import { FiArrowRight } from "react-icons/fi";
|
||||
import type { IconType } from "react-icons/lib";
|
||||
import { Button, MenuItem, TableContainer, Text } from "tw-components";
|
||||
import { TableContainer, Text } from "tw-components";
|
||||
|
||||
type CtaMenuItem<TRowData> = {
|
||||
icon?: IconType;
|
||||
@@ -54,6 +58,7 @@ type TWTableProps<TRowData> = {
|
||||
showLess?: boolean;
|
||||
};
|
||||
title: string;
|
||||
bodyRowClassName?: string;
|
||||
};
|
||||
|
||||
export function TWTable<TRowData>(tableProps: TWTableProps<TRowData>) {
|
||||
@@ -148,13 +153,13 @@ export function TWTable<TRowData>(tableProps: TWTableProps<TRowData>) {
|
||||
</Th>
|
||||
))}
|
||||
{(tableProps.onRowClick || tableProps.onMenuClick) && (
|
||||
<Th border="none" />
|
||||
<Th border="none" className="w-0" />
|
||||
)}
|
||||
</Tr>
|
||||
))}
|
||||
</Thead>
|
||||
|
||||
<Tbody>
|
||||
<Tbody className="!bg-background">
|
||||
{table.getRowModel().rows.map((row) => {
|
||||
return (
|
||||
<Tr
|
||||
@@ -176,6 +181,7 @@ export function TWTable<TRowData>(tableProps: TWTableProps<TRowData>) {
|
||||
},
|
||||
}
|
||||
: {})}
|
||||
className={tableProps.bodyRowClassName}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
return (
|
||||
@@ -207,30 +213,37 @@ export function TWTable<TRowData>(tableProps: TWTableProps<TRowData>) {
|
||||
borderBottomWidth="inherit"
|
||||
borderBottomColor="accent.100"
|
||||
>
|
||||
<Menu>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
variant="outline"
|
||||
icon={<Icon as={FaEllipsisVertical} boxSize={4} />}
|
||||
aria-label="Actions"
|
||||
/>
|
||||
<MenuList>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
aria-label="Actions"
|
||||
className="relative z-10 p-2.5 !h-auto"
|
||||
>
|
||||
<FaEllipsisVertical className="size-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
{tableProps.onMenuClick.map(
|
||||
({ icon, text, onClick, isDestructive }) => {
|
||||
return (
|
||||
<MenuItem
|
||||
<DropdownMenuItem
|
||||
key={text}
|
||||
onClick={() => onClick(row.original)}
|
||||
icon={icon && <Icon as={icon} boxSize={4} />}
|
||||
color={isDestructive ? "red.500" : undefined}
|
||||
className={cn(
|
||||
"gap-3 px-3 py-3 min-w-[170px] cursor-pointer",
|
||||
isDestructive &&
|
||||
"!text-destructive-text hover:!bg-destructive",
|
||||
)}
|
||||
>
|
||||
{icon && <Icon as={icon} boxSize={4} />}
|
||||
{text}
|
||||
</MenuItem>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Td>
|
||||
) : null}
|
||||
</Tr>
|
||||
|
||||
@@ -22,12 +22,12 @@ import {
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { NetworkDropdown } from "components/contract-components/contract-publish-form/NetworkDropdown";
|
||||
import { GatedSwitch } from "components/settings/Account/Billing/GatedSwitch";
|
||||
import { isAddress } from "ethers/lib/utils";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
import { useMemo } from "react";
|
||||
import { useFieldArray, useForm } from "react-hook-form";
|
||||
import { LuTrash2 } from "react-icons/lu";
|
||||
import { isAddress } from "thirdweb/utils";
|
||||
import {
|
||||
Button,
|
||||
FormErrorMessage,
|
||||
|
||||
+1
-1
@@ -1,3 +1,4 @@
|
||||
import { DASHBOARD_THIRDWEB_CLIENT_ID } from "@/constants/env";
|
||||
import {
|
||||
ThirdwebProvider,
|
||||
type WalletConfig,
|
||||
@@ -5,7 +6,6 @@ import {
|
||||
es,
|
||||
ja,
|
||||
} from "@thirdweb-dev/react";
|
||||
import { DASHBOARD_THIRDWEB_CLIENT_ID } from "constants/rpc";
|
||||
import { THIRDWEB_API_HOST, THIRDWEB_DOMAIN } from "constants/urls";
|
||||
import { StorageSingleton } from "lib/sdk";
|
||||
|
||||
|
||||
+1
-1
@@ -1,9 +1,9 @@
|
||||
import { isProd } from "@/constants/env";
|
||||
import {
|
||||
type WalletConfig,
|
||||
embeddedWallet,
|
||||
smartWallet,
|
||||
} from "@thirdweb-dev/react";
|
||||
import { isProd } from "constants/rpc";
|
||||
import { useState } from "react";
|
||||
import { type WalletId, walletInfoRecord } from "./walletInfoRecord";
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import { thirdwebClient } from "@/constants/client";
|
||||
import { getContract } from "thirdweb";
|
||||
import { polygon } from "thirdweb/chains";
|
||||
|
||||
/**
|
||||
* Putting this here solely for the purpose of getting rid of sdk v4
|
||||
*/
|
||||
@@ -16,3 +20,9 @@ export type ContractType =
|
||||
| "token-drop"
|
||||
| "token"
|
||||
| "vote";
|
||||
|
||||
export const MULTICHAIN_REGISTRY_CONTRACT = getContract({
|
||||
chain: polygon,
|
||||
client: thirdwebClient,
|
||||
address: "0xcdAD8FA86e18538aC207872E8ff3536501431B73",
|
||||
});
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* publicly available wallet for local nodes
|
||||
* extracted from SDK v4, used to request localhost testnet funds
|
||||
*/
|
||||
export const LOCAL_NODE_PKEY =
|
||||
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
|
||||
|
||||
/**
|
||||
* Used for gasless transactions on Dashboard:
|
||||
* 1. Add contracts to Multichain Registry
|
||||
* 2. Remove contracts from Multichain Registry
|
||||
*/
|
||||
export const DASHBOARD_ENGINE_RELAYER_URL =
|
||||
"https://checkout.engine.thirdweb.com/relayer/0c2bdd3a-307f-4243-b6e5-5ba495222d2b";
|
||||
|
||||
export const DASHBOARD_FORWARDER_ADDRESS =
|
||||
"0x409d530a6961297ece29121dbee2c917c3398659";
|
||||
@@ -1,11 +0,0 @@
|
||||
export const DASHBOARD_THIRDWEB_CLIENT_ID =
|
||||
process.env.NEXT_PUBLIC_DASHBOARD_CLIENT_ID || "";
|
||||
|
||||
export const DASHBOARD_THIRDWEB_SECRET_KEY =
|
||||
process.env.DASHBOARD_SECRET_KEY || "";
|
||||
|
||||
export const isProd =
|
||||
(process.env.VERCEL_ENV || process.env.NEXT_PUBLIC_VERCEL_ENV) ===
|
||||
"production";
|
||||
|
||||
export const PROD_OR_DEV_URL = isProd ? "thirdweb.com" : "thirdweb-dev.com";
|
||||
@@ -49,3 +49,19 @@ const AddressSchema = z.custom<string>(
|
||||
export const AddressOrEnsSchema = z.union([AddressSchema, EnsSchema], {
|
||||
invalid_type_error: "Provided value was not a valid address or ENS name",
|
||||
});
|
||||
|
||||
const FileSchema = z.instanceof(File) as z.ZodType<InstanceType<typeof File>>;
|
||||
|
||||
const FileOrStringSchema = z.union([FileSchema, z.string()]);
|
||||
|
||||
export const CommonContractSchema = z
|
||||
.object({
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
image: FileOrStringSchema.optional(),
|
||||
external_link: z.string().optional(),
|
||||
app_uri: z.string().optional(),
|
||||
social_urls: z.record(z.string()).optional(),
|
||||
defaultAdmin: AddressOrEnsSchema.optional(),
|
||||
})
|
||||
.catchall(z.unknown());
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isProd } from "constants/rpc";
|
||||
import { isProd } from "@/constants/env";
|
||||
import { useAllChainsData } from "hooks/chains/allChains";
|
||||
import { createContext, useCallback, useEffect, useState } from "react";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { ThirdwebSDKProvider, useSigner } from "@thirdweb-dev/react";
|
||||
import type { SDKOptions } from "@thirdweb-dev/sdk";
|
||||
import {
|
||||
DASHBOARD_THIRDWEB_CLIENT_ID,
|
||||
DASHBOARD_THIRDWEB_SECRET_KEY,
|
||||
} from "constants/rpc";
|
||||
} from "@/constants/env";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { ThirdwebSDKProvider, useSigner } from "@thirdweb-dev/react";
|
||||
import type { SDKOptions } from "@thirdweb-dev/sdk";
|
||||
import {
|
||||
useSupportedChain,
|
||||
useSupportedChains,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user