Compare commits
130
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f47aa61915 | ||
|
|
a0aea23e74 | ||
|
|
1405598e7e | ||
|
|
120d61f410 | ||
|
|
2973ba1f54 | ||
|
|
7a2de70310 | ||
|
|
659cb03eea | ||
|
|
8128d127ce | ||
|
|
d401a13cdc | ||
|
|
41fbdafcf7 | ||
|
|
44e8e7bce2 | ||
|
|
201a4b3b8f | ||
|
|
716a4d8c26 | ||
|
|
9a86559c36 | ||
|
|
3853e17656 | ||
|
|
ac2bb59fa0 | ||
|
|
14696d32fc | ||
|
|
dc903ae029 | ||
|
|
be03c28b5d | ||
|
|
bdf9507de8 | ||
|
|
86d2bafd72 | ||
|
|
6b9bfb6ec7 | ||
|
|
8a618d7bd3 | ||
|
|
e97412bec8 | ||
|
|
626072cc0d | ||
|
|
5d6b1e1d0a | ||
|
|
b1323ea211 | ||
|
|
cbbb414141 | ||
|
|
0417ab45f1 | ||
|
|
c49bbd3389 | ||
|
|
f192875547 | ||
|
|
55bf1ad5a3 | ||
|
|
01dd80fad9 | ||
|
|
279d81ec35 | ||
|
|
d93ec63cd7 | ||
|
|
a7f4484702 | ||
|
|
b38308d893 | ||
|
|
7ef1c721ef | ||
|
|
c48a0b1105 | ||
|
|
6b9b3efb51 | ||
|
|
654cbbaa2e | ||
|
|
88bbbd5a01 | ||
|
|
0c2fd4db98 | ||
|
|
2c37732984 | ||
|
|
783e448e2e | ||
|
|
6c87d7ba3a | ||
|
|
6b8bde1abf | ||
|
|
81b33c7d7c | ||
|
|
e37ac839b2 | ||
|
|
462e1fd2aa | ||
|
|
b4d8be4f91 | ||
|
|
15b7e8cfb8 | ||
|
|
886094fbac | ||
|
|
0a1bce8fa8 | ||
|
|
3ccfa981a8 | ||
|
|
fb96e83081 | ||
|
|
9768b0c0b7 | ||
|
|
2312f61329 | ||
|
|
042439aab9 | ||
|
|
0368ee538e | ||
|
|
c6d4aef91b | ||
|
|
86c953f18e | ||
|
|
4ce4072031 | ||
|
|
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
|
||||
|
||||
@@ -30,3 +30,6 @@ sitemap*.xml
|
||||
# typedoc
|
||||
legacy_packages/*/typedoc/*
|
||||
packages/*/typedoc/*
|
||||
|
||||
*storybook.log
|
||||
storybook-static
|
||||
@@ -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)
|
||||
#
|
||||
|
||||
@@ -5,6 +5,7 @@ module.exports = {
|
||||
"plugin:import/typescript",
|
||||
"plugin:@next/next/recommended",
|
||||
"plugin:promise/recommended",
|
||||
"plugin:storybook/recommended"
|
||||
],
|
||||
rules: {
|
||||
"react-compiler/react-compiler": "error",
|
||||
@@ -80,6 +81,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',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { StorybookConfig } from "@storybook/nextjs";
|
||||
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
/**
|
||||
* This function is used to resolve the absolute path of a package.
|
||||
* It is needed in projects that use Yarn PnP or are set up within a monorepo.
|
||||
*/
|
||||
function getAbsolutePath(value: string): string {
|
||||
return dirname(require.resolve(join(value, "package.json")));
|
||||
}
|
||||
const config: StorybookConfig = {
|
||||
stories: ["../src/**/*.stories.tsx"],
|
||||
addons: [
|
||||
getAbsolutePath("@storybook/addon-onboarding"),
|
||||
getAbsolutePath("@storybook/addon-links"),
|
||||
getAbsolutePath("@storybook/addon-essentials"),
|
||||
getAbsolutePath("@chromatic-com/storybook"),
|
||||
getAbsolutePath("@storybook/addon-interactions"),
|
||||
],
|
||||
framework: {
|
||||
name: getAbsolutePath("@storybook/nextjs"),
|
||||
options: {},
|
||||
},
|
||||
refs: {
|
||||
"@chakra-ui/react": {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
staticDirs: ["../public"],
|
||||
};
|
||||
export default config;
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { Preview } from "@storybook/react";
|
||||
import "@/styles/globals.css";
|
||||
import { Inter as interFont } from "next/font/google";
|
||||
// biome-ignore lint/correctness/noUnusedImports: <explanation>
|
||||
import React from "react";
|
||||
import { cn } from "../src/@/lib/utils";
|
||||
|
||||
// Note: Wrapping the Stoy with AppRouterProviders makes the storybook server time SUPER SLOW
|
||||
// so let's just not have it there - all stories should be independent from context anyway and just rely on props
|
||||
|
||||
const fontSans = interFont({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-sans",
|
||||
});
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
decorators: [
|
||||
(Story) => {
|
||||
return (
|
||||
<div className={cn("font-sans antialiased", fontSans.variable)}>
|
||||
<Story />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default preview;
|
||||
@@ -5,8 +5,8 @@ const ContentSecurityPolicy = `
|
||||
img-src * data: blob:;
|
||||
media-src * data: blob:;
|
||||
object-src 'none';
|
||||
style-src 'self' 'unsafe-inline';
|
||||
font-src 'self';
|
||||
style-src 'self' 'unsafe-inline' https://vercel.live;
|
||||
font-src 'self' https://vercel.live https://assets.vercel.com;
|
||||
frame-src * data:;
|
||||
script-src 'self' 'unsafe-eval' 'unsafe-inline' 'wasm-unsafe-eval' 'inline-speculation-rules' *.thirdweb.com *.thirdweb-dev.com vercel.live js.stripe.com pg.paper.xyz portal.usecontext.io;
|
||||
connect-src * data: blob:;
|
||||
|
||||
+17
-15
@@ -18,7 +18,9 @@
|
||||
"build:analyze": "ANALYZE=true pnpm run build",
|
||||
"knip": "knip",
|
||||
"playwright": "playwright test",
|
||||
"update-checkly": "npx checkly deploy"
|
||||
"update-checkly": "npx checkly deploy",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chakra-ui/react": "^2.8.2",
|
||||
@@ -45,12 +47,10 @@
|
||||
"@radix-ui/react-toggle-group": "^1.1.0",
|
||||
"@radix-ui/react-tooltip": "1.1.2",
|
||||
"@react-icons/all-files": "^4.1.0",
|
||||
"@sentry/nextjs": "^8.8.0",
|
||||
"@sentry/nextjs": "^8.26.0",
|
||||
"@stripe/react-stripe-js": "^2.7.3",
|
||||
"@stripe/stripe-js": "^3.5.0",
|
||||
"@tanstack/query-async-storage-persister": "^4.36.1",
|
||||
"@tanstack/react-query": "^4.36.1",
|
||||
"@tanstack/react-query-persist-client": "^4.36.1",
|
||||
"@tanstack/react-table": "^8.17.3",
|
||||
"@thirdweb-dev/react": "workspace:*",
|
||||
"@thirdweb-dev/sdk": "workspace:*",
|
||||
@@ -68,7 +68,6 @@
|
||||
"flat": "^6.0.1",
|
||||
"framer-motion": "^11.2.12",
|
||||
"fuse.js": "7.0.0",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"ioredis": "^5.4.1",
|
||||
"ipaddr.js": "^2.2.0",
|
||||
"lottie-react": "^2.4.0",
|
||||
@@ -82,13 +81,11 @@
|
||||
"pluralize": "^8.0.0",
|
||||
"posthog-js": "1.128.3",
|
||||
"posthog-js-opensource": "npm:[email protected]",
|
||||
"prettier": "^3.3.2",
|
||||
"prism-react-renderer": "^2.3.1",
|
||||
"prismjs": "^1.29.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "18.3.1",
|
||||
"react-children-utilities": "^2.10.0",
|
||||
"react-confetti": "^6.1.0",
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-dropzone": "^14.2.3",
|
||||
@@ -106,7 +103,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",
|
||||
@@ -117,9 +114,18 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chakra-ui/cli": "^2.4.1",
|
||||
"@chromatic-com/storybook": "^1.5.0",
|
||||
"@next/bundle-analyzer": "^14.2.4",
|
||||
"@next/eslint-plugin-next": "^14.2.4",
|
||||
"@playwright/test": "1.44.1",
|
||||
"@storybook/addon-essentials": "^8.2.9",
|
||||
"@storybook/addon-interactions": "^8.2.9",
|
||||
"@storybook/addon-links": "^8.2.9",
|
||||
"@storybook/addon-onboarding": "^8.2.9",
|
||||
"@storybook/blocks": "^8.2.9",
|
||||
"@storybook/nextjs": "^8.2.9",
|
||||
"@storybook/react": "^8.2.9",
|
||||
"@storybook/test": "^8.2.9",
|
||||
"@types/color": "^3.0.6",
|
||||
"@types/node": "20.14.9",
|
||||
"@types/papaparse": "^5.3.14",
|
||||
@@ -134,21 +140,17 @@
|
||||
"@typescript-eslint/parser": "7.14.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"checkly": "^4.7.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-biome": "^1.7.3",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-inclusive-language": "^2.2.1",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-promise": "^6.2.0",
|
||||
"eslint-plugin-react": "7.34.3",
|
||||
"eslint-plugin-react-compiler": "0.0.0-experimental-51a85ea-20240601",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-storybook": "^0.8.0",
|
||||
"knip": "^5.23.1",
|
||||
"next-sitemap": "^4.2.3",
|
||||
"next-unused": "^0.0.6",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"storybook": "^8.2.9",
|
||||
"tailwindcss": "^3.4.10",
|
||||
"typescript": "5.5.4"
|
||||
},
|
||||
"next-unused": {
|
||||
|
||||
@@ -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"]
|
||||
},
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 8.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
@@ -119,11 +119,6 @@ function redirects() {
|
||||
destination: "/template/erc721",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/abuse",
|
||||
destination: "https://forms.gle/v9UJtHbVw8fXypcd7",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/create-api-key",
|
||||
destination: "/dashboard/settings/api-keys",
|
||||
@@ -136,12 +131,17 @@ function redirects() {
|
||||
},
|
||||
{
|
||||
source: "/dashboard/connect",
|
||||
destination: "/dashboard/connect/playground",
|
||||
destination: "/dashboard/connect/in-app-wallets",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/dashboard/connect/playground",
|
||||
destination: "https://playground.thirdweb.com/connect/sign-in/button",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/dashboard/wallet",
|
||||
destination: "/dashboard/connect/playground",
|
||||
destination: "https://playground.thirdweb.com/connect/sign-in/button",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
@@ -166,7 +166,7 @@ function redirects() {
|
||||
},
|
||||
{
|
||||
source: "/dashboard/wallets",
|
||||
destination: "/dashboard/connect/playground",
|
||||
destination: "https://playground.thirdweb.com/connect/sign-in/button",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
@@ -186,7 +186,7 @@ function redirects() {
|
||||
},
|
||||
{
|
||||
source: "/dashboard/wallets/connect",
|
||||
destination: "/dashboard/connect/playground",
|
||||
destination: "https://playground.thirdweb.com/connect/sign-in/button",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import { Button } from "@/components/ui/button";
|
||||
import { ClientOnly } from "components/ClientOnly/ClientOnly";
|
||||
import { Moon, Sun } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import * as React from "react";
|
||||
import { Skeleton } from "./ui/skeleton";
|
||||
|
||||
export function ColorModeToggle() {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Highlight } from "prism-react-renderer";
|
||||
import React from "react";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { CopyButton } from "./CopyButton";
|
||||
import { Card } from "./card";
|
||||
|
||||
@@ -34,7 +34,7 @@ export function DynamicHeight(props: {
|
||||
);
|
||||
}
|
||||
|
||||
export function useHeightObserver() {
|
||||
function useHeightObserver() {
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
const [height, setHeight] = useState<number | undefined>();
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -8,10 +8,5 @@ const LOGGED_IN_ONLY_PATHS = [
|
||||
];
|
||||
|
||||
export function isLoginRequired(pathname: string) {
|
||||
// exceptions from LOGGED_IN_ONLY_PATHS
|
||||
if (pathname.startsWith("/dashboard/connect/playground")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return LOGGED_IN_ONLY_PATHS.some((path) => pathname.startsWith(path));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -3,7 +3,8 @@
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
:root,
|
||||
[data-theme="light"] {
|
||||
/* bg - neutral */
|
||||
--background: 0 0% 100%;
|
||||
--secondary: 0 0% 97%;
|
||||
@@ -44,7 +45,8 @@
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
.dark,
|
||||
[data-theme="dark"] {
|
||||
/* bg - neutral */
|
||||
--background: 0 0% 4.5%;
|
||||
--secondary: 0 0% 6.5%;
|
||||
@@ -94,19 +96,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%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,16 @@
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { thirdwebClient } from "@/constants/client";
|
||||
import { useSupportedChains } from "@thirdweb-dev/react";
|
||||
import { getSDKTheme } from "app/components/sdk-component-theme";
|
||||
import { CustomChainRenderer } from "components/selects/CustomChainRenderer";
|
||||
import { mapV4ChainToV5Chain } from "contexts/map-chains";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useSupportedChains } from "hooks/chains/configureChains";
|
||||
import {
|
||||
useAddRecentlyUsedChainId,
|
||||
useRecentlyUsedChains,
|
||||
} from "hooks/chains/recentlyUsedChains";
|
||||
import { useSetIsNetworkConfigModalOpen } from "hooks/networkConfigModal";
|
||||
import { useTheme } from "next-themes";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
@@ -16,10 +20,6 @@ import { usePathname } from "next/navigation";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import type { Chain } from "thirdweb";
|
||||
import { AutoConnect, ConnectButton, useConnectModal } from "thirdweb/react";
|
||||
import { getSDKTheme } from "../../../../app/components/sdk-component-theme";
|
||||
import { CustomChainRenderer } from "../../../../components/selects/CustomChainRenderer";
|
||||
import { mapV4ChainToV5Chain } from "../../../../contexts/map-chains";
|
||||
import { useSetIsNetworkConfigModalOpen } from "../../../../hooks/networkConfigModal";
|
||||
import { useFavoriteChains } from "../../hooks/useFavoriteChains";
|
||||
import { useLoggedInUser } from "../../hooks/useLoggedInUser";
|
||||
import { popularChains } from "../popularChains";
|
||||
|
||||
@@ -1,56 +1,70 @@
|
||||
import { useContract, useContractEvents } from "@thirdweb-dev/react";
|
||||
import type { ContractEvent } from "@thirdweb-dev/sdk";
|
||||
import { type AbiEvent, formatAbiItem } from "abitype";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
type PreparedEvent,
|
||||
type ThirdwebContract,
|
||||
prepareEvent,
|
||||
} from "thirdweb";
|
||||
import { useContractEvents } from "thirdweb/react";
|
||||
import { useResolveContractAbi } from "./useResolveContractAbi";
|
||||
|
||||
interface InternalTransaction {
|
||||
transactionHash: ContractEvent["transaction"]["transactionHash"];
|
||||
blockNumber: ContractEvent["transaction"]["blockNumber"];
|
||||
events: ContractEvent[];
|
||||
export interface InternalTransaction {
|
||||
transactionHash: string;
|
||||
blockNumber: bigint;
|
||||
events: Array<{
|
||||
logIndex: number;
|
||||
address: string;
|
||||
eventName: string;
|
||||
args: readonly unknown[] | Record<string, unknown>;
|
||||
}>;
|
||||
}
|
||||
|
||||
export function useActivity(contractAddress?: string, autoUpdate?: boolean) {
|
||||
// const provider = useProvider();
|
||||
const contractQuery = useContract(contractAddress);
|
||||
export function useActivity(contract: ThirdwebContract, autoUpdate?: boolean) {
|
||||
const abiQuery = useResolveContractAbi(contract);
|
||||
|
||||
const eventsQuery = useContractEvents(contractQuery.contract, undefined, {
|
||||
subscribe: autoUpdate,
|
||||
queryFilter: {
|
||||
fromBlock: -20000,
|
||||
},
|
||||
// Get all the PreprareEvents from the contract abis
|
||||
const events: PreparedEvent<AbiEvent>[] = useMemo(() => {
|
||||
const eventsItems = (abiQuery.data || []).filter((o) => o.type === "event");
|
||||
const eventSignatures = eventsItems.map((event) => formatAbiItem(event));
|
||||
return eventSignatures.map((signature) =>
|
||||
prepareEvent({ signature: signature as `event ${string}` }),
|
||||
);
|
||||
}, [abiQuery.data]);
|
||||
|
||||
const eventsQuery = useContractEvents({
|
||||
contract,
|
||||
events,
|
||||
blockRange: 20000,
|
||||
watch: autoUpdate,
|
||||
});
|
||||
|
||||
return useMemo(() => {
|
||||
if (!eventsQuery.data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const obj = eventsQuery.data.slice(0, 100).reduce(
|
||||
(acc, curr) => {
|
||||
if (acc[curr.transaction.transactionHash]) {
|
||||
acc[curr.transaction.transactionHash].events.push(curr);
|
||||
acc[curr.transaction.transactionHash].events.sort(
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
(a: any, b: any) => b.transaction.logIndex - a.transaction.logIndex,
|
||||
if (acc[curr.transactionHash]) {
|
||||
acc[curr.transactionHash].events.push(curr);
|
||||
acc[curr.transactionHash].events.sort(
|
||||
(a, b) => b.logIndex - a.logIndex,
|
||||
);
|
||||
if (
|
||||
acc[curr.transaction.transactionHash].blockNumber >
|
||||
curr.transaction.blockNumber
|
||||
) {
|
||||
acc[curr.transaction.transactionHash].blockNumber =
|
||||
curr.transaction.blockNumber;
|
||||
if (acc[curr.transactionHash].blockNumber > curr.blockNumber) {
|
||||
acc[curr.transactionHash].blockNumber = curr.blockNumber;
|
||||
}
|
||||
} else {
|
||||
acc[curr.transaction.transactionHash] = {
|
||||
transactionHash: curr.transaction.transactionHash,
|
||||
blockNumber: curr.transaction.blockNumber,
|
||||
acc[curr.transactionHash] = {
|
||||
transactionHash: curr.transactionHash,
|
||||
blockNumber: curr.blockNumber,
|
||||
events: [curr],
|
||||
};
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, InternalTransaction>,
|
||||
);
|
||||
return Object.values(obj).sort((a, b) => b.blockNumber - a.blockNumber);
|
||||
return Object.values(obj).sort((a, b) =>
|
||||
a.blockNumber > b.blockNumber ? -1 : 1,
|
||||
);
|
||||
}, [eventsQuery.data]);
|
||||
}
|
||||
|
||||
@@ -73,7 +73,6 @@ export type Account = {
|
||||
interface UpdateAccountInput {
|
||||
name?: string;
|
||||
email?: string;
|
||||
plan?: AccountPlan;
|
||||
linkWallet?: boolean;
|
||||
subscribeToUpdates?: boolean;
|
||||
onboardSkipped?: boolean;
|
||||
@@ -271,9 +270,9 @@ interface UseAccountInput {
|
||||
export function useAccount({ refetchInterval }: UseAccountInput = {}) {
|
||||
const { user, isLoggedIn } = useLoggedInUser();
|
||||
|
||||
return useQuery(
|
||||
accountKeys.me(user?.address as string),
|
||||
async () => {
|
||||
return useQuery({
|
||||
queryKey: accountKeys.me(user?.address as string),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/account/me`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
@@ -288,19 +287,17 @@ export function useAccount({ refetchInterval }: UseAccountInput = {}) {
|
||||
|
||||
return json.data as Account;
|
||||
},
|
||||
{
|
||||
enabled: !!user?.address && isLoggedIn,
|
||||
refetchInterval: refetchInterval ?? false,
|
||||
},
|
||||
);
|
||||
enabled: !!user?.address && isLoggedIn,
|
||||
refetchInterval: refetchInterval ?? false,
|
||||
});
|
||||
}
|
||||
|
||||
export function useAccountUsage() {
|
||||
const { user, isLoggedIn } = useLoggedInUser();
|
||||
|
||||
return useQuery(
|
||||
accountKeys.usage(user?.address as string),
|
||||
async () => {
|
||||
return useQuery({
|
||||
queryKey: accountKeys.usage(user?.address as string),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/account/usage`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
@@ -315,16 +312,16 @@ export function useAccountUsage() {
|
||||
|
||||
return json.data as UsageBillableByService;
|
||||
},
|
||||
{ enabled: !!user?.address && isLoggedIn },
|
||||
);
|
||||
enabled: !!user?.address && isLoggedIn,
|
||||
});
|
||||
}
|
||||
|
||||
export function useAccountCredits() {
|
||||
const { user, isLoggedIn } = useLoggedInUser();
|
||||
|
||||
return useQuery(
|
||||
accountKeys.credits(user?.address as string),
|
||||
async () => {
|
||||
return useQuery({
|
||||
queryKey: accountKeys.credits(user?.address as string),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/account/credits`, {
|
||||
method: "GET",
|
||||
|
||||
@@ -347,16 +344,19 @@ export function useAccountCredits() {
|
||||
|
||||
return credits;
|
||||
},
|
||||
{ enabled: !!user?.address && isLoggedIn },
|
||||
);
|
||||
enabled: !!user?.address && isLoggedIn,
|
||||
});
|
||||
}
|
||||
|
||||
export function useWalletStats(clientId: string | undefined) {
|
||||
const { user, isLoggedIn } = useLoggedInUser();
|
||||
|
||||
return useQuery(
|
||||
accountKeys.walletStats(user?.address as string, clientId as string),
|
||||
async () => {
|
||||
return useQuery({
|
||||
queryKey: accountKeys.walletStats(
|
||||
user?.address as string,
|
||||
clientId as string,
|
||||
),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(
|
||||
`${THIRDWEB_API_HOST}/v1/account/wallets?clientId=${clientId}`,
|
||||
{
|
||||
@@ -375,8 +375,8 @@ export function useWalletStats(clientId: string | undefined) {
|
||||
|
||||
return json.data as WalletStats;
|
||||
},
|
||||
{ enabled: !!clientId && !!user?.address && isLoggedIn },
|
||||
);
|
||||
enabled: !!clientId && !!user?.address && isLoggedIn,
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateAccount() {
|
||||
@@ -419,7 +419,7 @@ export function useUpdateAccountPlan(waitForWebhook?: boolean) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (input: { plan: string; feedback?: string; useTrial?: boolean }) => {
|
||||
async (input: { plan: string; feedback?: string }) => {
|
||||
invariant(user?.address, "walletAddress is required");
|
||||
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/account/plan`, {
|
||||
@@ -495,9 +495,9 @@ export function useUpdateNotifications() {
|
||||
export function useCreateBillingSession(enabled = false) {
|
||||
const { user } = useLoggedInUser();
|
||||
|
||||
return useQuery(
|
||||
accountKeys.billingSession(user?.address as string),
|
||||
async () => {
|
||||
return useQuery({
|
||||
queryKey: accountKeys.billingSession(user?.address as string),
|
||||
queryFn: async () => {
|
||||
invariant(user?.address, "walletAddress is required");
|
||||
|
||||
const res = await fetch(
|
||||
@@ -518,8 +518,8 @@ export function useCreateBillingSession(enabled = false) {
|
||||
|
||||
return json.data;
|
||||
},
|
||||
{ enabled },
|
||||
);
|
||||
enabled,
|
||||
});
|
||||
}
|
||||
|
||||
export function useConfirmEmail() {
|
||||
@@ -637,9 +637,9 @@ export function useCreatePaymentMethod() {
|
||||
|
||||
export function useApiKeys() {
|
||||
const { user, isLoggedIn } = useLoggedInUser();
|
||||
return useQuery(
|
||||
apiKeys.keys(user?.address as string),
|
||||
async () => {
|
||||
return useQuery({
|
||||
queryKey: apiKeys.keys(user?.address as string),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/keys`, {
|
||||
method: "GET",
|
||||
|
||||
@@ -654,8 +654,8 @@ export function useApiKeys() {
|
||||
}
|
||||
return json.data as ApiKey[];
|
||||
},
|
||||
{ enabled: !!user?.address && isLoggedIn },
|
||||
);
|
||||
enabled: !!user?.address && isLoggedIn,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateApiKey() {
|
||||
@@ -885,9 +885,9 @@ export function useRevokeAuthorizedWallet() {
|
||||
export function useAuthorizedWallets() {
|
||||
const { user, isLoggedIn } = useLoggedInUser();
|
||||
|
||||
return useQuery(
|
||||
authorizedWallets.authorizedWallets(user?.address as string),
|
||||
async () => {
|
||||
return useQuery({
|
||||
queryKey: authorizedWallets.authorizedWallets(user?.address as string),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/authorized-wallets`, {
|
||||
method: "GET",
|
||||
|
||||
@@ -903,8 +903,9 @@ export function useAuthorizedWallets() {
|
||||
|
||||
return json.data as AuthorizedWallet[];
|
||||
},
|
||||
{ enabled: !!user?.address && isLoggedIn, cacheTime: 0 },
|
||||
);
|
||||
enabled: !!user?.address && isLoggedIn,
|
||||
cacheTime: 0,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -924,7 +925,10 @@ export async function fetchChainsFromApi() {
|
||||
}
|
||||
|
||||
export function useApiChains() {
|
||||
return useQuery(["all-chains"], async () => {
|
||||
return fetchChainsFromApi();
|
||||
return useQuery({
|
||||
queryKey: ["all-chains"],
|
||||
queryFn: () => {
|
||||
return fetchChainsFromApi();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,9 +12,9 @@ import { getContractMetadata, name, symbol } from "thirdweb/extensions/common";
|
||||
* 2. our API endpoint
|
||||
*/
|
||||
export function useDashboardContractMetadata(contract: ThirdwebContract) {
|
||||
return useQuery(
|
||||
["contract-metadata-header", contract.chain.id, contract.address],
|
||||
async () => {
|
||||
return useQuery({
|
||||
queryKey: ["contract-metadata-header", contract.chain.id, contract.address],
|
||||
queryFn: async () => {
|
||||
const [contractMetadata, _name, _symbol, compilerMetadata] =
|
||||
await Promise.all([
|
||||
getContractMetadata({ contract }).catch(() => ({
|
||||
@@ -50,5 +50,5 @@ export function useDashboardContractMetadata(contract: ThirdwebContract) {
|
||||
contractType: compilerMetadata?.name || "",
|
||||
};
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { NFT, ThirdwebContract } from "thirdweb";
|
||||
import {
|
||||
getOwnedNFTs as getOwnedERC721,
|
||||
isERC721,
|
||||
} from "thirdweb/extensions/erc721";
|
||||
import { getOwnedNFTs as getOwnedERC1155 } from "thirdweb/extensions/erc1155";
|
||||
import invariant from "tiny-invariant";
|
||||
|
||||
/**
|
||||
* This hook is currently used for getting owned NFTs for Create DirectListing/English Auction
|
||||
* It should only be used if our indexing solutions don't support the current network
|
||||
*/
|
||||
export function useDashboardOwnedNFTs({
|
||||
contract,
|
||||
owner,
|
||||
disabled,
|
||||
}: { owner?: string; contract?: ThirdwebContract; disabled?: boolean }) {
|
||||
return useQuery({
|
||||
queryKey: [
|
||||
"owned-nfts",
|
||||
contract?.chain.id || "",
|
||||
contract?.address || "",
|
||||
owner || "",
|
||||
],
|
||||
queryFn: async (): Promise<NFT[]> => {
|
||||
invariant(contract, "Contract is required");
|
||||
invariant(owner, "owner address is required");
|
||||
const is721 = await isERC721({ contract });
|
||||
if (is721) {
|
||||
return getOwnedERC721({
|
||||
contract,
|
||||
owner,
|
||||
});
|
||||
}
|
||||
// Else default to 1155
|
||||
return getOwnedERC1155({ contract, address: owner });
|
||||
},
|
||||
enabled: !!contract && !!owner && !disabled,
|
||||
});
|
||||
}
|
||||
@@ -25,12 +25,12 @@ export type EmbeddedWalletUser = {
|
||||
export function useEmbeddedWallets(clientId: string) {
|
||||
const { user, isLoggedIn } = useLoggedInUser();
|
||||
|
||||
return useQuery(
|
||||
embeddedWalletsKeys.embeddedWallets(
|
||||
return useQuery({
|
||||
queryKey: embeddedWalletsKeys.embeddedWallets(
|
||||
user?.address as string,
|
||||
clientId as string,
|
||||
),
|
||||
async () => {
|
||||
queryFn: async () => {
|
||||
const res = await fetch(
|
||||
`${THIRDWEB_EWS_API_HOST}/api/thirdweb/embedded-wallet?clientId=${clientId}&lastAccessedAt=0`,
|
||||
{
|
||||
@@ -46,6 +46,6 @@ export function useEmbeddedWallets(clientId: string) {
|
||||
|
||||
return json.walletUsers as EmbeddedWalletUser[];
|
||||
},
|
||||
{ enabled: !!user?.address && isLoggedIn && !!clientId },
|
||||
);
|
||||
enabled: !!user?.address && isLoggedIn && !!clientId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -55,9 +55,9 @@ const getEngineRequestHeaders = (token: string | null): HeadersInit => {
|
||||
export function useEngineInstances() {
|
||||
const { user, isLoggedIn } = useLoggedInUser();
|
||||
|
||||
return useQuery(
|
||||
engineKeys.instances(user?.address as string),
|
||||
async (): Promise<EngineInstance[]> => {
|
||||
return useQuery({
|
||||
queryKey: engineKeys.instances(user?.address as string),
|
||||
queryFn: async (): Promise<EngineInstance[]> => {
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/engine`, {
|
||||
method: "GET",
|
||||
});
|
||||
@@ -79,8 +79,8 @@ export function useEngineInstances() {
|
||||
};
|
||||
});
|
||||
},
|
||||
{ enabled: !!user?.address && isLoggedIn },
|
||||
);
|
||||
enabled: !!user?.address && isLoggedIn,
|
||||
});
|
||||
}
|
||||
|
||||
// GET Requests
|
||||
@@ -100,9 +100,9 @@ export type BackendWallet = {
|
||||
export function useEngineBackendWallets(instance: string) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
|
||||
return useQuery(
|
||||
[engineKeys.backendWallets(instance)],
|
||||
async () => {
|
||||
return useQuery({
|
||||
queryKey: [engineKeys.backendWallets(instance)],
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`${instance}backend-wallet/get-all?limit=50`, {
|
||||
method: "GET",
|
||||
headers: getEngineRequestHeaders(token),
|
||||
@@ -112,8 +112,8 @@ export function useEngineBackendWallets(instance: string) {
|
||||
|
||||
return (json.result as BackendWallet[]) || [];
|
||||
},
|
||||
{ enabled: !!instance && !!token },
|
||||
);
|
||||
enabled: !!instance && !!token,
|
||||
});
|
||||
}
|
||||
|
||||
export interface EngineSystemHealth {
|
||||
@@ -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;
|
||||
@@ -381,9 +382,9 @@ type TransactionResponse = {
|
||||
export function useEngineTransactions(instance: string, autoUpdate: boolean) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
|
||||
return useQuery(
|
||||
engineKeys.transactions(instance),
|
||||
async () => {
|
||||
return useQuery({
|
||||
queryKey: engineKeys.transactions(instance),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`${instance}transaction/get-all`, {
|
||||
method: "GET",
|
||||
headers: getEngineRequestHeaders(token),
|
||||
@@ -393,11 +394,10 @@ export function useEngineTransactions(instance: string, autoUpdate: boolean) {
|
||||
|
||||
return (json.result as TransactionResponse) || {};
|
||||
},
|
||||
{
|
||||
enabled: !!instance && !!token,
|
||||
refetchInterval: autoUpdate ? 4_000 : false,
|
||||
},
|
||||
);
|
||||
|
||||
enabled: !!instance && !!token,
|
||||
refetchInterval: autoUpdate ? 4_000 : false,
|
||||
});
|
||||
}
|
||||
|
||||
type WalletConfig =
|
||||
@@ -422,9 +422,9 @@ type WalletConfig =
|
||||
export function useEngineWalletConfig(instance: string) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
|
||||
return useQuery(
|
||||
engineKeys.walletConfig(instance),
|
||||
async () => {
|
||||
return useQuery({
|
||||
queryKey: engineKeys.walletConfig(instance),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`${instance}configuration/wallets`, {
|
||||
method: "GET",
|
||||
headers: getEngineRequestHeaders(token),
|
||||
@@ -434,8 +434,8 @@ export function useEngineWalletConfig(instance: string) {
|
||||
|
||||
return (json.result as WalletConfig) || {};
|
||||
},
|
||||
{ enabled: !!instance && !!token },
|
||||
);
|
||||
enabled: !!instance && !!token,
|
||||
});
|
||||
}
|
||||
|
||||
type CurrencyValue = {
|
||||
@@ -455,9 +455,9 @@ export function useEngineBackendWalletBalance(
|
||||
|
||||
invariant(chainId, "chainId is required");
|
||||
|
||||
return useQuery(
|
||||
engineKeys.backendWalletBalance(address, chainId),
|
||||
async () => {
|
||||
return useQuery({
|
||||
queryKey: engineKeys.backendWalletBalance(address, chainId),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(
|
||||
`${instance}backend-wallet/${chainId}/${address}/get-balance`,
|
||||
{
|
||||
@@ -470,8 +470,8 @@ export function useEngineBackendWalletBalance(
|
||||
|
||||
return (json.result as CurrencyValue) || {};
|
||||
},
|
||||
{ enabled: !!instance && !!address && !!chainId && !!token },
|
||||
);
|
||||
enabled: !!instance && !!address && !!chainId && !!token,
|
||||
});
|
||||
}
|
||||
|
||||
export type EngineAdmin = {
|
||||
@@ -484,9 +484,9 @@ export function useEnginePermissions(instance: string) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const address = useActiveAccount()?.address;
|
||||
|
||||
return useQuery(
|
||||
engineKeys.permissions(instance),
|
||||
async () => {
|
||||
return useQuery({
|
||||
queryKey: engineKeys.permissions(instance),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`${instance}auth/permissions/get-all`, {
|
||||
method: "GET",
|
||||
headers: getEngineRequestHeaders(token),
|
||||
@@ -500,10 +500,9 @@ export function useEnginePermissions(instance: string) {
|
||||
|
||||
return (json.result as EngineAdmin[]) || [];
|
||||
},
|
||||
{
|
||||
enabled: !!instance && !!token && !!address,
|
||||
},
|
||||
);
|
||||
|
||||
enabled: !!instance && !!token && !!address,
|
||||
});
|
||||
}
|
||||
|
||||
export type AccessToken = {
|
||||
@@ -518,9 +517,9 @@ export type AccessToken = {
|
||||
export function useEngineAccessTokens(instance: string) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
|
||||
return useQuery(
|
||||
engineKeys.accessTokens(instance),
|
||||
async () => {
|
||||
return useQuery({
|
||||
queryKey: engineKeys.accessTokens(instance),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`${instance}auth/access-tokens/get-all`, {
|
||||
method: "GET",
|
||||
headers: getEngineRequestHeaders(token),
|
||||
@@ -530,8 +529,8 @@ export function useEngineAccessTokens(instance: string) {
|
||||
|
||||
return (json.result as AccessToken[]) || [];
|
||||
},
|
||||
{ enabled: !!instance && !!token },
|
||||
);
|
||||
enabled: !!instance && !!token,
|
||||
});
|
||||
}
|
||||
|
||||
export type KeypairAlgorithm = "ES256" | "RS256" | "PS256";
|
||||
@@ -548,9 +547,9 @@ export type Keypair = {
|
||||
export function useEngineKeypairs(instance: string) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
|
||||
return useQuery(
|
||||
engineKeys.keypairs(instance),
|
||||
async () => {
|
||||
return useQuery({
|
||||
queryKey: engineKeys.keypairs(instance),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`${instance}auth/keypair/get-all`, {
|
||||
method: "GET",
|
||||
headers: getEngineRequestHeaders(token),
|
||||
@@ -560,8 +559,8 @@ export function useEngineKeypairs(instance: string) {
|
||||
|
||||
return (json.result as Keypair[]) || [];
|
||||
},
|
||||
{ enabled: !!instance && !!token },
|
||||
);
|
||||
enabled: !!instance && !!token,
|
||||
});
|
||||
}
|
||||
|
||||
type AddKeypairInput = {
|
||||
@@ -644,9 +643,9 @@ export type EngineRelayer = {
|
||||
export function useEngineRelayer(instance: string) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
|
||||
return useQuery(
|
||||
engineKeys.relayers(instance),
|
||||
async () => {
|
||||
return useQuery({
|
||||
queryKey: engineKeys.relayers(instance),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`${instance}relayer/get-all`, {
|
||||
method: "GET",
|
||||
headers: getEngineRequestHeaders(token),
|
||||
@@ -656,8 +655,8 @@ export function useEngineRelayer(instance: string) {
|
||||
|
||||
return (json.result as EngineRelayer[]) || [];
|
||||
},
|
||||
{ enabled: !!instance && !!token },
|
||||
);
|
||||
enabled: !!instance && !!token,
|
||||
});
|
||||
}
|
||||
|
||||
export type CreateRelayerInput = {
|
||||
@@ -781,9 +780,9 @@ export interface EngineWebhook {
|
||||
export function useEngineWebhooks(instance: string) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
|
||||
return useQuery(
|
||||
engineKeys.webhooks(instance),
|
||||
async () => {
|
||||
return useQuery({
|
||||
queryKey: engineKeys.webhooks(instance),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`${instance}webhooks/get-all`, {
|
||||
method: "GET",
|
||||
headers: getEngineRequestHeaders(token),
|
||||
@@ -793,8 +792,8 @@ export function useEngineWebhooks(instance: string) {
|
||||
|
||||
return (json.result as EngineWebhook[]) || [];
|
||||
},
|
||||
{ enabled: !!instance && !!token },
|
||||
);
|
||||
enabled: !!instance && !!token,
|
||||
});
|
||||
}
|
||||
|
||||
// POST REQUESTS
|
||||
@@ -1240,9 +1239,9 @@ export function useEngineSendTokens(instance: string) {
|
||||
export function useEngineCorsConfiguration(instance: string) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
|
||||
return useQuery(
|
||||
engineKeys.corsUrls(instance),
|
||||
async () => {
|
||||
return useQuery({
|
||||
queryKey: engineKeys.corsUrls(instance),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`${instance}configuration/cors`, {
|
||||
method: "GET",
|
||||
headers: getEngineRequestHeaders(token),
|
||||
@@ -1252,8 +1251,8 @@ export function useEngineCorsConfiguration(instance: string) {
|
||||
|
||||
return (json.result as string[]) || [];
|
||||
},
|
||||
{ enabled: !!instance && !!token },
|
||||
);
|
||||
enabled: !!instance && !!token,
|
||||
});
|
||||
}
|
||||
|
||||
interface SetCorsUrlInput {
|
||||
@@ -1296,9 +1295,9 @@ export function useEngineIpAllowlistConfiguration(instance: string) {
|
||||
// if engine instance is not updated to have IP_ALLOWLIST
|
||||
const { data: health } = useEngineSystemHealth(instance);
|
||||
|
||||
return useQuery(
|
||||
engineKeys.ipAllowlist(instance),
|
||||
async () => {
|
||||
return useQuery({
|
||||
queryKey: engineKeys.ipAllowlist(instance),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`${instance}configuration/ip-allowlist`, {
|
||||
method: "GET",
|
||||
headers: getEngineRequestHeaders(token),
|
||||
@@ -1307,11 +1306,10 @@ export function useEngineIpAllowlistConfiguration(instance: string) {
|
||||
const json = await res.json();
|
||||
return (json.result as string[]) || [];
|
||||
},
|
||||
{
|
||||
enabled:
|
||||
!!instance && !!token && health?.features?.includes("IP_ALLOWLIST"),
|
||||
},
|
||||
);
|
||||
|
||||
enabled:
|
||||
!!instance && !!token && health?.features?.includes("IP_ALLOWLIST"),
|
||||
});
|
||||
}
|
||||
|
||||
interface SetIpAllowlistInput {
|
||||
@@ -1364,9 +1362,9 @@ export interface EngineContractSubscription {
|
||||
|
||||
export function useEngineContractSubscription(instance: string) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
return useQuery(
|
||||
engineKeys.contractSubscriptions(instance),
|
||||
async () => {
|
||||
return useQuery({
|
||||
queryKey: engineKeys.contractSubscriptions(instance),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`${instance}contract-subscriptions/get-all`, {
|
||||
method: "GET",
|
||||
headers: getEngineRequestHeaders(token),
|
||||
@@ -1375,10 +1373,9 @@ export function useEngineContractSubscription(instance: string) {
|
||||
const json = await res.json();
|
||||
return json.result as EngineContractSubscription[];
|
||||
},
|
||||
{
|
||||
enabled: !!instance && !!token,
|
||||
},
|
||||
);
|
||||
|
||||
enabled: !!instance && !!token,
|
||||
});
|
||||
}
|
||||
|
||||
export interface AddContractSubscriptionInput {
|
||||
@@ -1464,9 +1461,9 @@ export function useEngineSubscriptionsLastBlock(
|
||||
) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
|
||||
return useQuery(
|
||||
engineKeys.contractSubscriptionsLastBlock(instanceUrl, chainId),
|
||||
async () => {
|
||||
return useQuery({
|
||||
queryKey: engineKeys.contractSubscriptionsLastBlock(instanceUrl, chainId),
|
||||
queryFn: async () => {
|
||||
const response = await fetch(
|
||||
`${instanceUrl}contract-subscriptions/last-block?chain=${chainId}`,
|
||||
{
|
||||
@@ -1478,11 +1475,10 @@ export function useEngineSubscriptionsLastBlock(
|
||||
const json = await response.json();
|
||||
return json.result.lastBlock as number;
|
||||
},
|
||||
{
|
||||
enabled: !!instanceUrl && !!token,
|
||||
refetchInterval: autoUpdate ? 5_000 : false,
|
||||
},
|
||||
);
|
||||
|
||||
enabled: !!instanceUrl && !!token,
|
||||
refetchInterval: autoUpdate ? 5_000 : false,
|
||||
});
|
||||
}
|
||||
|
||||
export interface EngineResourceMetrics {
|
||||
@@ -1499,9 +1495,9 @@ export interface EngineResourceMetrics {
|
||||
export function useEngineSystemMetrics(engineId: string) {
|
||||
const [enabled, setEnabled] = useState(true);
|
||||
|
||||
return useQuery(
|
||||
engineKeys.systemMetrics(engineId),
|
||||
async () => {
|
||||
return useQuery({
|
||||
queryKey: engineKeys.systemMetrics(engineId),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(
|
||||
`${THIRDWEB_API_HOST}/v1/engine/${engineId}/metrics`,
|
||||
);
|
||||
@@ -1512,10 +1508,9 @@ export function useEngineSystemMetrics(engineId: string) {
|
||||
const json = (await res.json()) as EngineResourceMetrics;
|
||||
return json;
|
||||
},
|
||||
{
|
||||
// Poll every 5s unless disabled.
|
||||
enabled,
|
||||
refetchInterval: 5_000,
|
||||
},
|
||||
);
|
||||
|
||||
// Poll every 5s unless disabled.
|
||||
enabled,
|
||||
refetchInterval: 5_000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useSupportedChains } from "@thirdweb-dev/react";
|
||||
import { useFavouriteChainIds } from "app/(dashboard)/(chain)/components/client/star-button";
|
||||
import { useSupportedChains } from "hooks/chains/configureChains";
|
||||
import { useMemo } from "react";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
export type GasEstimate = { gasPrice: number; ethPrice: number };
|
||||
|
||||
export function useGas() {
|
||||
return useQuery(
|
||||
["gas-price", "ethereum"],
|
||||
async () => {
|
||||
const res = await fetch("/api/gas");
|
||||
return res.json() as Promise<GasEstimate>;
|
||||
},
|
||||
{
|
||||
refetchInterval: 60_000,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -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,47 +1,35 @@
|
||||
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 () => {
|
||||
return useQuery({
|
||||
queryKey: ["dashboard-registry", walletAddress, "multichain-contract-list"],
|
||||
queryFn: async () => {
|
||||
invariant(walletAddress, "walletAddress is required");
|
||||
const contracts = await getAllMultichainRegistry({
|
||||
contract: registryContract,
|
||||
contract: MULTICHAIN_REGISTRY_CONTRACT,
|
||||
address: walletAddress,
|
||||
});
|
||||
|
||||
return contracts;
|
||||
},
|
||||
{
|
||||
enabled: !!walletAddress,
|
||||
},
|
||||
);
|
||||
|
||||
enabled: !!walletAddress,
|
||||
});
|
||||
}
|
||||
|
||||
interface Options {
|
||||
@@ -97,28 +85,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
@@ -1,7 +1,6 @@
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { ChainLiveStats } from "../client/live-stats";
|
||||
import { SectionTitle } from "./SectionTitle";
|
||||
|
||||
+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 });
|
||||
|
||||
@@ -45,13 +45,12 @@ export default async function Page(props: {
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Buy Funds */}
|
||||
{chain.services.find((c) => c.service === "pay" && c.enabled) && (
|
||||
{/* Faucet / Buy Funds */}
|
||||
{chain.testnet ? (
|
||||
<FaucetSection chain={chain} />
|
||||
) : chain.services.find((c) => c.service === "pay" && c.enabled) ? (
|
||||
<BuyFundsSection chain={chain} />
|
||||
)}
|
||||
|
||||
{/* Faucet */}
|
||||
{chain.testnet && <FaucetSection chain={chain} />}
|
||||
) : null}
|
||||
|
||||
{/* Chain Overview */}
|
||||
<ChainOverviewSection chain={chain} />
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { type SortBy, fetchTopContracts } from "lib/search";
|
||||
import { CircleAlertIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
import { TrendingContractSection } from "../../../../trending/components/trending-table";
|
||||
import { getChain } from "../../../utils";
|
||||
|
||||
|
||||
@@ -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, {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 166 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 695 KiB |
@@ -2,6 +2,8 @@ import "server-only";
|
||||
|
||||
import { redirect } from "next/navigation";
|
||||
import type { ChainCTAProps } from "./[chain_id]/(chainPage)/components/server/cta-card";
|
||||
import zeroGCTA from "./temp-assets/0gCTA.png";
|
||||
import zeroGBanner from "./temp-assets/0gLabsBanner.png";
|
||||
import alephZeroBaner from "./temp-assets/AlephZeroBanner.jpg";
|
||||
import alephZeroCTA from "./temp-assets/AlephZeroCTA.jpg";
|
||||
import ancient8Banner from "./temp-assets/ancient8-banner.png";
|
||||
@@ -302,13 +304,13 @@ const chainMetaRecord = {
|
||||
},
|
||||
//0G-Newton-Testnet
|
||||
16600: {
|
||||
headerImgUrl: thirdwebBanner.src,
|
||||
headerImgUrl: zeroGBanner.src,
|
||||
about:
|
||||
"0G is an infinitely scalable data availability layer and data storage system that provides the necessary infrastructure to scale Web3 and bring novel use cases on-chain.",
|
||||
"ZeroGravity (0G) is the first infinitely scalable and decentralized data availability layer with a built-in general-purpose storage layer.",
|
||||
cta: {
|
||||
backgroundImageUrl: thirdwebCTA.src,
|
||||
title: "",
|
||||
buttonLink: "https://0g.ai/",
|
||||
backgroundImageUrl: zeroGCTA.src,
|
||||
title: "Unlock web3's full potential with 0G",
|
||||
buttonLink: "https://docs.0g.ai/0g-doc",
|
||||
buttonText: "Learn more",
|
||||
},
|
||||
},
|
||||
|
||||
+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";
|
||||
|
||||
@@ -7,20 +7,15 @@ const SIDEBAR_SECTIONS = [
|
||||
title: "Connect",
|
||||
items: [
|
||||
{
|
||||
id: "playground",
|
||||
title: "Playground",
|
||||
href: "/dashboard/connect/playground",
|
||||
id: "embedded-wallets",
|
||||
title: "In-App Wallets",
|
||||
href: "/dashboard/connect/in-app-wallets",
|
||||
},
|
||||
{
|
||||
id: "analytics",
|
||||
title: "Analytics",
|
||||
href: "/dashboard/connect/analytics",
|
||||
},
|
||||
{
|
||||
id: "embedded-wallets",
|
||||
title: "In-App Wallets",
|
||||
href: "/dashboard/connect/in-app-wallets",
|
||||
},
|
||||
{
|
||||
id: "ecosystem",
|
||||
title: "Ecosystem Wallets",
|
||||
@@ -36,6 +31,11 @@ const SIDEBAR_SECTIONS = [
|
||||
title: "Pay",
|
||||
href: "/dashboard/connect/pay",
|
||||
},
|
||||
{
|
||||
id: "playground",
|
||||
title: "Playground",
|
||||
href: "https://playground.thirdweb.com/connect/sign-in/button",
|
||||
},
|
||||
],
|
||||
},
|
||||
] satisfies Array<SidebarNavSection>;
|
||||
|
||||
+1
-4
@@ -1,6 +1,5 @@
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -247,9 +246,7 @@ function CreateWebhookButton(props: PropsWithChildren<WebhooksPageProps>) {
|
||||
|
||||
{/* Note: this is a "fake" form field since there is nothing to select yet */}
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Event Type <Badge variant="outline">Coming Soon</Badge>
|
||||
</FormLabel>
|
||||
<FormLabel>Event Type</FormLabel>
|
||||
<Select disabled defaultValue="purchase_complete">
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Purchase Complete" />
|
||||
|
||||
@@ -79,3 +79,6 @@ export default async function DashboardConnectPayPage() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// because cookies() is used
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -73,7 +73,7 @@ function prepareEmailBody(
|
||||
}
|
||||
|
||||
export async function createTicketAction(
|
||||
previousState: State,
|
||||
_previousState: State,
|
||||
formData: FormData,
|
||||
) {
|
||||
const cookieManager = cookies();
|
||||
|
||||
@@ -21,7 +21,7 @@ export const HexConverter = () => {
|
||||
setHex(toHex(args.dec));
|
||||
setDec(args.dec);
|
||||
}
|
||||
} catch (e) {}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
+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";
|
||||
|
||||
@@ -26,7 +26,7 @@ export const WeiConverter = () => {
|
||||
setWei(_wei.toString());
|
||||
setGwei(toTokens(_wei, 9));
|
||||
setEther(toEther(_wei));
|
||||
} catch (e) {}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -141,7 +141,7 @@ function isValidRedirectPath(encodedPath: string): boolean {
|
||||
// ensure the path always starts with a _single_ slash
|
||||
// dobule slash could be interpreted as `//example.com` which is not allowed
|
||||
return decodedPath.startsWith("/") && !decodedPath.startsWith("//");
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// If decoding fails, return false
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ export const slides: Slide[] = [
|
||||
<ListItem>
|
||||
<TrackedLink
|
||||
category={TRACK_CATEGORY}
|
||||
href="/dashboard/connect/playground"
|
||||
href="https://playground.thirdweb.com/connect/sign-in/button"
|
||||
isExternal
|
||||
label="auth"
|
||||
color="blue.500"
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
import { useTheme } from "next-themes";
|
||||
import { useId, useMemo } from "react";
|
||||
import {
|
||||
Bar,
|
||||
BarChart as RechartsBarChart,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
import { CustomToolTip } from "./custom-tooltip";
|
||||
|
||||
type GenericDataType = Record<string, string | number>;
|
||||
|
||||
type IndexType = "date";
|
||||
|
||||
interface BarChartProps<
|
||||
TData extends GenericDataType,
|
||||
TIndexKey extends keyof TData,
|
||||
> {
|
||||
data: TData[];
|
||||
index: {
|
||||
id: TIndexKey;
|
||||
label?: string;
|
||||
type?: IndexType;
|
||||
format?: (index: TData[TIndexKey]) => string;
|
||||
};
|
||||
|
||||
categories: Array<{
|
||||
id: keyof TData;
|
||||
label?: string;
|
||||
color?: string;
|
||||
format?: (value: number) => string;
|
||||
}>;
|
||||
|
||||
showXAxis?: boolean;
|
||||
showYAxis?: boolean;
|
||||
}
|
||||
|
||||
export const BarChart = <
|
||||
TData extends GenericDataType,
|
||||
TIndexKey extends keyof TData,
|
||||
>({
|
||||
data,
|
||||
index,
|
||||
categories,
|
||||
showXAxis,
|
||||
showYAxis,
|
||||
}: BarChartProps<TData, TIndexKey>) => {
|
||||
const { theme } = useTheme();
|
||||
const id = useId();
|
||||
|
||||
const defaultBarColor = useMemo(() => {
|
||||
if (theme === "light") {
|
||||
return "#649CDD";
|
||||
}
|
||||
|
||||
return "#3682DA";
|
||||
}, [theme]);
|
||||
|
||||
if (!data.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<RechartsBarChart data={data}>
|
||||
<defs>
|
||||
{categories.map((cat) => (
|
||||
<linearGradient
|
||||
key={`${cat.id as string}`}
|
||||
id={`bar_color_${id}_${cat.id as string}`}
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="100%"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop
|
||||
offset="0"
|
||||
stopColor={cat.color || defaultBarColor}
|
||||
stopOpacity={1}
|
||||
/>
|
||||
<stop
|
||||
offset="1"
|
||||
stopColor={cat.color || defaultBarColor}
|
||||
stopOpacity={0.75}
|
||||
/>
|
||||
</linearGradient>
|
||||
))}
|
||||
</defs>
|
||||
|
||||
{categories.map((cat) => (
|
||||
<Bar
|
||||
key={`${cat.id as string}`}
|
||||
dataKey={cat.id as string}
|
||||
stackId="a"
|
||||
stroke={cat.color || defaultBarColor}
|
||||
fill={`url(#bar_color_${id}_${cat.id as string})`}
|
||||
strokeWidth={0}
|
||||
/>
|
||||
))}
|
||||
<Tooltip
|
||||
wrapperStyle={{ outline: "none" }}
|
||||
content={({ active, payload }) => {
|
||||
const payloadKey = payload?.[0]?.dataKey;
|
||||
const category = categories.find((cat) => cat.id === payloadKey);
|
||||
return (
|
||||
<CustomToolTip
|
||||
active={active}
|
||||
payload={payload}
|
||||
valueLabel={category?.label || ""}
|
||||
valueFormatter={category?.format}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
cursor={{
|
||||
stroke: defaultBarColor,
|
||||
fill: defaultBarColor,
|
||||
opacity: 0.2,
|
||||
strokeDasharray: 2,
|
||||
strokeWidth: 0,
|
||||
}}
|
||||
/>
|
||||
|
||||
<XAxis
|
||||
hide={!showXAxis}
|
||||
dataKey={index.id as string}
|
||||
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
|
||||
}
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
fontFamily: "var(--chakra-fonts-body)",
|
||||
}}
|
||||
stroke="var(--chakra-colors-paragraph)"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
interval="preserveStartEnd"
|
||||
minTickGap={5}
|
||||
domain={["dataMin - 86400000", "dataMax + 86400000"]}
|
||||
type="number"
|
||||
tick={{ transform: "translate(0, 6)" }}
|
||||
ticks={[
|
||||
data[0][index.id],
|
||||
data[Math.floor(data.length / 2)][index.id],
|
||||
data[data.length - 1][index.id],
|
||||
]}
|
||||
/>
|
||||
|
||||
<YAxis
|
||||
hide={!showYAxis}
|
||||
width={60}
|
||||
tickFormatter={(payload) => {
|
||||
const category = categories[0];
|
||||
return category?.format
|
||||
? category.format(payload)
|
||||
: payload.toLocaleString();
|
||||
}}
|
||||
className="font-sans text-xs"
|
||||
stroke="hsl(var(--muted-foreground))"
|
||||
type="number"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
</RechartsBarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
};
|
||||
@@ -1,26 +1,21 @@
|
||||
import { isProd } from "@/constants/env";
|
||||
import {
|
||||
type EVMContractInfo,
|
||||
EVMContractInfoProvider,
|
||||
} from "@3rdweb-sdk/react";
|
||||
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
|
||||
import { Flex, SimpleGrid } from "@chakra-ui/react";
|
||||
import { createAsyncStoragePersister } from "@tanstack/query-async-storage-persister";
|
||||
import {
|
||||
type DehydratedState,
|
||||
Hydrate,
|
||||
QueryClient,
|
||||
QueryClientProvider,
|
||||
} from "@tanstack/react-query";
|
||||
import {
|
||||
PersistQueryClientProvider,
|
||||
type Persister,
|
||||
} from "@tanstack/react-query-persist-client";
|
||||
import { shouldNeverPersistQuery } from "@thirdweb-dev/react";
|
||||
import { ConfigureNetworkModal } from "components/configure-networks/ConfigureNetworkModal";
|
||||
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";
|
||||
@@ -30,66 +25,12 @@ import {
|
||||
useIsNetworkConfigModalOpen,
|
||||
useSetIsNetworkConfigModalOpen,
|
||||
} from "hooks/networkConfigModal";
|
||||
import { del, get, set } from "idb-keyval";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import { Heading } from "tw-components";
|
||||
import type { ComponentWithChildren } from "types/component-with-children";
|
||||
import { bigNumberReplacer } from "utils/bignumber";
|
||||
import { isBrowser } from "utils/isBrowser";
|
||||
import { DashboardThirdwebProvider } from "./providers";
|
||||
|
||||
const __CACHE_BUSTER = "3.14.40-nightly-1e6f9dcc-20230831023648";
|
||||
|
||||
interface AsyncStorage {
|
||||
getItem: (key: string) => Promise<string | null>;
|
||||
setItem: (key: string, value: string) => Promise<void>;
|
||||
removeItem: (key: string) => Promise<void>;
|
||||
}
|
||||
|
||||
let currStorage: AsyncStorage | undefined;
|
||||
|
||||
function getStorage() {
|
||||
if (!isBrowser()) {
|
||||
return undefined;
|
||||
}
|
||||
if (currStorage) {
|
||||
return currStorage;
|
||||
}
|
||||
|
||||
currStorage = {
|
||||
getItem: async (key) => {
|
||||
const i = await get(key);
|
||||
if (!i) {
|
||||
return null;
|
||||
}
|
||||
return i as string | null;
|
||||
},
|
||||
setItem: set,
|
||||
removeItem: del,
|
||||
} as AsyncStorage;
|
||||
return currStorage;
|
||||
}
|
||||
|
||||
const persister: Persister = createAsyncStoragePersister({
|
||||
storage: getStorage(),
|
||||
serialize: (data) => {
|
||||
return JSON.stringify(
|
||||
{
|
||||
...data,
|
||||
clientState: {
|
||||
...data.clientState,
|
||||
queries: data.clientState.queries.filter(
|
||||
(q) => !shouldNeverPersistQuery(q.queryKey),
|
||||
),
|
||||
},
|
||||
},
|
||||
bigNumberReplacer,
|
||||
);
|
||||
},
|
||||
key: "tw-query-cache",
|
||||
});
|
||||
|
||||
interface AppLayoutProps extends AppShellProps {
|
||||
dehydratedState?: DehydratedState;
|
||||
contractInfo?: EVMContractInfo;
|
||||
@@ -112,16 +53,7 @@ export const AppLayout: ComponentWithChildren<AppLayoutProps> = (props) => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<PersistQueryClientProvider
|
||||
client={queryClient}
|
||||
persistOptions={{
|
||||
persister,
|
||||
buster: __CACHE_BUSTER,
|
||||
dehydrateOptions: {
|
||||
shouldDehydrateQuery: (q) => !shouldNeverPersistQuery(q.queryKey),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Hydrate state={props.dehydratedState}>
|
||||
<ErrorProvider>
|
||||
<AllChainsProvider>
|
||||
@@ -143,7 +75,7 @@ export const AppLayout: ComponentWithChildren<AppLayoutProps> = (props) => {
|
||||
</AllChainsProvider>
|
||||
</ErrorProvider>
|
||||
</Hydrate>
|
||||
</PersistQueryClientProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
@@ -414,7 +417,7 @@ const MismatchNotice: React.FC<{
|
||||
try {
|
||||
await switchNetwork(chainV5);
|
||||
onClose(true);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// failed to switch network
|
||||
onClose(false);
|
||||
}
|
||||
@@ -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";
|
||||
@@ -14,7 +15,7 @@ export const RpcInput: React.FC<{
|
||||
try {
|
||||
new URL(value);
|
||||
return true;
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ const showcaseMenus = [
|
||||
{
|
||||
id: "connect-icon",
|
||||
title: "Onboard users",
|
||||
href: "/dashboard/connect/playground",
|
||||
href: "https://playground.thirdweb.com/connect/sign-in/button",
|
||||
description: "Onboard anyone with every sign-in option for web2 & web3.",
|
||||
image: require("../../../public/assets/product-pages/connect/icon-connect.png"),
|
||||
},
|
||||
|
||||
+15
-11
@@ -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"
|
||||
@@ -454,7 +458,7 @@ const CustomContractForm: React.FC<CustomContractFormProps> = ({
|
||||
contractAddress: deployedContractAddress,
|
||||
chainId: selectedChain,
|
||||
});
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
|
||||
+59
-73
@@ -2,10 +2,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { Flex, FormControl, InputGroup } from "@chakra-ui/react";
|
||||
import { SolidityInput } from "contract-ui/components/solidity-inputs";
|
||||
import { useState } from "react";
|
||||
import { FormErrorMessage, FormHelperText, FormLabel } from "tw-components";
|
||||
import { DynamicHeight } from "../../../@/components/ui/DynamicHeight";
|
||||
import { Switch } from "../../../@/components/ui/switch";
|
||||
import { useDefaultForwarders } from "../hooks";
|
||||
import { Fieldset } from "./common";
|
||||
import type { CustomContractDeploymentForm } from "./custom-contract";
|
||||
@@ -18,86 +15,75 @@ export const TrustedForwardersFieldset: React.FC<
|
||||
TrustedForwardersFieldsetProps
|
||||
> = ({ form }) => {
|
||||
const defaultForwarders = useDefaultForwarders();
|
||||
const [showFull, setShowFull] = useState(false);
|
||||
|
||||
return (
|
||||
<Fieldset legend="Gasless">
|
||||
<DynamicHeight>
|
||||
<FormControl
|
||||
isRequired
|
||||
isInvalid={
|
||||
!!form.getFieldState(
|
||||
"deployParams._trustedForwarders",
|
||||
form.formState,
|
||||
).error
|
||||
}
|
||||
>
|
||||
<div className="flex gap-6 justify-between items-center">
|
||||
{/* left */}
|
||||
<div>
|
||||
<FormLabel>Trusted Forwarders</FormLabel>
|
||||
<FormControl
|
||||
isRequired
|
||||
isInvalid={
|
||||
!!form.getFieldState(
|
||||
"deployParams._trustedForwarders",
|
||||
form.formState,
|
||||
).error
|
||||
}
|
||||
>
|
||||
<div className="flex gap-6 justify-between items-center">
|
||||
{/* left */}
|
||||
<div>
|
||||
<FormLabel>Trusted Forwarders</FormLabel>
|
||||
|
||||
<FormHelperText className="!text-sm text-muted-foreground">
|
||||
<span className="text-muted-foreground text-sm mb-1 block">
|
||||
Trusted forwarder addresses to enable ERC-2771 transactions
|
||||
(i.e. gasless).
|
||||
</span>
|
||||
<FormHelperText className="!text-sm text-muted-foreground">
|
||||
<span className="text-muted-foreground text-sm mb-1 block">
|
||||
Trusted forwarder addresses to enable ERC-2771 transactions
|
||||
(i.e. gasless).
|
||||
</span>
|
||||
|
||||
<span className="text-muted-foreground text-sm block">
|
||||
You can provide your own forwarder, or click the button below
|
||||
to use default forwarders provided by thirdweb. Leave empty if
|
||||
not needed.
|
||||
</span>
|
||||
</FormHelperText>
|
||||
</div>
|
||||
{/* Right */}
|
||||
<Switch checked={showFull} onCheckedChange={setShowFull} />
|
||||
<span className="text-muted-foreground text-sm block">
|
||||
You can provide your own forwarder, or click the button below to
|
||||
use default forwarders provided by thirdweb. Leave empty if not
|
||||
needed.
|
||||
</span>
|
||||
</FormHelperText>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={
|
||||
!showFull
|
||||
? "hidden"
|
||||
: "block pt-3 fade-in-0 animate-in duration-400"
|
||||
}
|
||||
>
|
||||
<InputGroup size="md">
|
||||
<Flex flexDir="column" w="full">
|
||||
<SolidityInput
|
||||
value={form.watch("deployParams._trustedForwarders")}
|
||||
solidityType="address[]"
|
||||
{...form.register("deployParams._trustedForwarders")}
|
||||
/>
|
||||
</Flex>
|
||||
</InputGroup>
|
||||
<div className="block pt-3 fade-in-0 animate-in duration-400">
|
||||
<InputGroup size="md">
|
||||
<Flex flexDir="column" w="full">
|
||||
<SolidityInput
|
||||
value={form.watch("deployParams._trustedForwarders")}
|
||||
solidityType="address[]"
|
||||
{...form.register("deployParams._trustedForwarders")}
|
||||
/>
|
||||
</Flex>
|
||||
</InputGroup>
|
||||
|
||||
<ToolTipLabel label="Click to apply default trusted forwarders">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="mt-2"
|
||||
onClick={() =>
|
||||
form.setValue(
|
||||
"deployParams._trustedForwarders",
|
||||
JSON.stringify(defaultForwarders.data),
|
||||
)
|
||||
}
|
||||
>
|
||||
Get Default
|
||||
</Button>
|
||||
</ToolTipLabel>
|
||||
|
||||
<FormErrorMessage>
|
||||
{
|
||||
form.getFieldState(
|
||||
<ToolTipLabel label="Click to apply default trusted forwarders">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="mt-2"
|
||||
onClick={() =>
|
||||
form.setValue(
|
||||
"deployParams._trustedForwarders",
|
||||
form.formState,
|
||||
).error?.message
|
||||
JSON.stringify(defaultForwarders.data),
|
||||
)
|
||||
}
|
||||
</FormErrorMessage>
|
||||
</div>
|
||||
</FormControl>
|
||||
</DynamicHeight>
|
||||
>
|
||||
Get Default
|
||||
</Button>
|
||||
</ToolTipLabel>
|
||||
|
||||
<FormErrorMessage>
|
||||
{
|
||||
form.getFieldState(
|
||||
"deployParams._trustedForwarders",
|
||||
form.formState,
|
||||
).error?.message
|
||||
}
|
||||
</FormErrorMessage>
|
||||
</div>
|
||||
</FormControl>
|
||||
</Fieldset>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { contractKeys, networkKeys } from "@3rdweb-sdk/react";
|
||||
import {
|
||||
contractKeys,
|
||||
networkKeys,
|
||||
useDashboardEVMChainId,
|
||||
} from "@3rdweb-sdk/react";
|
||||
import { useMutationWithInvalidate } from "@3rdweb-sdk/react/hooks/query/useQueryWithNetwork";
|
||||
import {
|
||||
type QueryClient,
|
||||
@@ -6,15 +10,13 @@ import {
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import { useSDK, useSDKChainId, useSigner } from "@thirdweb-dev/react";
|
||||
import { useSDK, useSigner } from "@thirdweb-dev/react";
|
||||
import {
|
||||
type Abi,
|
||||
type ContractInfoSchema,
|
||||
type DeploymentTransaction,
|
||||
type ExtraPublishMetadata,
|
||||
type FeatureName,
|
||||
type FeatureWithEnabled,
|
||||
type ProfileMetadata,
|
||||
type PublishedContract,
|
||||
type ThirdwebSDK,
|
||||
detectFeatures,
|
||||
@@ -33,13 +35,12 @@ import {
|
||||
getZkTransactionsForDeploy,
|
||||
zkDeployContractFromUri,
|
||||
} from "@thirdweb-dev/sdk/evm/zksync";
|
||||
import type { SnippetApiResponse } from "components/contract-tabs/code/types";
|
||||
import type { ProfileMetadata, ProfileMetadataInput } from "constants/schemas";
|
||||
import type { providers } from "ethers";
|
||||
import { useSupportedChain } from "hooks/chains/configureChains";
|
||||
import { isEnsName, resolveEns } from "lib/ens";
|
||||
import { getDashboardChainRpc } from "lib/rpc";
|
||||
import { StorageSingleton, getThirdwebSDK } from "lib/sdk";
|
||||
import type { StaticImageData } from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
@@ -57,7 +58,6 @@ import {
|
||||
import { isAddress } from "thirdweb/utils";
|
||||
import invariant from "tiny-invariant";
|
||||
import { Web3Provider } from "zksync-ethers";
|
||||
import type { z } from "zod";
|
||||
import type { CustomContractDeploymentFormData } from "./contract-deploy-form/custom-contract";
|
||||
import type {
|
||||
DeployModalStep,
|
||||
@@ -67,31 +67,6 @@ import { uploadContractMetadata } from "./contract-deploy-form/deploy-form-utils
|
||||
import type { ContractId } from "./types";
|
||||
import { addContractToMultiChainRegistry } from "./utils";
|
||||
|
||||
interface ContractPublishMetadata {
|
||||
image: string | StaticImageData;
|
||||
name: string;
|
||||
description?: string;
|
||||
abi?: Abi;
|
||||
bytecode?: string;
|
||||
deployDisabled?: boolean;
|
||||
info?: z.infer<typeof ContractInfoSchema>;
|
||||
licenses?: string[];
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
compilerMetadata?: Record<string, any>;
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
analytics?: Record<string, any>;
|
||||
}
|
||||
|
||||
interface RawPredeployMetadata {
|
||||
name: string;
|
||||
metadataUri: string;
|
||||
bytecodeUri: string;
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
analytics?: Record<string, any>;
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
compilers?: Record<string, any>;
|
||||
}
|
||||
|
||||
function isChainIdZkSync(chainId?: number) {
|
||||
switch (chainId) {
|
||||
case zkSync.id:
|
||||
@@ -140,13 +115,12 @@ export async function fetchRawPredeployMetadataFromURI(contractId: ContractId) {
|
||||
}
|
||||
|
||||
export function useContractRawPredeployMetadataFromURI(contractId: ContractId) {
|
||||
return useQuery<RawPredeployMetadata>(
|
||||
["raw-predeploy-metadata", contractId],
|
||||
() => fetchRawPredeployMetadataFromURI(contractId),
|
||||
{
|
||||
enabled: !!contractId,
|
||||
},
|
||||
);
|
||||
return useQuery({
|
||||
queryKey: ["raw-predeploy-metadata", contractId],
|
||||
queryFn: () => fetchRawPredeployMetadataFromURI(contractId),
|
||||
|
||||
enabled: !!contractId,
|
||||
});
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
@@ -200,13 +174,11 @@ export async function fetchContractPublishMetadataFromURI(
|
||||
}
|
||||
|
||||
export function useContractPublishMetadataFromURI(contractId: ContractId) {
|
||||
return useQuery<ContractPublishMetadata>(
|
||||
["publish-metadata", contractId],
|
||||
() => fetchContractPublishMetadataFromURI(contractId),
|
||||
{
|
||||
enabled: !!contractId,
|
||||
},
|
||||
);
|
||||
return useQuery({
|
||||
queryKey: ["publish-metadata", contractId],
|
||||
queryFn: () => fetchContractPublishMetadataFromURI(contractId),
|
||||
enabled: !!contractId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useDefaultForwarders() {
|
||||
@@ -214,11 +186,13 @@ export function useDefaultForwarders() {
|
||||
const provider = sdk?.getProvider();
|
||||
invariant(provider, "Require provider");
|
||||
|
||||
const chainId = useSDKChainId();
|
||||
const chainId = useDashboardEVMChainId();
|
||||
|
||||
return useQuery(["default-forwarders", chainId], async () => {
|
||||
const forwarders = await getTrustedForwarders(provider, StorageSingleton);
|
||||
return forwarders;
|
||||
return useQuery({
|
||||
queryKey: ["default-forwarders", chainId],
|
||||
queryFn: () => {
|
||||
return getTrustedForwarders(provider, StorageSingleton);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -227,9 +201,9 @@ export function useDefaultForwarders() {
|
||||
export function useContractPrePublishMetadata(uri: string, address?: string) {
|
||||
const contractIdIpfsHash = toContractIdIpfsHash(uri);
|
||||
|
||||
return useQuery(
|
||||
["pre-publish-metadata", uri, address],
|
||||
async () => {
|
||||
return useQuery({
|
||||
queryKey: ["pre-publish-metadata", uri, address],
|
||||
queryFn: async () => {
|
||||
invariant(address, "address is not defined");
|
||||
// TODO: Make this nicer.
|
||||
invariant(uri !== "ipfs://undefined", "uri can't be undefined");
|
||||
@@ -241,10 +215,9 @@ export function useContractPrePublishMetadata(uri: string, address?: string) {
|
||||
?.getPublisher()
|
||||
.fetchPrePublishMetadata(contractIdIpfsHash, address);
|
||||
},
|
||||
{
|
||||
enabled: !!uri && !!address,
|
||||
},
|
||||
);
|
||||
|
||||
enabled: !!uri && !!address,
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchFullPublishMetadata(
|
||||
@@ -276,9 +249,9 @@ export function useContractFullPublishMetadata(uri: string) {
|
||||
);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useQuery(
|
||||
["full-publish-metadata", uri],
|
||||
async () => {
|
||||
return useQuery({
|
||||
queryKey: ["full-publish-metadata", uri],
|
||||
queryFn: async () => {
|
||||
invariant(sdk, "sdk is not defined");
|
||||
// TODO: Make this nicer.
|
||||
invariant(uri !== "ipfs://undefined", "uri can't be undefined");
|
||||
@@ -288,10 +261,9 @@ export function useContractFullPublishMetadata(uri: string) {
|
||||
queryClient,
|
||||
);
|
||||
},
|
||||
{
|
||||
enabled: !!uri && !!sdk,
|
||||
},
|
||||
);
|
||||
|
||||
enabled: !!uri && !!sdk,
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchPublisherProfile(publisherAddress?: string | null) {
|
||||
@@ -300,7 +272,10 @@ async function fetchPublisherProfile(publisherAddress?: string | null) {
|
||||
getDashboardChainRpc(polygon.id, undefined),
|
||||
);
|
||||
invariant(publisherAddress, "address is not defined");
|
||||
return await sdk.getPublisher().getPublisherProfile(publisherAddress);
|
||||
return (await sdk
|
||||
.getPublisher()
|
||||
// todo: remove type-casting once we have replaced this method
|
||||
.getPublisherProfile(publisherAddress)) as ProfileMetadata;
|
||||
}
|
||||
|
||||
export function publisherProfileQuery(publisherAddress?: string) {
|
||||
@@ -372,29 +347,28 @@ export function useAllVersions(
|
||||
polygon.id,
|
||||
getDashboardChainRpc(polygon.id, undefined),
|
||||
);
|
||||
return useQuery(
|
||||
["all-releases", publisherAddress, contractName],
|
||||
() => fetchAllVersions(sdk, publisherAddress, contractName),
|
||||
{
|
||||
enabled: !!publisherAddress && !!contractName && !!sdk,
|
||||
},
|
||||
);
|
||||
return useQuery({
|
||||
queryKey: ["all-releases", publisherAddress, contractName],
|
||||
queryFn: () => fetchAllVersions(sdk, publisherAddress, contractName),
|
||||
|
||||
enabled: !!publisherAddress && !!contractName && !!sdk,
|
||||
});
|
||||
}
|
||||
|
||||
export function usePublishedContractsFromDeploy(
|
||||
contractAddress?: string,
|
||||
chainId?: number,
|
||||
) {
|
||||
const activeChainId = useSDKChainId();
|
||||
const activeChainId = useDashboardEVMChainId();
|
||||
const cId = chainId || activeChainId;
|
||||
const chainInfo = useSupportedChain(cId || -1);
|
||||
|
||||
return useQuery(
|
||||
(networkKeys.chain(cId) as readonly unknown[]).concat([
|
||||
return useQuery({
|
||||
queryKey: (networkKeys.chain(cId) as readonly unknown[]).concat([
|
||||
"release-from-deploy",
|
||||
contractAddress,
|
||||
]),
|
||||
async () => {
|
||||
queryFn: async () => {
|
||||
invariant(contractAddress, "contractAddress is not defined");
|
||||
invariant(cId, "chain not defined");
|
||||
|
||||
@@ -418,11 +392,10 @@ export function usePublishedContractsFromDeploy(
|
||||
.getPublisher()
|
||||
.resolvePublishMetadataFromCompilerMetadata(contractUri);
|
||||
},
|
||||
{
|
||||
enabled: !!contractAddress && !!cId && !!chainInfo,
|
||||
retry: false,
|
||||
},
|
||||
);
|
||||
|
||||
enabled: !!contractAddress && !!cId && !!chainInfo,
|
||||
retry: false,
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchPublishedContractInfo(
|
||||
@@ -439,13 +412,12 @@ export function usePublishedContractInfo(contract: PublishedContract) {
|
||||
polygon.id,
|
||||
getDashboardChainRpc(polygon.id, undefined),
|
||||
);
|
||||
return useQuery(
|
||||
["released-contract", contract],
|
||||
() => fetchPublishedContractInfo(sdk, contract),
|
||||
{
|
||||
enabled: !!contract,
|
||||
},
|
||||
);
|
||||
return useQuery({
|
||||
queryKey: ["released-contract", contract],
|
||||
queryFn: () => fetchPublishedContractInfo(sdk, contract),
|
||||
|
||||
enabled: !!contract,
|
||||
});
|
||||
}
|
||||
export function usePublishedContractFunctions(contract: PublishedContract) {
|
||||
const publishedContractInfo = usePublishedContractInfo(contract);
|
||||
@@ -569,7 +541,7 @@ export function useEditProfileMutation() {
|
||||
const address = useActiveAccount()?.address;
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (data: ProfileMetadata) => {
|
||||
async (data: ProfileMetadataInput) => {
|
||||
invariant(sdk, "sdk not provided");
|
||||
await sdk.getPublisher().updatePublisherProfile(data);
|
||||
},
|
||||
@@ -750,6 +722,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,
|
||||
@@ -887,7 +873,7 @@ export function useCustomContractDeployMutation(options: {
|
||||
});
|
||||
|
||||
deployStatusModal.nextStep();
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// failed to set metadata - for now just close the modal
|
||||
deployStatusModal.close();
|
||||
// not re-throwing the error, this is not technically a failure to deploy, just to set metadata - the contract is deployed already at this stage
|
||||
@@ -909,7 +895,7 @@ export function useCustomContractDeployMutation(options: {
|
||||
|
||||
deployStatusModal.nextStep();
|
||||
}
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// failed to add to dashboard - for now just close the modal
|
||||
deployStatusModal.close();
|
||||
router.replace(`/${chainId}/${contractAddress}`);
|
||||
@@ -935,9 +921,9 @@ export function useTransactionsForDeploy(publishMetadataOrUri: string) {
|
||||
const sdk = useSDK();
|
||||
const chainId = useActiveWalletChain()?.id;
|
||||
|
||||
const queryResult = useQuery<DeploymentTransaction[]>(
|
||||
["transactions-for-deploy", publishMetadataOrUri, chainId],
|
||||
async () => {
|
||||
const queryResult = useQuery<DeploymentTransaction[]>({
|
||||
queryKey: ["transactions-for-deploy", publishMetadataOrUri, chainId],
|
||||
queryFn: async () => {
|
||||
invariant(sdk, "sdk not provided");
|
||||
|
||||
// Handle separately for ZkSync
|
||||
@@ -951,10 +937,9 @@ export function useTransactionsForDeploy(publishMetadataOrUri: string) {
|
||||
: `ipfs://${publishMetadataOrUri}`,
|
||||
);
|
||||
},
|
||||
{
|
||||
enabled: !!publishMetadataOrUri && !!sdk,
|
||||
},
|
||||
);
|
||||
|
||||
enabled: !!publishMetadataOrUri && !!sdk,
|
||||
});
|
||||
|
||||
return queryResult;
|
||||
}
|
||||
@@ -1024,18 +1009,16 @@ export function usePublishedContractsQuery(
|
||||
getDashboardChainRpc(polygon.id, undefined),
|
||||
);
|
||||
const queryClient = useQueryClient();
|
||||
return useQuery<PublishedContractDetails[]>(
|
||||
["published-contracts", address, feature],
|
||||
() => {
|
||||
return useQuery<PublishedContractDetails[]>({
|
||||
queryKey: ["published-contracts", address, feature],
|
||||
queryFn: () => {
|
||||
invariant(sdk, "sdk not provided");
|
||||
return feature && feature.length > 0
|
||||
? fetchPublishedContractsWithFeature(sdk, queryClient, feature, address)
|
||||
: fetchPublishedContracts(sdk, queryClient, address);
|
||||
},
|
||||
{
|
||||
enabled: !!address && !!sdk,
|
||||
},
|
||||
);
|
||||
enabled: !!address && !!sdk,
|
||||
});
|
||||
}
|
||||
|
||||
const ALWAYS_SUGGESTED = ["ContractMetadata", "Permissions"];
|
||||
@@ -1163,42 +1146,15 @@ export function useContractEvents(abi: Abi) {
|
||||
return abi ? extractEventsFromAbi(abi) : undefined;
|
||||
}
|
||||
|
||||
// TODO: this points to very old snippets, we need to update this!
|
||||
export function useFeatureContractCodeSnippetQuery(language: string) {
|
||||
if (language === "javascript") {
|
||||
// biome-ignore lint/style/noParameterAssign: FIXME
|
||||
language = "sdk";
|
||||
}
|
||||
|
||||
if (language === "react-native") {
|
||||
// biome-ignore lint/style/noParameterAssign: FIXME
|
||||
language = "react";
|
||||
}
|
||||
|
||||
return useQuery(["feature-code-snippet", language], async () => {
|
||||
// only allow specific languages
|
||||
if (
|
||||
["go", "python", "react", "sdk", "unity"].includes(language) === false
|
||||
) {
|
||||
throw new Error("Invalid language");
|
||||
}
|
||||
const res = await fetch(
|
||||
`https://raw.githubusercontent.com/thirdweb-dev/docs/main/docs/feature_snippets_${language}.json`,
|
||||
);
|
||||
return (await res.json()) as SnippetApiResponse;
|
||||
});
|
||||
}
|
||||
|
||||
export function useCustomFactoryAbi(contractAddress: string, chainId: number) {
|
||||
const chain = useSupportedChain(chainId);
|
||||
return useQuery(
|
||||
["custom-factory-abi", { contractAddress, chainId }],
|
||||
async () => {
|
||||
return useQuery({
|
||||
queryKey: ["custom-factory-abi", { contractAddress, chainId }],
|
||||
queryFn: async () => {
|
||||
const sdk = getThirdwebSDK(chainId, getDashboardChainRpc(chainId, chain));
|
||||
return (await sdk.getContract(contractAddress)).abi;
|
||||
},
|
||||
{
|
||||
enabled: !!contractAddress && !!chainId,
|
||||
},
|
||||
);
|
||||
|
||||
enabled: !!contractAddress && !!chainId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -179,9 +179,9 @@ Deploy it in one click`,
|
||||
return url.href;
|
||||
}, [publishedContractName, currentRoute]);
|
||||
|
||||
const sources = useQuery(
|
||||
["sources", contract],
|
||||
async () => {
|
||||
const sources = useQuery({
|
||||
queryKey: ["sources", contract],
|
||||
queryFn: async () => {
|
||||
invariant(
|
||||
contractPublishMetadata.data?.compilerMetadata?.sources,
|
||||
"no compilerMetadata sources available",
|
||||
@@ -205,8 +205,8 @@ Deploy it in one click`,
|
||||
.slice()
|
||||
.reverse();
|
||||
},
|
||||
{ enabled: !!contractPublishMetadata.data?.compilerMetadata?.sources },
|
||||
);
|
||||
enabled: !!contractPublishMetadata.data?.compilerMetadata?.sources,
|
||||
});
|
||||
|
||||
const title = useMemo(() => {
|
||||
let clearType = "";
|
||||
|
||||
+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;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { SiMedium } from "@react-icons/all-files/si/SiMedium";
|
||||
import { SiReddit } from "@react-icons/all-files/si/SiReddit";
|
||||
import { SiTelegram } from "@react-icons/all-files/si/SiTelegram";
|
||||
import { SiTwitter } from "@react-icons/all-files/si/SiTwitter";
|
||||
import type { ProfileMetadata } from "@thirdweb-dev/sdk";
|
||||
import type { ProfileMetadata } from "constants/schemas";
|
||||
import { FiGlobe } from "react-icons/fi";
|
||||
import { LinkButton, TrackedIconButton } from "tw-components";
|
||||
import { hostnameEndsWith } from "../../../utils/url";
|
||||
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
import { SiDiscord } from "@react-icons/all-files/si/SiDiscord";
|
||||
import { SiGithub } from "@react-icons/all-files/si/SiGithub";
|
||||
import { SiTwitter } from "@react-icons/all-files/si/SiTwitter";
|
||||
import type { ProfileMetadata, ProfileMetadataInput } from "@thirdweb-dev/sdk";
|
||||
import { FileInput } from "components/shared/FileInput";
|
||||
import type { ProfileMetadata, ProfileMetadataInput } from "constants/schemas";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useImageFileOrUrl } from "hooks/useImageFileOrUrl";
|
||||
import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
|
||||
@@ -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";
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user