Compare commits
173
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8ebbe54d9 | ||
|
|
ca756819db | ||
|
|
7217e774b2 | ||
|
|
5de72c5ecd | ||
|
|
60c5c26583 | ||
|
|
dc74c3b4b9 | ||
|
|
69a921d01f | ||
|
|
9abdb92028 | ||
|
|
6ed2fd13de | ||
|
|
a22457a69c | ||
|
|
ae675db8c8 | ||
|
|
54c9333fc9 | ||
|
|
541cf0b3e9 | ||
|
|
c65022174d | ||
|
|
6818cbabf2 | ||
|
|
a7fb844d87 | ||
|
|
bf431961ec | ||
|
|
c5d4cab8f8 | ||
|
|
1ee0dffec4 | ||
|
|
33611409b0 | ||
|
|
ab55aec10c | ||
|
|
ca22e4659e | ||
|
|
411895c002 | ||
|
|
cfa1c20a91 | ||
|
|
2b16f90715 | ||
|
|
e8892b152f | ||
|
|
073b25afd7 | ||
|
|
4550bb2663 | ||
|
|
f5f8a40320 | ||
|
|
3569763501 | ||
|
|
1401f8d039 | ||
|
|
1616b7f619 | ||
|
|
aa548a359f | ||
|
|
eef52dfa7e | ||
|
|
19ec571500 | ||
|
|
47dce2e05c | ||
|
|
8b4695224c | ||
|
|
94e2d5b8dd | ||
|
|
40fa323294 | ||
|
|
2cebb420e6 | ||
|
|
e89ddb24f2 | ||
|
|
86f205e2aa | ||
|
|
6c277ae676 | ||
|
|
153651df35 | ||
|
|
8ba460b6fc | ||
|
|
1e61a9edfc | ||
|
|
d03e687aa9 | ||
|
|
e1c0dbe19e | ||
|
|
3e7df081d7 | ||
|
|
b6f189c220 | ||
|
|
fb4d0a26f1 | ||
|
|
bc345371ba | ||
|
|
7a8e4e5f86 | ||
|
|
6ba3b30b60 | ||
|
|
5ad442cf0c | ||
|
|
5756c74a71 | ||
|
|
a1cff7a579 | ||
|
|
eafaefec20 | ||
|
|
b81d7a1854 | ||
|
|
21cc45adc2 | ||
|
|
a67d342779 | ||
|
|
60edce49c0 | ||
|
|
20c0dca958 | ||
|
|
4b30b847e2 | ||
|
|
b2cc17fd1e | ||
|
|
ace1dc5b09 | ||
|
|
9f79277344 | ||
|
|
6e0fd205ff | ||
|
|
69b1ee9176 | ||
|
|
0a19142a24 | ||
|
|
feb4fd0908 | ||
|
|
1d8695eb42 | ||
|
|
858dc53a25 | ||
|
|
8997fac1b0 | ||
|
|
403e4224e4 | ||
|
|
de039ef462 | ||
|
|
35fd3bcafa | ||
|
|
80a22ff45e | ||
|
|
eefa2ab424 | ||
|
|
ab9a148b75 | ||
|
|
8bbee03c77 | ||
|
|
b38604cce8 | ||
|
|
59eda22043 | ||
|
|
577636c037 | ||
|
|
1d844ac5d6 | ||
|
|
c7be92f75b | ||
|
|
79ed399e56 | ||
|
|
ded5e22bb7 | ||
|
|
5029345461 | ||
|
|
c9ccc59464 | ||
|
|
dc05f2ea3c | ||
|
|
7d9986f066 | ||
|
|
bb6c71e968 | ||
|
|
89beb3c5d3 | ||
|
|
3b98fbf8bb | ||
|
|
8b5cb47339 | ||
|
|
0760706dbb | ||
|
|
eded4ff1be | ||
|
|
b705406c5d | ||
|
|
17374d15f2 | ||
|
|
79769d6859 | ||
|
|
b6d65cf1c4 | ||
|
|
a92617f0b0 | ||
|
|
8e82ec860c | ||
|
|
0b6239735e | ||
|
|
df4da68d68 | ||
|
|
94ba9cf7aa | ||
|
|
1b64d6eb2d | ||
|
|
f1afa3b52d | ||
|
|
dbb64ea190 | ||
|
|
eed734c766 | ||
|
|
a3a3062ae1 | ||
|
|
4ffcf305ab | ||
|
|
fe3f24e309 | ||
|
|
9f5510018e | ||
|
|
73894d23b9 | ||
|
|
4538441334 | ||
|
|
913f2e4366 | ||
|
|
da4ba21a52 | ||
|
|
8d9854daf1 | ||
|
|
9cbcbe7760 | ||
|
|
00f1d29654 | ||
|
|
b511a695f9 | ||
|
|
ea34a975ca | ||
|
|
a834199dac | ||
|
|
be7759d2fe | ||
|
|
15178b2f96 | ||
|
|
afd0ccfdfd | ||
|
|
07a5c98b5b | ||
|
|
51a28c4ed6 | ||
|
|
c290997b11 | ||
|
|
81b146a87a | ||
|
|
b10f306fba | ||
|
|
d1c03b0cbc | ||
|
|
9ae56bc765 | ||
|
|
0fdfb8aa77 | ||
|
|
46bf17c393 | ||
|
|
a6fe226239 | ||
|
|
7a3dff01cd | ||
|
|
e331e433ac | ||
|
|
23d80011de | ||
|
|
815f47cd12 | ||
|
|
22dc493e62 | ||
|
|
fbf26d82d5 | ||
|
|
d7f2249fb1 | ||
|
|
0682f26cb0 | ||
|
|
0e2b3df42a | ||
|
|
c33980352a | ||
|
|
a693958bf4 | ||
|
|
332536bdee | ||
|
|
4745786ba1 | ||
|
|
3cb1ab9f98 | ||
|
|
5c554730d0 | ||
|
|
6953d06246 | ||
|
|
4b5661b981 | ||
|
|
42a313f3b2 | ||
|
|
7fb5ce1cc3 | ||
|
|
ba86d51847 | ||
|
|
5866088894 | ||
|
|
ed0d215f4a | ||
|
|
c5c6f9d741 | ||
|
|
b14aa54a6e | ||
|
|
5d71415618 | ||
|
|
bfc4778334 | ||
|
|
f323f7c21c | ||
|
|
b2b5ae34e6 | ||
|
|
c3bcf82d22 | ||
|
|
d53a5c16c2 | ||
|
|
70b7b5c5ff | ||
|
|
5e2eec3b17 | ||
|
|
0fbce0617b | ||
|
|
53b24a2a0e | ||
|
|
3b689d4d48 |
+1
-1
@@ -12,7 +12,7 @@ packages/eslint-config-thirdweb/ @thirdweb-dev/core-platform @edwardysun
|
||||
packages/tw-tsconfig/ @thirdweb-dev/core-platform @edwardysun
|
||||
|
||||
# apps
|
||||
apps/ @thirdweb-dev/developer-tools @thirdweb-dev/core-platform @jakubkrehel
|
||||
apps/ @thirdweb-dev/developer-tools @thirdweb-dev/core-platform
|
||||
|
||||
# .github folder + .changeset + turbo config is owned by jonas for now
|
||||
scripts/ @jnsdls @joaquim-verges
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
---
|
||||
title: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"
|
||||
---
|
||||
<!--
|
||||
|
||||
## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"
|
||||
|
||||
If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):
|
||||
|
||||
## Notes for the reviewer
|
||||
|
||||
Anything important to call out? Be sure to also clarify these in your comments.
|
||||
|
||||
## How to test
|
||||
|
||||
Unit tests, playground, etc.
|
||||
|
||||
-->
|
||||
|
||||
@@ -2,7 +2,7 @@ name: Auto Author Assign
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened]
|
||||
types: [opened, reopened, ready_for_review, draft]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
Vendored
+5
-1
@@ -27,5 +27,9 @@
|
||||
},
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
}
|
||||
},
|
||||
"eslint.workingDirectories": [
|
||||
{ "pattern": "./packages/*/" },
|
||||
{ "pattern": "./apps/*/" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ module.exports = [
|
||||
// -- connect
|
||||
"/connect",
|
||||
"/connect/sign-in",
|
||||
"/connect/pay",
|
||||
// -- nebula
|
||||
"/nebula",
|
||||
// --insight
|
||||
@@ -26,7 +27,7 @@ module.exports = [
|
||||
"/solutions/defi",
|
||||
"/solutions/ecosystem",
|
||||
// -- campaigns --
|
||||
"/unlimited-wallets",
|
||||
// "/unlimited-wallets", -- OFF for now
|
||||
// -- TPP --
|
||||
"/trusted-partner-program",
|
||||
"/trusted-partner-program/:partner_slug",
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
"next": true,
|
||||
"ignore": [
|
||||
"src/@/components/ui/**",
|
||||
"src/components/notices/AnnouncementBanner.tsx"
|
||||
"src/components/notices/AnnouncementBanner.tsx",
|
||||
"src/components/cmd-k-search/index.tsx",
|
||||
"src/lib/search.ts"
|
||||
],
|
||||
"project": ["src/**"],
|
||||
"ignoreBinaries": ["only-allow", "biome"],
|
||||
|
||||
Vendored
-1
@@ -1,6 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference types="next/navigation-types/compat/navigation" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -115,6 +115,15 @@ const SENTRY_OPTIONS: SentryBuildOptions = {
|
||||
};
|
||||
|
||||
const baseNextConfig: NextConfig = {
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
productionBrowserSourceMaps: false,
|
||||
experimental: {
|
||||
webpackBuildWorker: true,
|
||||
webpackMemoryOptimizations: true,
|
||||
serverSourceMaps: false,
|
||||
},
|
||||
serverExternalPackages: ["pino-pretty"],
|
||||
async headers() {
|
||||
return [
|
||||
@@ -190,17 +199,9 @@ function getConfig(): NextConfig {
|
||||
withSentryConfig(
|
||||
{
|
||||
...baseNextConfig,
|
||||
experimental: {
|
||||
webpackBuildWorker: true,
|
||||
webpackMemoryOptimizations: true,
|
||||
},
|
||||
// @ts-expect-error - this is a valid option
|
||||
webpack: (config) => {
|
||||
if (config.cache) {
|
||||
config.cache = Object.freeze({
|
||||
type: "filesystem",
|
||||
maxMemoryGenerations: 0,
|
||||
});
|
||||
config.cache = Object.freeze({
|
||||
type: "memory",
|
||||
});
|
||||
|
||||
+45
-46
@@ -31,29 +31,29 @@
|
||||
"@marsidev/react-turnstile": "^1.1.0",
|
||||
"@n8tb1t/use-scroll-position": "^2.0.3",
|
||||
"@radix-ui/react-accordion": "^1.2.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.4",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.5",
|
||||
"@radix-ui/react-avatar": "^1.1.2",
|
||||
"@radix-ui/react-checkbox": "^1.1.3",
|
||||
"@radix-ui/react-dialog": "1.1.3",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.3",
|
||||
"@radix-ui/react-hover-card": "^1.1.3",
|
||||
"@radix-ui/react-dialog": "1.1.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.5",
|
||||
"@radix-ui/react-hover-card": "^1.1.5",
|
||||
"@radix-ui/react-label": "^2.1.1",
|
||||
"@radix-ui/react-popover": "^1.1.3",
|
||||
"@radix-ui/react-popover": "^1.1.5",
|
||||
"@radix-ui/react-progress": "^1.1.1",
|
||||
"@radix-ui/react-radio-group": "^1.2.2",
|
||||
"@radix-ui/react-select": "^2.1.3",
|
||||
"@radix-ui/react-select": "^2.1.5",
|
||||
"@radix-ui/react-separator": "^1.1.1",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-switch": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "1.1.5",
|
||||
"@sentry/nextjs": "8.45.1",
|
||||
"@shazow/whatsabi": "^0.18.0",
|
||||
"@tanstack/react-query": "5.62.7",
|
||||
"@radix-ui/react-tooltip": "1.1.7",
|
||||
"@sentry/nextjs": "8.52.0",
|
||||
"@shazow/whatsabi": "^0.19.0",
|
||||
"@tanstack/react-query": "5.65.1",
|
||||
"@tanstack/react-table": "^8.20.6",
|
||||
"@thirdweb-dev/service-utils": "workspace:*",
|
||||
"@vercel/functions": "^1.5.2",
|
||||
"@vercel/og": "^0.6.4",
|
||||
"abitype": "1.0.7",
|
||||
"@vercel/functions": "^1.6.0",
|
||||
"@vercel/og": "^0.6.5",
|
||||
"abitype": "1.0.8",
|
||||
"chakra-react-select": "^4.7.6",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -62,20 +62,19 @@
|
||||
"date-fns": "4.1.0",
|
||||
"fetch-event-stream": "0.1.5",
|
||||
"flat": "^6.0.1",
|
||||
"framer-motion": "11.15.0",
|
||||
"framer-motion": "12.0.6",
|
||||
"fuse.js": "7.0.0",
|
||||
"input-otp": "^1.4.1",
|
||||
"ioredis": "^5.4.1",
|
||||
"ipaddr.js": "^2.2.0",
|
||||
"lucide-react": "0.468.0",
|
||||
"next": "15.1.3",
|
||||
"lucide-react": "0.474.0",
|
||||
"next": "15.1.6",
|
||||
"next-plausible": "^3.12.4",
|
||||
"next-seo": "^6.5.0",
|
||||
"next-themes": "^0.4.4",
|
||||
"nextjs-toploader": "^1.6.12",
|
||||
"openapi-types": "^12.1.3",
|
||||
"p-limit": "^6.2.0",
|
||||
"papaparse": "^5.4.1",
|
||||
"papaparse": "^5.5.2",
|
||||
"pluralize": "^8.0.0",
|
||||
"posthog-js": "1.67.1",
|
||||
"qrcode": "^1.5.3",
|
||||
@@ -84,17 +83,17 @@
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "19.0.0",
|
||||
"react-dropzone": "^14.3.5",
|
||||
"react-error-boundary": "^4.1.2",
|
||||
"react-error-boundary": "^5.0.0",
|
||||
"react-hook-form": "7.54.2",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-table": "^7.8.0",
|
||||
"recharts": "2.14.1",
|
||||
"recharts": "2.15.1",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"server-only": "^0.0.1",
|
||||
"shiki": "1.24.2",
|
||||
"sonner": "^1.7.1",
|
||||
"shiki": "1.27.0",
|
||||
"sonner": "^1.7.3",
|
||||
"spdx-correct": "^3.2.0",
|
||||
"swagger-ui-react": "^5.18.2",
|
||||
"swagger-ui-react": "^5.18.3",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"thirdweb": "workspace:*",
|
||||
@@ -104,42 +103,42 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chakra-ui/cli": "^2.4.1",
|
||||
"@chromatic-com/storybook": "3.2.2",
|
||||
"@next/bundle-analyzer": "15.1.0",
|
||||
"@next/eslint-plugin-next": "15.1.0",
|
||||
"@playwright/test": "1.49.1",
|
||||
"@storybook/addon-essentials": "8.4.7",
|
||||
"@storybook/addon-interactions": "8.4.7",
|
||||
"@storybook/addon-links": "8.4.7",
|
||||
"@storybook/addon-onboarding": "8.4.7",
|
||||
"@storybook/addon-viewport": "8.4.7",
|
||||
"@storybook/blocks": "8.4.7",
|
||||
"@storybook/nextjs": "8.4.7",
|
||||
"@storybook/react": "8.4.7",
|
||||
"@storybook/test": "8.4.7",
|
||||
"@chromatic-com/storybook": "3.2.4",
|
||||
"@next/bundle-analyzer": "15.1.6",
|
||||
"@next/eslint-plugin-next": "15.1.6",
|
||||
"@playwright/test": "1.50.0",
|
||||
"@storybook/addon-essentials": "8.5.2",
|
||||
"@storybook/addon-interactions": "8.5.2",
|
||||
"@storybook/addon-links": "8.5.2",
|
||||
"@storybook/addon-onboarding": "8.5.2",
|
||||
"@storybook/addon-viewport": "8.5.2",
|
||||
"@storybook/blocks": "8.5.2",
|
||||
"@storybook/nextjs": "8.5.2",
|
||||
"@storybook/react": "8.5.2",
|
||||
"@storybook/test": "8.5.2",
|
||||
"@types/color": "4.2.0",
|
||||
"@types/node": "22.10.2",
|
||||
"@types/node": "22.10.10",
|
||||
"@types/papaparse": "^5.3.15",
|
||||
"@types/pluralize": "^0.0.33",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/react": "19.0.2",
|
||||
"@types/react-dom": "19.0.2",
|
||||
"@types/react": "19.0.8",
|
||||
"@types/react-dom": "19.0.3",
|
||||
"@types/react-table": "^7.7.20",
|
||||
"@types/spdx-correct": "^3.1.3",
|
||||
"@types/swagger-ui-react": "^4.18.3",
|
||||
"@types/swagger-ui-react": "^4.19.0",
|
||||
"@typescript-eslint/eslint-plugin": "7.14.1",
|
||||
"@typescript-eslint/parser": "7.14.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"checkly": "^4.15.0",
|
||||
"checkly": "^4.19.1",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-biome": "1.9.4",
|
||||
"eslint-plugin-react-compiler": "19.0.0-beta-df7b47d-20241124",
|
||||
"eslint-plugin-storybook": "^0.11.1",
|
||||
"knip": "5.41.0",
|
||||
"knip": "5.43.6",
|
||||
"next-sitemap": "^4.2.3",
|
||||
"postcss": "8.4.49",
|
||||
"storybook": "8.4.7",
|
||||
"tailwindcss": "3.4.16",
|
||||
"typescript": "5.7.2"
|
||||
"postcss": "8.5.1",
|
||||
"storybook": "8.5.2",
|
||||
"tailwindcss": "3.4.17",
|
||||
"typescript": "5.7.3"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
name,description,external_url,background_color,youtube_url,additional,properties,can be,added
|
||||
name,description,external_url,background_color,youtube_url,additional,properties,can be,added
|
||||
Token 0 Name,Token 0 Description,https://thirdweb.com,#0098EE,,every,row,is a ,property
|
||||
|
|
Before Width: | Height: | Size: 881 KiB After Width: | Height: | Size: 881 KiB |
@@ -21,7 +21,7 @@ const legacyDashboardToTeamRedirects = [
|
||||
},
|
||||
{
|
||||
source: "/dashboard/settings/api-keys",
|
||||
destination: "/team/~/~/projects",
|
||||
destination: "/team",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
@@ -160,12 +160,12 @@ async function redirects() {
|
||||
},
|
||||
{
|
||||
source: "/create-api-key",
|
||||
destination: "/team/~/~/projects",
|
||||
destination: "/team",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/dashboard/settings",
|
||||
destination: "/team/~/~/projects",
|
||||
destination: "/team",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
@@ -331,6 +331,12 @@ async function redirects() {
|
||||
destination: "/templates/:slug",
|
||||
permanent: false,
|
||||
},
|
||||
// PREVIOUS CAMPAIGNS
|
||||
{
|
||||
source: "/unlimited-wallets",
|
||||
destination: "/",
|
||||
permanent: false,
|
||||
},
|
||||
...legacyDashboardToTeamRedirects,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
"use server";
|
||||
|
||||
type EmailSignupParams = {
|
||||
email: string;
|
||||
send_welcome_email?: boolean;
|
||||
};
|
||||
|
||||
export async function emailSignup(payLoad: EmailSignupParams) {
|
||||
const response = await fetch(
|
||||
"https://api.beehiiv.com/v2/publications/pub_9f54090a-6d14-406b-adfd-dbb30574f664/subscriptions",
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${process.env.BEEHIIV_API_KEY}`,
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
email: payLoad.email,
|
||||
send_welcome_email: payLoad.send_welcome_email || false,
|
||||
utm_source: "thirdweb.com",
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
};
|
||||
}
|
||||
+27
-20
@@ -1,15 +1,11 @@
|
||||
"use server";
|
||||
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import { defineDashboardChain } from "lib/defineDashboardChain";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { ZERO_ADDRESS, isAddress, toTokens } from "thirdweb";
|
||||
import { getWalletBalance } from "thirdweb/wallets";
|
||||
|
||||
export type BalanceQueryRequest = {
|
||||
chainId: number;
|
||||
address: string;
|
||||
};
|
||||
|
||||
export type BalanceQueryResponse = Array<{
|
||||
type BalanceQueryResponse = Array<{
|
||||
balance: string;
|
||||
decimals: number;
|
||||
name?: string;
|
||||
@@ -18,21 +14,30 @@ export type BalanceQueryResponse = Array<{
|
||||
display_balance: string;
|
||||
}>;
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(400).json({ error: "invalid method" });
|
||||
}
|
||||
export async function getTokenBalancesFromMoralis(params: {
|
||||
contractAddress: string;
|
||||
chainId: number;
|
||||
}): Promise<
|
||||
| { data: BalanceQueryResponse; error: undefined }
|
||||
| {
|
||||
data: undefined;
|
||||
error: string;
|
||||
}
|
||||
> {
|
||||
const { contractAddress, chainId } = params;
|
||||
|
||||
const { chainId, address } = req.body;
|
||||
if (!isAddress(address)) {
|
||||
return res.status(400).json({ error: "invalid address" });
|
||||
if (!isAddress(contractAddress)) {
|
||||
return {
|
||||
data: undefined,
|
||||
error: "invalid address",
|
||||
};
|
||||
}
|
||||
|
||||
const getNativeBalance = async (): Promise<BalanceQueryResponse> => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const chain = defineDashboardChain(chainId, undefined);
|
||||
const balance = await getWalletBalance({
|
||||
address,
|
||||
address: contractAddress,
|
||||
chain,
|
||||
client: getThirdwebClient(),
|
||||
});
|
||||
@@ -50,7 +55,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
|
||||
const getTokenBalances = async (): Promise<BalanceQueryResponse> => {
|
||||
const _chain = encodeURIComponent(`0x${chainId?.toString(16)}`);
|
||||
const _address = encodeURIComponent(address);
|
||||
const _address = encodeURIComponent(contractAddress);
|
||||
const tokenBalanceEndpoint = `https://deep-index.moralis.io/api/v2/${_address}/erc20?chain=${_chain}`;
|
||||
|
||||
const resp = await fetch(tokenBalanceEndpoint, {
|
||||
@@ -59,6 +64,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
"x-api-key": process.env.MORALIS_API_KEY || "",
|
||||
},
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
resp.body?.cancel();
|
||||
return [];
|
||||
@@ -76,7 +82,8 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
getTokenBalances(),
|
||||
]);
|
||||
|
||||
return res.status(200).json([...nativeBalance, ...tokenBalances]);
|
||||
};
|
||||
|
||||
export default handler;
|
||||
return {
|
||||
error: undefined,
|
||||
data: [...nativeBalance, ...tokenBalances],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
"use server";
|
||||
|
||||
import {
|
||||
generateAlchemyUrl,
|
||||
isAlchemySupported,
|
||||
transformAlchemyResponseToNFT,
|
||||
} from "lib/wallet/nfts/alchemy";
|
||||
import {
|
||||
generateMoralisUrl,
|
||||
isMoralisSupported,
|
||||
transformMoralisResponseToNFT,
|
||||
} from "lib/wallet/nfts/moralis";
|
||||
import {
|
||||
generateSimpleHashUrl,
|
||||
isSimpleHashSupported,
|
||||
transformSimpleHashResponseToNFT,
|
||||
} from "lib/wallet/nfts/simpleHash";
|
||||
import type { WalletNFT } from "lib/wallet/nfts/types";
|
||||
|
||||
type WalletNFTApiReturn =
|
||||
| { result: WalletNFT[]; error?: undefined }
|
||||
| { result?: undefined; error: string };
|
||||
|
||||
export async function getWalletNFTs(params: {
|
||||
chainId: number;
|
||||
owner: string;
|
||||
}): Promise<WalletNFTApiReturn> {
|
||||
const { chainId, owner } = params;
|
||||
const supportedChainSlug = await isSimpleHashSupported(chainId);
|
||||
|
||||
if (supportedChainSlug && process.env.SIMPLEHASH_API_KEY) {
|
||||
const url = generateSimpleHashUrl({ chainSlug: supportedChainSlug, owner });
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-API-KEY": process.env.SIMPLEHASH_API_KEY,
|
||||
},
|
||||
next: {
|
||||
revalidate: 10, // cache for 10 seconds
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status >= 400) {
|
||||
return {
|
||||
error: response.statusText,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedResponse = await response.json();
|
||||
const result = await transformSimpleHashResponseToNFT(
|
||||
parsedResponse,
|
||||
owner,
|
||||
);
|
||||
|
||||
return { result };
|
||||
} catch {
|
||||
return { error: "error parsing response" };
|
||||
}
|
||||
}
|
||||
|
||||
if (isAlchemySupported(chainId)) {
|
||||
const url = generateAlchemyUrl({ chainId, owner });
|
||||
|
||||
const response = await fetch(url, {
|
||||
next: {
|
||||
revalidate: 10, // cache for 10 seconds
|
||||
},
|
||||
});
|
||||
if (response.status >= 400) {
|
||||
return { error: response.statusText };
|
||||
}
|
||||
try {
|
||||
const parsedResponse = await response.json();
|
||||
const result = await transformAlchemyResponseToNFT(parsedResponse, owner);
|
||||
|
||||
return { result, error: undefined };
|
||||
} catch (err) {
|
||||
console.error("Error fetching NFTs", err);
|
||||
return { error: "error parsing response" };
|
||||
}
|
||||
}
|
||||
|
||||
if (isMoralisSupported(chainId) && process.env.MORALIS_API_KEY) {
|
||||
const url = generateMoralisUrl({ chainId, owner });
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-API-Key": process.env.MORALIS_API_KEY,
|
||||
},
|
||||
next: {
|
||||
revalidate: 10, // cache for 10 seconds
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status >= 400) {
|
||||
return { error: response.statusText };
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedResponse = await response.json();
|
||||
const result = await transformMoralisResponseToNFT(
|
||||
await parsedResponse,
|
||||
owner,
|
||||
);
|
||||
|
||||
return { result };
|
||||
} catch (err) {
|
||||
console.error("Error fetching NFTs", err);
|
||||
return { error: "error parsing response" };
|
||||
}
|
||||
}
|
||||
|
||||
return { error: "unsupported chain" };
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
"use server";
|
||||
|
||||
import { getAuthToken } from "../../app/api/lib/getAuthToken";
|
||||
import { API_SERVER_URL } from "../constants/env";
|
||||
|
||||
type ProxyActionParams = {
|
||||
pathname: string;
|
||||
searchParams?: Record<string, string>;
|
||||
method: "GET" | "POST" | "PUT" | "DELETE";
|
||||
body?: string;
|
||||
headers?: Record<string, string>;
|
||||
};
|
||||
|
||||
type ProxyActionResult<T extends object> =
|
||||
| {
|
||||
status: number;
|
||||
ok: true;
|
||||
data: T;
|
||||
}
|
||||
| {
|
||||
status: number;
|
||||
ok: false;
|
||||
error: string;
|
||||
};
|
||||
|
||||
async function proxy<T extends object>(
|
||||
baseUrl: string,
|
||||
params: ProxyActionParams,
|
||||
): Promise<ProxyActionResult<T>> {
|
||||
const authToken = await getAuthToken();
|
||||
|
||||
// build URL
|
||||
const url = new URL(baseUrl);
|
||||
url.pathname = params.pathname;
|
||||
if (params.searchParams) {
|
||||
for (const key in params.searchParams) {
|
||||
url.searchParams.append(key, params.searchParams[key] as string);
|
||||
}
|
||||
}
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: params.method,
|
||||
headers: {
|
||||
...params.headers,
|
||||
...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
|
||||
},
|
||||
body: params.body,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
try {
|
||||
const errorMessage = await res.text();
|
||||
return {
|
||||
status: res.status,
|
||||
ok: false,
|
||||
error: errorMessage || res.statusText,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
status: res.status,
|
||||
ok: false,
|
||||
error: res.statusText,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: res.status,
|
||||
ok: true,
|
||||
data: await res.json(),
|
||||
};
|
||||
}
|
||||
|
||||
export async function analyticsServerProxy<T extends object = object>(
|
||||
params: ProxyActionParams,
|
||||
) {
|
||||
return proxy<T>(
|
||||
process.env.ANALYTICS_SERVICE_URL || "https://analytics.thirdweb.com",
|
||||
params,
|
||||
);
|
||||
}
|
||||
|
||||
export async function apiServerProxy<T extends object = object>(
|
||||
params: ProxyActionParams,
|
||||
) {
|
||||
return proxy<T>(API_SERVER_URL, params);
|
||||
}
|
||||
|
||||
export async function payServerProxy<T extends object = object>(
|
||||
params: ProxyActionParams,
|
||||
) {
|
||||
return proxy<T>(
|
||||
process.env.NEXT_PUBLIC_PAY_URL
|
||||
? `https://${process.env.NEXT_PUBLIC_PAY_URL}`
|
||||
: "https://pay.thirdweb-dev.com",
|
||||
params,
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
AnalyticsQueryParams,
|
||||
InAppWalletStats,
|
||||
RpcMethodStats,
|
||||
TransactionStats,
|
||||
UserOpStats,
|
||||
WalletStats,
|
||||
WalletUserStats,
|
||||
@@ -77,7 +78,10 @@ export async function getUserOpUsage(
|
||||
});
|
||||
|
||||
if (res?.status !== 200) {
|
||||
console.error("Failed to fetch user ops usage");
|
||||
const reason = await res?.text();
|
||||
console.error(
|
||||
`Failed to fetch user ops usage: ${res?.status} - ${res.statusText} - ${reason}`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -85,6 +89,30 @@ export async function getUserOpUsage(
|
||||
return json.data as UserOpStats[];
|
||||
}
|
||||
|
||||
export async function getClientTransactions(
|
||||
params: AnalyticsQueryParams,
|
||||
): Promise<TransactionStats[]> {
|
||||
const searchParams = buildSearchParams(params);
|
||||
const res = await fetchAnalytics(
|
||||
`v1/transactions/client?${searchParams.toString()}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
},
|
||||
);
|
||||
|
||||
if (res?.status !== 200) {
|
||||
const reason = await res?.text();
|
||||
console.error(
|
||||
`Failed to fetch client transactions stats: ${res?.status} - ${res.statusText} - ${reason}`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return json.data as TransactionStats[];
|
||||
}
|
||||
|
||||
export async function getRpcMethodUsage(
|
||||
params: AnalyticsQueryParams,
|
||||
): Promise<RpcMethodStats[]> {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import "server-only";
|
||||
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
|
||||
import { API_SERVER_URL } from "@/constants/env";
|
||||
import { cookies } from "next/headers";
|
||||
import { getAuthToken } from "../../app/api/lib/getAuthToken";
|
||||
|
||||
export type Project = {
|
||||
id: string;
|
||||
@@ -21,11 +20,7 @@ export type Project = {
|
||||
};
|
||||
|
||||
export async function getProjects(teamSlug: string) {
|
||||
const cookiesManager = await cookies();
|
||||
const activeAccount = cookiesManager.get(COOKIE_ACTIVE_ACCOUNT)?.value;
|
||||
const token = activeAccount
|
||||
? cookiesManager.get(COOKIE_PREFIX_TOKEN + activeAccount)?.value
|
||||
: null;
|
||||
const token = await getAuthToken();
|
||||
|
||||
if (!token) {
|
||||
return [];
|
||||
@@ -46,11 +41,7 @@ export async function getProjects(teamSlug: string) {
|
||||
}
|
||||
|
||||
export async function getProject(teamSlug: string, projectSlug: string) {
|
||||
const cookiesManager = await cookies();
|
||||
const activeAccount = cookiesManager.get(COOKIE_ACTIVE_ACCOUNT)?.value;
|
||||
const token = activeAccount
|
||||
? cookiesManager.get(COOKIE_PREFIX_TOKEN + activeAccount)?.value
|
||||
: null;
|
||||
const token = await getAuthToken();
|
||||
|
||||
if (!token) {
|
||||
return null;
|
||||
|
||||
@@ -25,7 +25,7 @@ export type Team = {
|
||||
billingPlan: "pro" | "growth" | "free" | "starter";
|
||||
billingStatus: "validPayment" | (string & {}) | null;
|
||||
billingEmail: string | null;
|
||||
growthTrialEligible: boolean | null;
|
||||
growthTrialEligible: false;
|
||||
enabledScopes: EnabledTeamScope[];
|
||||
};
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ export function ProjectAvatar(props: {
|
||||
className={cn("rounded-lg border border-border", props.className)}
|
||||
alt={""}
|
||||
fallback={
|
||||
<div className="flex items-center justify-center bg-muted/50">
|
||||
<div className="flex items-center justify-center bg-card">
|
||||
<BoxIcon className="size-[50%] text-muted-foreground" />
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export function DangerSettingCard(props: {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"overflow-hidden rounded-lg border border-red-500/70",
|
||||
"overflow-hidden rounded-lg border border-red-500/70 bg-card",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -25,7 +25,7 @@ export function Img(props: imgElementProps) {
|
||||
: _status;
|
||||
const { className, fallback, skeleton, ...restProps } = props;
|
||||
const defaultSkeleton = <div className="animate-pulse bg-accent" />;
|
||||
const defaultFallback = <div className="bg-muted" />;
|
||||
const defaultFallback = <div className="bg-accent" />;
|
||||
const imgRef = useRef<HTMLImageElement>(null);
|
||||
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
|
||||
@@ -47,7 +47,7 @@ export function MobileSidebar(props: {
|
||||
const defaultTrigger = (
|
||||
<Button
|
||||
className={cn(
|
||||
"w-full justify-between gap-2 bg-muted/50 text-left lg:hidden",
|
||||
"w-full justify-between gap-2 bg-card text-left lg:hidden",
|
||||
props.triggerClassName,
|
||||
)}
|
||||
variant="outline"
|
||||
|
||||
@@ -16,6 +16,8 @@ type Option = { label: string; value: string };
|
||||
export function MultiNetworkSelector(props: {
|
||||
selectedChainIds: number[];
|
||||
onChange: (chainIds: number[]) => void;
|
||||
disableChainId?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
const { allChains, idToChain } = useAllChainsData();
|
||||
|
||||
@@ -53,16 +55,24 @@ export function MultiNetworkSelector(props: {
|
||||
return (
|
||||
<div className="flex justify-between gap-4">
|
||||
<span className="flex grow gap-2 truncate text-left">
|
||||
<ChainIcon
|
||||
className="size-5"
|
||||
ipfsSrc={chain.icon?.url}
|
||||
loading="lazy"
|
||||
/>
|
||||
{cleanChainName(chain.name)}
|
||||
</span>
|
||||
<Badge variant="outline" className="gap-2">
|
||||
<span className="text-muted-foreground">Chain ID</span>
|
||||
{chain.chainId}
|
||||
</Badge>
|
||||
|
||||
{!props.disableChainId && (
|
||||
<Badge variant="outline" className="gap-2">
|
||||
<span className="text-muted-foreground">Chain ID</span>
|
||||
{chain.chainId}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[idToChain],
|
||||
[idToChain, props.disableChainId],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -79,6 +89,7 @@ export function MultiNetworkSelector(props: {
|
||||
disabled={allChains.length === 0}
|
||||
overrideSearchFn={searchFn}
|
||||
renderOption={renderOption}
|
||||
className={props.className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -90,6 +101,9 @@ export function SingleNetworkSelector(props: {
|
||||
popoverContentClassName?: string;
|
||||
// if specified - only these chains will be shown
|
||||
chainIds?: number[];
|
||||
side?: "left" | "right" | "top" | "bottom";
|
||||
disableChainId?: boolean;
|
||||
align?: "center" | "start" | "end";
|
||||
}) {
|
||||
const { allChains, idToChain } = useAllChainsData();
|
||||
|
||||
@@ -140,16 +154,19 @@ export function SingleNetworkSelector(props: {
|
||||
ipfsSrc={chain.icon?.url}
|
||||
loading="lazy"
|
||||
/>
|
||||
{chain.name}
|
||||
{cleanChainName(chain.name)}
|
||||
</span>
|
||||
<Badge variant="outline" className="gap-2 max-sm:hidden">
|
||||
<span className="text-muted-foreground">Chain ID</span>
|
||||
{chain.chainId}
|
||||
</Badge>
|
||||
|
||||
{!props.disableChainId && (
|
||||
<Badge variant="outline" className="gap-2 max-sm:hidden">
|
||||
<span className="text-muted-foreground">Chain ID</span>
|
||||
{chain.chainId}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[idToChain],
|
||||
[idToChain, props.disableChainId],
|
||||
);
|
||||
|
||||
const isLoadingChains = allChains.length === 0;
|
||||
@@ -162,12 +179,15 @@ export function SingleNetworkSelector(props: {
|
||||
onValueChange={(chainId) => {
|
||||
props.onChange(Number(chainId));
|
||||
}}
|
||||
closeOnSelect={true}
|
||||
placeholder={isLoadingChains ? "Loading Chains..." : "Select Chain"}
|
||||
overrideSearchFn={searchFn}
|
||||
renderOption={renderOption}
|
||||
className={props.className}
|
||||
popoverContentClassName={props.popoverContentClassName}
|
||||
disabled={isLoadingChains}
|
||||
side={props.side}
|
||||
align={props.align}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export const Mobile: Story = {
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background py-6 text-foreground">
|
||||
<div className="min-h-dvh bg-background py-6 text-foreground">
|
||||
<div className="container flex max-w-[1100px] flex-col gap-10 lg:p-10">
|
||||
<BadgeContainer label="No Header">
|
||||
<SettingsCard
|
||||
|
||||
@@ -30,7 +30,7 @@ export function SettingsCard(
|
||||
}>,
|
||||
) {
|
||||
return (
|
||||
<div className="relative rounded-lg border border-border bg-muted/50">
|
||||
<div className="relative rounded-lg border border-border bg-card">
|
||||
<div
|
||||
className={cn(
|
||||
"relative border-border border-b px-4 py-6 lg:px-6",
|
||||
|
||||
@@ -67,7 +67,7 @@ export function RenderSidebarLinks(props: { links: SidebarLink[] }) {
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: items won't be reordered
|
||||
key={i}
|
||||
href={link.href}
|
||||
className="flex items-center gap-2 rounded-md px-3 py-2 text-muted-foreground text-sm hover:bg-muted"
|
||||
className="flex items-center gap-2 rounded-md px-3 py-2 text-muted-foreground text-sm hover:bg-accent"
|
||||
activeClassName="text-foreground"
|
||||
exactMatch={link.exactMatch}
|
||||
>
|
||||
|
||||
@@ -19,7 +19,7 @@ export function AppFooter(props: AppFooterProps) {
|
||||
return (
|
||||
<footer
|
||||
className={cn(
|
||||
"w-full border-border border-t bg-muted/50 py-6 md:py-8",
|
||||
"w-full border-border border-t bg-card py-6 md:py-8",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
@@ -28,7 +28,9 @@ export function AppFooter(props: AppFooterProps) {
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div className="flex flex-row items-center gap-3">
|
||||
<ThirdwebMiniLogo className="h-7" />
|
||||
<p className="text-muted-foreground text-sm">© 2024 thirdweb</p>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
© {new Date().getFullYear()} thirdweb
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-row gap-3">
|
||||
<Button size="icon" variant="ghost" asChild className="size-9">
|
||||
|
||||
@@ -8,8 +8,13 @@ import {
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/components/ui/chart";
|
||||
import { formatDate } from "date-fns";
|
||||
import { useMemo } from "react";
|
||||
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
|
||||
import {
|
||||
EmptyChartState,
|
||||
LoadingChartState,
|
||||
} from "../../../../components/analytics/empty-chart-state";
|
||||
|
||||
type ThirdwebAreaChartProps<TConfig extends ChartConfig> = {
|
||||
// chart config
|
||||
@@ -19,6 +24,7 @@ type ThirdwebAreaChartProps<TConfig extends ChartConfig> = {
|
||||
|
||||
// chart className
|
||||
chartClassName?: string;
|
||||
isPending: boolean;
|
||||
};
|
||||
|
||||
export function ThirdwebAreaChart<TConfig extends ChartConfig>(
|
||||
@@ -26,64 +32,70 @@ export function ThirdwebAreaChart<TConfig extends ChartConfig>(
|
||||
) {
|
||||
const configKeys = useMemo(() => Object.keys(props.config), [props.config]);
|
||||
return (
|
||||
<div className="rounded-lg border border-border bg-muted/50 px-4 pt-10 pb-4">
|
||||
<div className="rounded-lg border border-border px-4 pt-10 pb-4">
|
||||
<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={20}
|
||||
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.isPending ? (
|
||||
<LoadingChartState />
|
||||
) : props.data.length === 0 ? (
|
||||
<EmptyChartState />
|
||||
) : (
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
data={props.data}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={20}
|
||||
tickFormatter={(value) => formatDate(new Date(value), "MMM dd")}
|
||||
/>
|
||||
))}
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
|
||||
<defs>
|
||||
{configKeys.map((key) => (
|
||||
<linearGradient
|
||||
key={key}
|
||||
id={`fill_${key}`}
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop
|
||||
offset="5%"
|
||||
stopColor={`var(--color-${key})`}
|
||||
stopOpacity={0.8}
|
||||
/>
|
||||
<stop
|
||||
offset="95%"
|
||||
stopColor={`var(--color-${key})`}
|
||||
stopOpacity={0.1}
|
||||
/>
|
||||
</linearGradient>
|
||||
))}
|
||||
</defs>
|
||||
{configKeys.map((key) => (
|
||||
<Area
|
||||
key={key}
|
||||
dataKey={key}
|
||||
type="natural"
|
||||
fill={`url(#fill_${key})`}
|
||||
fillOpacity={0.4}
|
||||
stroke={`var(--color-${key})`}
|
||||
stackId="a"
|
||||
/>
|
||||
))}
|
||||
|
||||
{props.showLegend && (
|
||||
<ChartLegend content={<ChartLegendContent />} className="pt-8" />
|
||||
)}
|
||||
</AreaChart>
|
||||
{props.showLegend && (
|
||||
<ChartLegend content={<ChartLegendContent />} className="pt-8" />
|
||||
)}
|
||||
</AreaChart>
|
||||
)}
|
||||
</ChartContainer>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -17,7 +17,13 @@ import {
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/components/ui/chart";
|
||||
import { formatDate } from "date-fns";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
EmptyChartState,
|
||||
LoadingChartState,
|
||||
} from "../../../../components/analytics/empty-chart-state";
|
||||
import { cn } from "../../../lib/utils";
|
||||
|
||||
type ThirdwebBarChartProps<TConfig extends ChartConfig> = {
|
||||
// metadata
|
||||
@@ -30,6 +36,10 @@ type ThirdwebBarChartProps<TConfig extends ChartConfig> = {
|
||||
variant?: "stacked" | "grouped";
|
||||
// chart className
|
||||
chartClassName?: string;
|
||||
isPending: boolean;
|
||||
toolTipLabelFormatter?: (label: string, payload: unknown) => React.ReactNode;
|
||||
hideLabel?: boolean;
|
||||
titleClassName?: string;
|
||||
};
|
||||
|
||||
export function ThirdwebBarChart<TConfig extends ChartConfig>(
|
||||
@@ -39,51 +49,71 @@ export function ThirdwebBarChart<TConfig extends ChartConfig>(
|
||||
// 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>
|
||||
<CardTitle className={cn("mb-2", props.titleClassName)}>
|
||||
{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]
|
||||
{props.isPending ? (
|
||||
<LoadingChartState />
|
||||
) : props.data.length === 0 ? (
|
||||
<EmptyChartState />
|
||||
) : (
|
||||
<BarChart accessibilityLayer data={props.data}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={10}
|
||||
tickFormatter={(value) => formatDate(new Date(value), "MMM d")}
|
||||
/>
|
||||
<ChartTooltip
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
hideLabel={
|
||||
props.hideLabel !== undefined ? props.hideLabel : true
|
||||
}
|
||||
labelFormatter={props.toolTipLabelFormatter}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</BarChart>
|
||||
{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]
|
||||
}
|
||||
strokeWidth={1}
|
||||
className="stroke-background"
|
||||
/>
|
||||
))}
|
||||
</BarChart>
|
||||
)}
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ export function UnexpectedValueErrorMessage(props: {
|
||||
{stringifiedValue && (
|
||||
<div className="mt-3">
|
||||
<p className="mb-0.5 text-muted-foreground text-sm">Value Received</p>
|
||||
<ScrollShadow className="rounded-lg bg-muted/50">
|
||||
<ScrollShadow className="rounded-lg bg-card">
|
||||
<code className="block whitespace-pre p-4 font-mono text-xs">
|
||||
{stringifiedValue}
|
||||
</code>
|
||||
|
||||
@@ -202,7 +202,7 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
|
||||
event.stopPropagation();
|
||||
handleClear();
|
||||
}}
|
||||
className="rounded p-1 hover:bg-muted"
|
||||
className="rounded p-1 hover:bg-accent"
|
||||
>
|
||||
<XIcon className="h-4 cursor-pointer text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
@@ -57,8 +57,9 @@ export const PricingCard: React.FC<PricingCardProps> = ({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"z-[999] flex w-full flex-col gap-6 rounded-xl border border-border bg-muted/50 p-4 md:p-6",
|
||||
"z-10 flex w-full flex-col gap-6 rounded-xl border border-border bg-card p-4 md:p-6",
|
||||
current && "border-blue-500",
|
||||
highlighted && "border-active-border",
|
||||
)}
|
||||
style={
|
||||
highlighted
|
||||
|
||||
@@ -31,6 +31,9 @@ interface SelectWithSearchProps
|
||||
) => boolean;
|
||||
renderOption?: (option: { value: string; label: string }) => React.ReactNode;
|
||||
popoverContentClassName?: string;
|
||||
side?: "left" | "right" | "top" | "bottom";
|
||||
align?: "center" | "start" | "end";
|
||||
closeOnSelect?: boolean;
|
||||
}
|
||||
|
||||
export const SelectWithSearch = React.forwardRef<
|
||||
@@ -48,6 +51,7 @@ export const SelectWithSearch = React.forwardRef<
|
||||
overrideSearchFn,
|
||||
popoverContentClassName,
|
||||
searchPlaceholder,
|
||||
closeOnSelect,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
@@ -107,7 +111,7 @@ export const SelectWithSearch = React.forwardRef<
|
||||
}, [searchValue]);
|
||||
|
||||
return (
|
||||
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
|
||||
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen} modal>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
ref={ref}
|
||||
@@ -118,23 +122,24 @@ export const SelectWithSearch = React.forwardRef<
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
"text-muted-foreground text-sm",
|
||||
"truncate text-muted-foreground text-sm",
|
||||
selectedOption && "text-foreground",
|
||||
)}
|
||||
>
|
||||
{selectedOption?.label || placeholder}
|
||||
</span>
|
||||
<ChevronDown className="h-4 cursor-pointer text-muted-foreground" />
|
||||
<ChevronDown className="size-4 cursor-pointer text-muted-foreground" />
|
||||
</div>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent
|
||||
className={cn("z-[10001] p-0", popoverContentClassName)}
|
||||
align="center"
|
||||
align={props.align || "center"}
|
||||
side={props.side}
|
||||
sideOffset={10}
|
||||
onEscapeKeyDown={() => setIsPopoverOpen(false)}
|
||||
style={{
|
||||
@@ -174,7 +179,12 @@ export const SelectWithSearch = React.forwardRef<
|
||||
key={option.value}
|
||||
role="option"
|
||||
aria-selected={isSelected}
|
||||
onClick={() => onValueChange(option.value)}
|
||||
onClick={() => {
|
||||
onValueChange(option.value);
|
||||
if (closeOnSelect) {
|
||||
setIsPopoverOpen(false);
|
||||
}
|
||||
}}
|
||||
variant="ghost"
|
||||
className="flex w-full cursor-pointer justify-start gap-3 rounded-sm px-3 py-2 text-left"
|
||||
ref={
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import { useThirdwebClient } from "@/constants/thirdweb.client";
|
||||
import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler";
|
||||
import { useClipboard } from "hooks/useClipboard";
|
||||
import { Check, Copy, ExternalLinkIcon } from "lucide-react";
|
||||
import { Check, Copy } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { type ThirdwebClient, isAddress } from "thirdweb";
|
||||
import { ZERO_ADDRESS } from "thirdweb";
|
||||
@@ -121,7 +121,11 @@ export function WalletAddress(props: {
|
||||
>
|
||||
{walletAvatarLink && (
|
||||
<Avatar>
|
||||
<AvatarImage src={walletAvatarLink} alt={profile.name} />
|
||||
<AvatarImage
|
||||
src={walletAvatarLink}
|
||||
alt={profile.name}
|
||||
className="object-cover"
|
||||
/>
|
||||
{profile.name && (
|
||||
<AvatarFallback>
|
||||
{profile.name.slice(0, 2)}
|
||||
@@ -144,21 +148,6 @@ export function WalletAddress(props: {
|
||||
);
|
||||
})
|
||||
)}
|
||||
<Button
|
||||
asChild
|
||||
variant="upsell"
|
||||
className="flex flex-row items-center gap-2 text-sm"
|
||||
size="sm"
|
||||
>
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://blog.thirdweb.com/changelog/introducing-the-social-sdk?ref=dashboard-social-wallet"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Learn more
|
||||
<ExternalLinkIcon className="size-4" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useState } from "react";
|
||||
import { BadgeContainer, mobileViewport } from "../../stories/utils";
|
||||
import { PaginationButtons } from "./pagination-buttons";
|
||||
|
||||
const meta = {
|
||||
title: "blocks/PaginationButtons",
|
||||
component: Story,
|
||||
parameters: {
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof Story>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="container flex max-w-[1000px] flex-col gap-8 py-10">
|
||||
<Variant label="10 Pages" totalPages={10} />
|
||||
<Variant label="100 Pages" totalPages={100} />
|
||||
<Variant label="3 Pages" totalPages={2} />
|
||||
<Variant label="6 Pages" totalPages={6} />
|
||||
<Variant label="1 Page - nothing rendered" totalPages={1} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Variant(props: {
|
||||
label: string;
|
||||
totalPages: number;
|
||||
}) {
|
||||
const [activePage, setActivePage] = useState(1);
|
||||
return (
|
||||
<BadgeContainer label={props.label}>
|
||||
<div className="border bg-card py-5">
|
||||
<PaginationButtons
|
||||
activePage={activePage}
|
||||
onPageClick={setActivePage}
|
||||
totalPages={props.totalPages}
|
||||
/>
|
||||
</div>
|
||||
</BadgeContainer>
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,11 @@ import {
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
} from "@/components/ui/pagination";
|
||||
import { ArrowUpRightIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { cn } from "../lib/utils";
|
||||
import { Button } from "./ui/button";
|
||||
import { Input } from "./ui/input";
|
||||
|
||||
export const PaginationButtons = (props: {
|
||||
activePage: number;
|
||||
@@ -16,6 +21,47 @@ export const PaginationButtons = (props: {
|
||||
onPageClick: (page: number) => void;
|
||||
}) => {
|
||||
const { activePage, totalPages, onPageClick: setPage } = props;
|
||||
const [inputHasError, setInputHasError] = useState(false);
|
||||
const [pageNumberInput, setPageNumberInput] = useState("");
|
||||
|
||||
if (totalPages === 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function handlePageInputSubmit() {
|
||||
const page = Number(pageNumberInput);
|
||||
|
||||
setInputHasError(false);
|
||||
if (Number.isInteger(page) && page > 0 && page <= totalPages) {
|
||||
setPage(page);
|
||||
setPageNumberInput("");
|
||||
} else {
|
||||
setInputHasError(true);
|
||||
}
|
||||
}
|
||||
|
||||
// just render all the page buttons directly
|
||||
if (totalPages <= 6) {
|
||||
const pages = [...Array(totalPages)].map((_, i) => i + 1);
|
||||
return (
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
{pages.map((page) => (
|
||||
<PaginationItem key={page}>
|
||||
<PaginationLink
|
||||
isActive={activePage === page}
|
||||
onClick={() => {
|
||||
setPage(page);
|
||||
}}
|
||||
>
|
||||
{page}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
))}
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Pagination>
|
||||
@@ -28,24 +74,28 @@ export const PaginationButtons = (props: {
|
||||
}}
|
||||
/>
|
||||
</PaginationItem>
|
||||
|
||||
{/* First page + ... */}
|
||||
{activePage - 3 > 0 && (
|
||||
<PaginationItem className="max-sm:hidden">
|
||||
<PaginationEllipsis />
|
||||
</PaginationItem>
|
||||
)}
|
||||
{activePage - 2 > 0 && (
|
||||
<PaginationItem className="max-sm:hidden">
|
||||
<PaginationLink
|
||||
onClick={() => {
|
||||
setPage(activePage - 2);
|
||||
}}
|
||||
>
|
||||
{activePage - 2}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
<>
|
||||
<PaginationItem>
|
||||
<PaginationLink
|
||||
onClick={() => {
|
||||
setPage(1);
|
||||
}}
|
||||
>
|
||||
1
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
|
||||
<PaginationItem>
|
||||
<PaginationEllipsis className="max-sm:w-3" />
|
||||
</PaginationItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
{activePage - 1 > 0 && (
|
||||
<PaginationItem>
|
||||
<PaginationItem className="max-sm:hidden">
|
||||
<PaginationLink
|
||||
onClick={() => {
|
||||
setPage(activePage - 1);
|
||||
@@ -55,11 +105,13 @@ export const PaginationButtons = (props: {
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
)}
|
||||
|
||||
<PaginationItem>
|
||||
<PaginationLink isActive>{activePage}</PaginationLink>
|
||||
</PaginationItem>
|
||||
|
||||
{activePage + 1 <= totalPages && (
|
||||
<PaginationItem>
|
||||
<PaginationItem className="max-sm:hidden">
|
||||
<PaginationLink
|
||||
onClick={() => {
|
||||
setPage(activePage + 1);
|
||||
@@ -69,22 +121,26 @@ export const PaginationButtons = (props: {
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
)}
|
||||
{activePage + 2 <= totalPages && (
|
||||
<PaginationItem className="max-sm:hidden">
|
||||
<PaginationLink
|
||||
onClick={() => {
|
||||
setPage(activePage + 2);
|
||||
}}
|
||||
>
|
||||
{activePage + 2}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
)}
|
||||
|
||||
{/* ... + Last page */}
|
||||
{activePage + 3 <= totalPages && (
|
||||
<PaginationItem className="max-sm:hidden">
|
||||
<PaginationEllipsis />
|
||||
</PaginationItem>
|
||||
<>
|
||||
<PaginationItem>
|
||||
<PaginationEllipsis className="max-sm:w-3" />
|
||||
</PaginationItem>
|
||||
|
||||
<PaginationItem>
|
||||
<PaginationLink
|
||||
onClick={() => {
|
||||
setPage(totalPages);
|
||||
}}
|
||||
>
|
||||
{totalPages}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
disabled={activePage === totalPages}
|
||||
@@ -93,6 +149,34 @@ export const PaginationButtons = (props: {
|
||||
}}
|
||||
/>
|
||||
</PaginationItem>
|
||||
|
||||
<div className="relative flex items-center">
|
||||
<Input
|
||||
value={pageNumberInput}
|
||||
onChange={(e) => {
|
||||
setInputHasError(false);
|
||||
setPageNumberInput(e.target.value);
|
||||
}}
|
||||
type="number"
|
||||
placeholder="Page"
|
||||
className={cn(
|
||||
"w-[60px] bg-transparent [appearance:textfield] max-sm:placeholder:text-sm lg:w-[100px] lg:pr-8 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none",
|
||||
inputHasError && "text-red-500",
|
||||
)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
handlePageInputSubmit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="absolute right-1 h-auto w-auto p-2 max-sm:hidden"
|
||||
onClick={handlePageInputSubmit}
|
||||
>
|
||||
<ArrowUpRightIcon className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
);
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ToolTipLabel } from "./tooltip";
|
||||
export function CopyTextButton(props: {
|
||||
textToShow: string;
|
||||
textToCopy: string;
|
||||
tooltip: string;
|
||||
tooltip: string | undefined;
|
||||
className?: string;
|
||||
iconClassName?: string;
|
||||
variant?:
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const alertVariants = cva(
|
||||
"dark:bg-muted/50 relative w-full rounded-lg border border-border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
||||
"bg-card relative w-full rounded-lg border border-border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
||||
@@ -20,7 +20,7 @@ export const AllVariants: Story = {
|
||||
|
||||
function Component() {
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col gap-6 bg-background p-6 text-foreground">
|
||||
<div className="flex min-h-dvh flex-col gap-6 bg-background p-6 text-foreground">
|
||||
<Variants size="default" />
|
||||
<Variants size="sm" />
|
||||
<Variants size="icon" />
|
||||
|
||||
@@ -113,7 +113,10 @@ const ChartTooltipContent = React.forwardRef<
|
||||
indicator?: "line" | "dot" | "dashed";
|
||||
nameKey?: string;
|
||||
labelKey?: string;
|
||||
valueFormatter?: (v: unknown) => string | undefined;
|
||||
valueFormatter?: (
|
||||
v: unknown,
|
||||
payLoad: unknown,
|
||||
) => React.ReactNode | undefined;
|
||||
}
|
||||
>(
|
||||
(
|
||||
@@ -183,7 +186,7 @@ const ChartTooltipContent = React.forwardRef<
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
|
||||
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-popover px-2.5 py-1.5 text-xs shadow-xl",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
@@ -233,7 +236,7 @@ const ChartTooltipContent = React.forwardRef<
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-1 justify-between gap-2 leading-none",
|
||||
"flex flex-1 justify-between gap-5 leading-none",
|
||||
nestLabel ? "items-end" : "items-center",
|
||||
)}
|
||||
>
|
||||
@@ -243,10 +246,10 @@ const ChartTooltipContent = React.forwardRef<
|
||||
{itemConfig?.label || item.name}
|
||||
</span>
|
||||
</div>
|
||||
{item.value && (
|
||||
{item.value !== undefined && (
|
||||
<span className="font-medium font-mono text-foreground tabular-nums">
|
||||
{valueFormatter
|
||||
? valueFormatter(item.value) || item.value
|
||||
? valueFormatter(item.value, item) || item.value
|
||||
: item.value}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -21,7 +21,7 @@ export function CodeBlockContainer(props: {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"group relative max-w-full overflow-hidden rounded-lg border border-border bg-background",
|
||||
"group relative max-w-full overflow-hidden rounded-lg border border-border bg-card",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -33,7 +33,7 @@ const ImageUpload = React.forwardRef<HTMLInputElement, ImageUploadProps>(
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className={cn(
|
||||
"group relative flex cursor-pointer flex-col items-center justify-center gap-1.5 overflow-hidden rounded-md border bg-card p-4 py-6 text-muted-foreground transition-all hover:border-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"group relative flex cursor-pointer flex-col items-center justify-center gap-1.5 overflow-hidden rounded-md border bg-background p-4 py-6 text-muted-foreground transition-all hover:border-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
isDragActive ? "border-primary" : "border-input",
|
||||
!activeFile ? "w-full" : "min-h-32 min-w-32",
|
||||
className,
|
||||
|
||||
@@ -11,7 +11,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-card px-3 py-2 text-sm ring-offset-background selection:bg-foreground selection:text-background file:border-0 file:bg-transparent file:font-medium file:text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background selection:bg-foreground selection:text-background file:border-0 file:bg-transparent file:font-medium file:text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
|
||||
@@ -63,11 +63,11 @@ const PaginationPrevious = ({
|
||||
<PaginationLink
|
||||
aria-label="Go to previous page"
|
||||
size="default"
|
||||
className={cn("gap-1 pl-2.5", className)}
|
||||
className={cn("h-10 gap-1 pl-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
<span>Previous</span>
|
||||
<span className="max-sm:hidden">Previous</span>
|
||||
</PaginationLink>
|
||||
);
|
||||
PaginationPrevious.displayName = "PaginationPrevious";
|
||||
@@ -79,10 +79,10 @@ const PaginationNext = ({
|
||||
<PaginationLink
|
||||
aria-label="Go to next page"
|
||||
size="default"
|
||||
className={cn("gap-1 pr-2.5", className)}
|
||||
className={cn("h-10 gap-1 pr-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<span>Next</span>
|
||||
<span className="max-sm:hidden">Next</span>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</PaginationLink>
|
||||
);
|
||||
|
||||
@@ -51,7 +51,7 @@ const RadioGroupItemButton = React.forwardRef<
|
||||
<RadioGroupPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"group flex min-w-32 cursor-pointer items-center space-x-3 space-y-0 rounded-lg border px-4 py-3 font-medium transition-all hover:border-foreground/25 hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 peer-hover:border-nonce data-[state=checked]:border-muted-foreground",
|
||||
"group flex min-w-32 cursor-pointer items-center space-x-3 space-y-0 rounded-lg border px-4 py-3 font-medium transition-all hover:border-foreground/25 hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 peer-hover:border-nonce data-[state=checked]:border-muted-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -31,7 +31,7 @@ function randomName() {
|
||||
|
||||
function Component() {
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col gap-10 bg-background p-6 text-foreground">
|
||||
<div className="flex min-h-dvh flex-col gap-10 bg-background p-6 text-foreground">
|
||||
<BadgeContainer label="5 items, no placeholder, no label, value filled">
|
||||
<SelectDemo listItems={5} selectFirst />
|
||||
</BadgeContainer>
|
||||
|
||||
@@ -21,7 +21,7 @@ const SelectTrigger = React.forwardRef<
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full items-center justify-between gap-2 rounded-md border border-input bg-card px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
"flex h-10 w-full items-center justify-between gap-2 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -114,7 +114,9 @@ function TableDemo(props: {
|
||||
<TableRow
|
||||
key={invoice.invoice}
|
||||
linkBox={props.linkBox}
|
||||
className={cn(props.linkBox && "cursor-pointer hover:bg-muted")}
|
||||
className={cn(
|
||||
props.linkBox && "cursor-pointer hover:bg-accent/50",
|
||||
)}
|
||||
>
|
||||
<TableCell className="font-medium">
|
||||
<Link
|
||||
|
||||
@@ -25,7 +25,8 @@ const TableHeader = React.forwardRef<
|
||||
<thead
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-border border-b bg-muted/50 [&_tr]:border-b",
|
||||
"border-border border-b bg-background",
|
||||
"sticky top-0 z-20 border-none before:absolute before:inset-0 before:border-border before:border-b",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -53,7 +54,7 @@ const TableFooter = React.forwardRef<
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-border border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||
"border-border border-t bg-card font-medium [&>tr]:last:border-b-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -125,10 +126,20 @@ const TableCaption = React.forwardRef<
|
||||
));
|
||||
TableCaption.displayName = "TableCaption";
|
||||
|
||||
function TableContainer(props: { children: React.ReactNode }) {
|
||||
function TableContainer(props: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
scrollableContainerClassName?: string;
|
||||
}) {
|
||||
return (
|
||||
<ScrollShadow
|
||||
className="relative whitespace-nowrap rounded-lg border border-border"
|
||||
shadowClassName="z-30"
|
||||
disableTopShadow
|
||||
className={cn(
|
||||
"relative whitespace-nowrap rounded-lg border border-border bg-card",
|
||||
props.className,
|
||||
)}
|
||||
scrollableClassName={props.scrollableContainerClassName}
|
||||
shadowColor="hsl(var(--muted))"
|
||||
>
|
||||
{props.children}
|
||||
|
||||
@@ -51,7 +51,7 @@ export function TabLinks(props: {
|
||||
href={tab.href}
|
||||
aria-disabled={tab.isDisabled}
|
||||
className={cn(
|
||||
"relative h-auto rounded-lg px-3 font-normal text-muted-foreground text-sm hover:bg-muted lg:text-sm",
|
||||
"relative h-auto rounded-lg px-3 font-normal text-muted-foreground text-sm hover:bg-accent lg:text-sm",
|
||||
!tab.isActive && !tab.isDisabled && "hover:text-foreground",
|
||||
tab.isDisabled && "pointer-events-none",
|
||||
tab.isActive && "!text-foreground",
|
||||
|
||||
@@ -10,7 +10,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"flex min-h-[80px] w-full rounded-md border border-input bg-card px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
|
||||
@@ -36,6 +36,8 @@ export function ToolTipLabel(props: {
|
||||
leftIcon?: React.ReactNode;
|
||||
hoverable?: boolean;
|
||||
contentClassName?: string;
|
||||
side?: "top" | "right" | "bottom" | "left";
|
||||
align?: "center" | "start" | "end";
|
||||
}) {
|
||||
if (!props.label) {
|
||||
return props.children;
|
||||
@@ -48,6 +50,8 @@ export function ToolTipLabel(props: {
|
||||
{props.children}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side={props.side}
|
||||
align={props.align}
|
||||
sideOffset={10}
|
||||
className={cn(
|
||||
"max-w-[400px] whitespace-normal leading-relaxed",
|
||||
|
||||
@@ -41,3 +41,6 @@ export const BASE_URL = isProd
|
||||
: "http://localhost:3000") || "https://thirdweb-dev.com";
|
||||
|
||||
export const NEXT_PUBLIC_NEBULA_URL = process.env.NEXT_PUBLIC_NEBULA_URL;
|
||||
|
||||
export const INSIGHT_SERVICE_API_KEY =
|
||||
process.env.INSIGHT_SERVICE_API_KEY || "";
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
:root,
|
||||
[data-theme="light"] {
|
||||
/* bg - neutral */
|
||||
--background: 0 0% 100%;
|
||||
--background: 0 0% 98%;
|
||||
--popover: 0 0% 100%;
|
||||
--card: 0 0% 100%;
|
||||
--secondary: 0 0% 92%;
|
||||
--muted: 0 0% 95%;
|
||||
--accent: 0 0% 95%;
|
||||
--secondary: 0 0% 90%;
|
||||
--muted: 0 0% 93%;
|
||||
--accent: 0 0% 93%;
|
||||
--inverted: 0 0% 4%;
|
||||
|
||||
/* bg - colorful */
|
||||
@@ -34,9 +34,10 @@
|
||||
--destructive-text: 357.15deg 100% 68.72%;
|
||||
|
||||
/* Borders */
|
||||
--border: 0 0% 80%;
|
||||
--input: 0 0% 80%;
|
||||
--ring: 0 0% 85%;
|
||||
--border: 0 0% 85%;
|
||||
--active-border: 0 0% 70%;
|
||||
--input: 0 0% 85%;
|
||||
--ring: 0 0% 80%;
|
||||
|
||||
/* Others */
|
||||
--radius: 0.5rem;
|
||||
@@ -46,12 +47,12 @@
|
||||
[data-theme="dark"] {
|
||||
/* bg - neutral */
|
||||
--background: 0 0% 0%;
|
||||
--card: 0 0% 0%;
|
||||
--card: 0 0% 3.92%;
|
||||
--popover: 0 0% 0%;
|
||||
--secondary: 0 0% 10%;
|
||||
--muted: 0 0% 9%;
|
||||
--accent: 0 0% 9%;
|
||||
--inverted: 0 0% 98%;
|
||||
--secondary: 0 0% 11%;
|
||||
--muted: 0 0% 11%;
|
||||
--accent: 0 0% 11%;
|
||||
--inverted: 0 0% 100%;
|
||||
|
||||
/* bg - colorful */
|
||||
--primary: 221 83% 54%;
|
||||
@@ -74,6 +75,7 @@
|
||||
|
||||
/* Borders */
|
||||
--border: 0 0% 15%;
|
||||
--active-border: 0 0% 22%;
|
||||
--ring: 0 0% 30%;
|
||||
--input: 0 0% 15%;
|
||||
}
|
||||
@@ -157,10 +159,14 @@ input:-webkit-autofill:active {
|
||||
transition: background-color 5000s ease-in-out 0s;
|
||||
}
|
||||
|
||||
.shiki,
|
||||
.shiki span {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.dark .shiki,
|
||||
.dark .shiki span {
|
||||
color: var(--shiki-dark) !important;
|
||||
background-color: var(--background) !important;
|
||||
/* Optional, if you also want font styles */
|
||||
font-style: var(--shiki-dark-font-style) !important;
|
||||
font-weight: var(--shiki-dark-font-weight) !important;
|
||||
|
||||
@@ -76,8 +76,8 @@ export const engineKeys = {
|
||||
[...engineKeys.all, address, "instances"] as const,
|
||||
backendWallets: (instance: string) =>
|
||||
[...engineKeys.all, instance, "backendWallets"] as const,
|
||||
transactions: (instance: string) =>
|
||||
[...engineKeys.all, instance, "transactions"] as const,
|
||||
transactions: (instance: string, params: object) =>
|
||||
[...engineKeys.all, instance, "transactions", params] as const,
|
||||
permissions: (instance: string) =>
|
||||
[...engineKeys.all, instance, "permissions"] as const,
|
||||
accessTokens: (instance: string) =>
|
||||
|
||||
@@ -93,14 +93,14 @@ export const CustomConnectWallet = (props: {
|
||||
|
||||
const chainSections: NetworkSelectorProps["sections"] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
label: "Favorites",
|
||||
chains: favoriteChainsWithMetadata,
|
||||
},
|
||||
{
|
||||
label: "Recent",
|
||||
chains: recentlyUsedChainsWithMetadata,
|
||||
},
|
||||
{
|
||||
label: "Favorites",
|
||||
chains: favoriteChainsWithMetadata,
|
||||
},
|
||||
{
|
||||
label: "Popular",
|
||||
chains: popularChainsWithMetadata,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { analyticsServerProxy, apiServerProxy } from "@/actions/proxies";
|
||||
import type { Team } from "@/api/team";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { THIRDWEB_ANALYTICS_API_HOST, THIRDWEB_API_HOST } from "constants/urls";
|
||||
import { useAllChainsData } from "hooks/chains/allChains";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import type { UserOpStats } from "types/analytics";
|
||||
@@ -216,19 +216,30 @@ export function useAccountCredits() {
|
||||
return useQuery({
|
||||
queryKey: accountKeys.credits(address || ""),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/account/credits`, {
|
||||
type Result = {
|
||||
data: BillingCredit[];
|
||||
error?: { message: string };
|
||||
};
|
||||
|
||||
const res = await apiServerProxy<Result>({
|
||||
pathname: "/v1/account/credits",
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
const json = res.data;
|
||||
|
||||
if (json.error) {
|
||||
throw new Error(json.error.message);
|
||||
}
|
||||
|
||||
const credits = (json.data as BillingCredit[]).filter(
|
||||
const credits = json.data.filter(
|
||||
(credit) =>
|
||||
credit.remainingValueUsdCents > 0 &&
|
||||
(!credit.expiresAt || credit.expiresAt > new Date().toISOString()) &&
|
||||
@@ -241,6 +252,8 @@ export function useAccountCredits() {
|
||||
});
|
||||
}
|
||||
|
||||
type UserOpUsageQueryResult = (UserOpStats & { chainId?: string })[];
|
||||
|
||||
async function getUserOpUsage(args: {
|
||||
clientId: string;
|
||||
from?: Date;
|
||||
@@ -249,32 +262,35 @@ async function getUserOpUsage(args: {
|
||||
}) {
|
||||
const { clientId, from, to, period } = args;
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.append("clientId", clientId);
|
||||
const searchParams: Record<string, string> = {
|
||||
clientId,
|
||||
};
|
||||
|
||||
if (from) {
|
||||
searchParams.append("from", from.toISOString());
|
||||
searchParams.from = from.toISOString();
|
||||
}
|
||||
if (to) {
|
||||
searchParams.append("to", to.toISOString());
|
||||
searchParams.to = to.toISOString();
|
||||
}
|
||||
if (period) {
|
||||
searchParams.append("period", period);
|
||||
searchParams.period = period;
|
||||
}
|
||||
const res = await fetch(
|
||||
`${THIRDWEB_ANALYTICS_API_HOST}/v1/user-ops?${searchParams.toString()}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
const json = await res.json();
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error(json.message);
|
||||
const res = await analyticsServerProxy<{ data: UserOpUsageQueryResult }>({
|
||||
pathname: "/v1/user-ops",
|
||||
method: "GET",
|
||||
searchParams: searchParams,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
const json = res.data;
|
||||
|
||||
return json.data;
|
||||
}
|
||||
|
||||
@@ -297,13 +313,12 @@ export function useUserOpUsageAggregate(args: {
|
||||
"all",
|
||||
),
|
||||
queryFn: async () => {
|
||||
const userOpStats: (UserOpStats & { chainId?: string })[] =
|
||||
await getUserOpUsage({
|
||||
clientId,
|
||||
from,
|
||||
to,
|
||||
period: "all",
|
||||
});
|
||||
const userOpStats = await getUserOpUsage({
|
||||
clientId,
|
||||
from,
|
||||
to,
|
||||
period: "all",
|
||||
});
|
||||
|
||||
// Aggregate stats across wallet types
|
||||
return userOpStats.reduce(
|
||||
@@ -369,7 +384,13 @@ export function useUpdateAccount() {
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (input: UpdateAccountInput) => {
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/account`, {
|
||||
type Result = {
|
||||
data: object;
|
||||
error?: { message: string };
|
||||
};
|
||||
|
||||
const res = await apiServerProxy<Result>({
|
||||
pathname: "/v1/account",
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -377,7 +398,11 @@ export function useUpdateAccount() {
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
|
||||
const json = await res.json();
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
const json = res.data;
|
||||
|
||||
if (json.error) {
|
||||
throw new Error(json.error.message);
|
||||
@@ -400,15 +425,25 @@ export function useUpdateNotifications() {
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (input: UpdateAccountNotificationsInput) => {
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/account/notifications`, {
|
||||
method: "PUT",
|
||||
type Result = {
|
||||
data: object;
|
||||
error?: { message: string };
|
||||
};
|
||||
|
||||
const res = await apiServerProxy<Result>({
|
||||
pathname: "/v1/account/notifications",
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ preferences: input }),
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
const json = res.data;
|
||||
|
||||
if (json.error) {
|
||||
throw new Error(json.error.message);
|
||||
@@ -430,21 +465,31 @@ export function useConfirmEmail() {
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (input: ConfirmEmailInput) => {
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/account/confirmEmail`, {
|
||||
method: "PUT",
|
||||
type Result = {
|
||||
error?: { message: string };
|
||||
data: { team: Team; account: Account };
|
||||
};
|
||||
|
||||
const res = await apiServerProxy<Result>({
|
||||
pathname: "/v1/account/confirmEmail",
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
const json = res.data;
|
||||
|
||||
if (json.error) {
|
||||
throw new Error(json.error.message);
|
||||
}
|
||||
|
||||
return json.data as { team: Team; account: Account };
|
||||
return json.data;
|
||||
},
|
||||
onSuccess: async () => {
|
||||
// invalidate related cache, since could be relinking account
|
||||
@@ -469,18 +514,25 @@ export function useResendEmailConfirmation() {
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
const res = await fetch(
|
||||
`${THIRDWEB_API_HOST}/v1/account/resendEmailConfirmation`,
|
||||
{
|
||||
method: "POST",
|
||||
type Result = {
|
||||
error?: { message: string };
|
||||
data: object;
|
||||
};
|
||||
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
const res = await apiServerProxy<Result>({
|
||||
pathname: "/v1/account/resendEmailConfirmation",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
);
|
||||
const json = await res.json();
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
const json = res.data;
|
||||
|
||||
if (json.error) {
|
||||
throw new Error(json.error.message);
|
||||
@@ -503,18 +555,29 @@ export function useApiKeys(props: {
|
||||
return useQuery({
|
||||
queryKey: apiKeys.keys(address || ""),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/keys`, {
|
||||
type Result = {
|
||||
data: ApiKey[];
|
||||
error?: { message: string };
|
||||
};
|
||||
|
||||
const res = await apiServerProxy<Result>({
|
||||
pathname: "/v1/keys",
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
const json = res.data;
|
||||
|
||||
if (json.error) {
|
||||
throw new Error(json.error.message);
|
||||
}
|
||||
return json.data as ApiKey[];
|
||||
return json.data;
|
||||
},
|
||||
enabled: !!address && props.isLoggedIn,
|
||||
});
|
||||
@@ -526,22 +589,32 @@ export function useCreateApiKey() {
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (input: CreateKeyInput) => {
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/keys`, {
|
||||
type Result = {
|
||||
data: ApiKey;
|
||||
error?: { message: string };
|
||||
};
|
||||
|
||||
const res = await apiServerProxy<Result>({
|
||||
pathname: "/v1/keys",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
const json = res.data;
|
||||
|
||||
if (json.error) {
|
||||
throw new Error(json.error.message);
|
||||
}
|
||||
|
||||
return json.data as ApiKey;
|
||||
return json.data;
|
||||
},
|
||||
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: apiKeys.keys(address || ""),
|
||||
@@ -556,16 +629,25 @@ export function useUpdateApiKey() {
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (input: UpdateKeyInput) => {
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/keys/${input.id}`, {
|
||||
method: "PUT",
|
||||
type Result = {
|
||||
data: ApiKey;
|
||||
error?: { message: string };
|
||||
};
|
||||
|
||||
const res = await apiServerProxy<Result>({
|
||||
pathname: `/v1/keys/${input.id}`,
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
|
||||
const json = await res.json();
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
const json = res.data;
|
||||
|
||||
if (json.error) {
|
||||
throw new Error(json.error.message);
|
||||
@@ -587,14 +669,25 @@ export function useRevokeApiKey() {
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (id: string) => {
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/keys/${id}/revoke`, {
|
||||
type Result = {
|
||||
data: ApiKey;
|
||||
error?: { message: string };
|
||||
};
|
||||
|
||||
const res = await apiServerProxy<Result>({
|
||||
pathname: `/v1/keys/${id}/revoke`,
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
const json = res.data;
|
||||
|
||||
if (json.error) {
|
||||
throw new Error(json.error.message);
|
||||
@@ -614,21 +707,35 @@ export const usePolicies = (serviceId?: string) => {
|
||||
return useQuery({
|
||||
queryKey: ["policies", serviceId],
|
||||
queryFn: async () => {
|
||||
const res = await fetch(
|
||||
`${THIRDWEB_API_HOST}/v1/policies?serviceId=${serviceId}`,
|
||||
{
|
||||
method: "GET",
|
||||
if (!serviceId) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
type Result = {
|
||||
data: ApiKeyServicePolicy;
|
||||
error?: { message: string };
|
||||
};
|
||||
|
||||
const res = await apiServerProxy<Result>({
|
||||
pathname: "/v1/policies",
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
);
|
||||
const json = await res.json();
|
||||
searchParams: {
|
||||
serviceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
const json = res.data;
|
||||
if (json.error) {
|
||||
throw new Error(json.error.message);
|
||||
}
|
||||
return json.data as ApiKeyServicePolicy;
|
||||
return json.data;
|
||||
},
|
||||
enabled: !!serviceId,
|
||||
});
|
||||
@@ -641,9 +748,14 @@ export const useUpdatePolicies = () => {
|
||||
serviceId: string;
|
||||
data: ApiKeyServicePolicy;
|
||||
}) => {
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/policies`, {
|
||||
method: "POST",
|
||||
type Result = {
|
||||
data: ApiKeyServicePolicy;
|
||||
error?: { message: string };
|
||||
};
|
||||
|
||||
const res = await apiServerProxy<Result>({
|
||||
pathname: "/v1/policies",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -652,11 +764,16 @@ export const useUpdatePolicies = () => {
|
||||
data: input.data,
|
||||
}),
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
const json = res.data;
|
||||
if (json.error) {
|
||||
throw new Error(json.error.message);
|
||||
}
|
||||
return json.data as ApiKeyServicePolicy;
|
||||
return json.data;
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
return queryClient.invalidateQueries({
|
||||
@@ -673,19 +790,25 @@ export function useRevokeAuthorizedWallet() {
|
||||
return useMutation({
|
||||
mutationFn: async (variables: { authorizedWalletId: string }) => {
|
||||
const { authorizedWalletId } = variables;
|
||||
type Result = {
|
||||
data: object;
|
||||
error?: { message: string };
|
||||
};
|
||||
|
||||
const res = await fetch(
|
||||
`${THIRDWEB_API_HOST}/v1/authorized-wallets/${authorizedWalletId}/revoke`,
|
||||
{
|
||||
method: "POST",
|
||||
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
const res = await apiServerProxy<Result>({
|
||||
pathname: `/v1/authorized-wallets/${authorizedWalletId}/revoke`,
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
);
|
||||
const json = await res.json();
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
const json = res.data;
|
||||
|
||||
if (json.error) {
|
||||
throw new Error(json.error.message);
|
||||
@@ -706,20 +829,30 @@ export function useAuthorizedWallets() {
|
||||
return useQuery({
|
||||
queryKey: authorizedWallets.authorizedWallets(address || ""),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/authorized-wallets`, {
|
||||
method: "GET",
|
||||
type Result = {
|
||||
data: AuthorizedWallet[];
|
||||
error?: { message: string };
|
||||
};
|
||||
|
||||
const res = await apiServerProxy<Result>({
|
||||
pathname: "/v1/authorized-wallets",
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
const json = res.data;
|
||||
|
||||
if (json.error) {
|
||||
throw new Error(json.error.message);
|
||||
}
|
||||
|
||||
return json.data as AuthorizedWallet[];
|
||||
return json.data;
|
||||
},
|
||||
enabled: !!address,
|
||||
gcTime: 0,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { type ThirdwebContract, ZERO_ADDRESS } from "thirdweb";
|
||||
import { hasRole } from "thirdweb/extensions/permissions";
|
||||
import { useActiveAccount, useReadContract } from "thirdweb/react";
|
||||
import type { RequiredParam } from "utils/types";
|
||||
|
||||
export function useIsAdmin(contract: ThirdwebContract, failOpen = true) {
|
||||
const address = useActiveAccount()?.address;
|
||||
@@ -19,7 +18,7 @@ export function useIsAdmin(contract: ThirdwebContract, failOpen = true) {
|
||||
|
||||
export function useIsAdminOrSelf(
|
||||
contract: ThirdwebContract,
|
||||
self: RequiredParam<string>,
|
||||
self: string | null | undefined,
|
||||
) {
|
||||
const address = useActiveAccount()?.address;
|
||||
const isAdmin = useIsAdmin(contract);
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { apiServerProxy } from "@/actions/proxies";
|
||||
import {
|
||||
keepPreviousData,
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import type { ResultItem } from "app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/metrics/components/StatusCodes";
|
||||
import { THIRDWEB_API_HOST } from "constants/urls";
|
||||
import type { EngineBackendWalletType } from "lib/engine";
|
||||
import { useState } from "react";
|
||||
import { useActiveAccount, useActiveWalletChain } from "thirdweb/react";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import invariant from "tiny-invariant";
|
||||
import type { EngineStatus } from "../../../app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/overview/components/transactions-table";
|
||||
import { engineKeys } from "../cache-keys";
|
||||
|
||||
export type EngineTier = "STARTER" | "PREMIUM" | "ENTERPRISE";
|
||||
@@ -47,37 +53,6 @@ const getEngineRequestHeaders = (token: string | null): HeadersInit => {
|
||||
};
|
||||
};
|
||||
|
||||
// TODO - add authToken in queryKey instead
|
||||
export function useEngineInstances() {
|
||||
const address = useActiveAccount()?.address;
|
||||
return useQuery({
|
||||
queryKey: engineKeys.instances(address || ""),
|
||||
queryFn: async (): Promise<EngineInstance[]> => {
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/engine`, {
|
||||
method: "GET",
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`Unexpected status ${res.status}: ${await res.text()}`);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
const instances = (json.data?.instances as EngineInstance[]) || [];
|
||||
|
||||
return instances.map((instance) => {
|
||||
// Sanitize: Add trailing slash if not present.
|
||||
const url = instance.url.endsWith("/")
|
||||
? instance.url
|
||||
: `${instance.url}/`;
|
||||
return {
|
||||
...instance,
|
||||
url,
|
||||
};
|
||||
});
|
||||
},
|
||||
enabled: !!address,
|
||||
});
|
||||
}
|
||||
|
||||
// GET Requests
|
||||
export type BackendWallet = {
|
||||
address: string;
|
||||
@@ -98,7 +73,7 @@ export function useEngineBackendWallets(params: {
|
||||
}) {
|
||||
const { instanceUrl, authToken } = params;
|
||||
return useQuery({
|
||||
queryKey: [engineKeys.backendWallets(instanceUrl), authToken],
|
||||
queryKey: [...engineKeys.backendWallets(instanceUrl), authToken],
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`${instanceUrl}backend-wallet/get-all?limit=50`, {
|
||||
method: "GET",
|
||||
@@ -210,16 +185,19 @@ export function useEngineGetDeploymentPublicConfiguration(
|
||||
return useQuery<DeploymentPublicConfigurationResponse>({
|
||||
queryKey: engineKeys.deploymentPublicConfiguration(),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(
|
||||
`${THIRDWEB_API_HOST}/v1/teams/${input.teamSlug}/engine/deployments/public-configuration`,
|
||||
{ method: "GET" },
|
||||
);
|
||||
const res = await apiServerProxy<{
|
||||
data: DeploymentPublicConfigurationResponse;
|
||||
}>({
|
||||
pathname: `/v1/teams/${input.teamSlug}/engine/deployments/public-configuration`,
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Unexpected status ${res.status}: ${await res.text()}`);
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return json.data as DeploymentPublicConfigurationResponse;
|
||||
const json = res.data;
|
||||
return json.data;
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -233,22 +211,19 @@ interface UpdateDeploymentInput {
|
||||
export function useEngineUpdateDeployment() {
|
||||
return useMutation({
|
||||
mutationFn: async (input: UpdateDeploymentInput) => {
|
||||
const res = await fetch(
|
||||
`${THIRDWEB_API_HOST}/v1/teams/${input.teamSlug}/engine/deployments/${input.deploymentId}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
serverVersion: input.serverVersion,
|
||||
}),
|
||||
const res = await apiServerProxy({
|
||||
pathname: `/v1/teams/${input.teamSlug}/engine/deployments/${input.deploymentId}`,
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
);
|
||||
// we never use the response body
|
||||
res.body?.cancel();
|
||||
body: JSON.stringify({
|
||||
serverVersion: input.serverVersion,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Unexpected status ${res.status}: ${await res.text()}`);
|
||||
throw new Error(res.error);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -262,13 +237,13 @@ export function useEngineRemoveFromDashboard() {
|
||||
mutationFn: async (instanceId: string) => {
|
||||
invariant(instanceId, "instance is required");
|
||||
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/engine/${instanceId}`, {
|
||||
const res = await apiServerProxy({
|
||||
pathname: `/v1/engine/${instanceId}`,
|
||||
method: "DELETE",
|
||||
});
|
||||
// we never use the response body
|
||||
res.body?.cancel();
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Unexpected status ${res.status}: ${await res.text()}`);
|
||||
throw new Error(res.error);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -296,20 +271,17 @@ export function useEngineDeleteCloudHosted() {
|
||||
reason,
|
||||
feedback,
|
||||
}: DeleteCloudHostedInput) => {
|
||||
const res = await fetch(
|
||||
`${THIRDWEB_API_HOST}/v2/engine/deployments/${deploymentId}/infrastructure/delete`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ reason, feedback }),
|
||||
const res = await apiServerProxy({
|
||||
pathname: `/v2/engine/deployments/${deploymentId}/infrastructure/delete`,
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
);
|
||||
// we never use the response body
|
||||
res.body?.cancel();
|
||||
body: JSON.stringify({ reason, feedback }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Unexpected status ${res.status}: ${await res.text()}`);
|
||||
throw new Error(res.error);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -333,21 +305,19 @@ export function useEngineEditInstance() {
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ instanceId, name, url }: EditEngineInstanceInput) => {
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/engine/${instanceId}`, {
|
||||
const res = await apiServerProxy({
|
||||
pathname: `/v1/engine/${instanceId}`,
|
||||
method: "PUT",
|
||||
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ name, url }),
|
||||
});
|
||||
// we never use the response body
|
||||
res.body?.cancel();
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Unexpected status ${res.status}: ${await res.text()}`);
|
||||
throw new Error(res.error);
|
||||
}
|
||||
},
|
||||
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.instances(address || ""),
|
||||
@@ -419,13 +389,29 @@ export function useEngineTransactions(params: {
|
||||
instanceUrl: string;
|
||||
autoUpdate: boolean;
|
||||
authToken: string;
|
||||
queryParams?: {
|
||||
limit?: number;
|
||||
page?: number;
|
||||
status?: EngineStatus;
|
||||
};
|
||||
}) {
|
||||
const { instanceUrl, autoUpdate, authToken } = params;
|
||||
|
||||
return useQuery({
|
||||
queryKey: engineKeys.transactions(instanceUrl),
|
||||
queryKey: engineKeys.transactions(instanceUrl, params),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`${instanceUrl}transaction/get-all`, {
|
||||
const url = new URL(`${instanceUrl}transaction/get-all`);
|
||||
if (params.queryParams) {
|
||||
for (const key in params.queryParams) {
|
||||
const value =
|
||||
params.queryParams[key as keyof typeof params.queryParams];
|
||||
if (value !== undefined) {
|
||||
url.searchParams.append(key, value.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: getEngineRequestHeaders(authToken),
|
||||
});
|
||||
@@ -434,8 +420,8 @@ export function useEngineTransactions(params: {
|
||||
|
||||
return (json.result as TransactionResponse) || {};
|
||||
},
|
||||
enabled: !!instanceUrl,
|
||||
refetchInterval: autoUpdate ? 4_000 : false,
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -483,12 +469,12 @@ export function useEngineBackendWalletBalance(params: {
|
||||
instanceUrl: string;
|
||||
address: string;
|
||||
authToken: string;
|
||||
chainId: number;
|
||||
}) {
|
||||
const { instanceUrl, address, authToken } = params;
|
||||
const chainId = useActiveWalletChain()?.id;
|
||||
const { instanceUrl, address, authToken, chainId } = params;
|
||||
|
||||
return useQuery({
|
||||
queryKey: engineKeys.backendWalletBalance(address, chainId || 1),
|
||||
queryKey: engineKeys.backendWalletBalance(address, chainId),
|
||||
queryFn: async () => {
|
||||
invariant(chainId, "chainId is required");
|
||||
|
||||
@@ -1675,14 +1661,16 @@ export function useEngineSystemMetrics(engineId: string) {
|
||||
return useQuery({
|
||||
queryKey: engineKeys.systemMetrics(engineId),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(
|
||||
`${THIRDWEB_API_HOST}/v1/engine/${engineId}/metrics`,
|
||||
);
|
||||
const res = await apiServerProxy({
|
||||
method: "GET",
|
||||
pathname: `/v1/engine/${engineId}/metrics`,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
setEnabled(false);
|
||||
throw new Error(`Unexpected status ${res.status}: ${await res.text()}`);
|
||||
throw new Error(res.error);
|
||||
}
|
||||
const json = (await res.json()) as EngineResourceMetrics;
|
||||
const json = res.data as EngineResourceMetrics;
|
||||
return json;
|
||||
},
|
||||
|
||||
@@ -1708,16 +1696,19 @@ export function useEngineAlertRules(engineId: string) {
|
||||
return useQuery({
|
||||
queryKey: engineKeys.alertRules(engineId),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(
|
||||
`${THIRDWEB_API_HOST}/v1/engine/${engineId}/alert-rules`,
|
||||
{ method: "GET" },
|
||||
);
|
||||
const res = await apiServerProxy<{
|
||||
data: EngineAlertRule[];
|
||||
}>({
|
||||
pathname: `/v1/engine/${engineId}/alert-rules`,
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Unexpected status ${res.status}: ${await res.text()}`);
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return json.data as EngineAlertRule[];
|
||||
const json = res.data;
|
||||
return json.data;
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1734,16 +1725,23 @@ export function useEngineAlerts(engineId: string, limit: number, offset = 0) {
|
||||
return useQuery({
|
||||
queryKey: engineKeys.alerts(engineId),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(
|
||||
`${THIRDWEB_API_HOST}/v1/engine/${engineId}/alerts?limit=${limit}&offset=${offset}`,
|
||||
{ method: "GET" },
|
||||
);
|
||||
const res = await apiServerProxy<{
|
||||
data: EngineAlert[];
|
||||
}>({
|
||||
pathname: `/v1/engine/${engineId}/alerts`,
|
||||
searchParams: {
|
||||
limit: `${limit}`,
|
||||
offset: `${offset}`,
|
||||
},
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Unexpected status ${res.status}: ${await res.text()}`);
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return json.data as EngineAlert[];
|
||||
const json = res.data;
|
||||
return json.data;
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1778,16 +1776,19 @@ export function useEngineNotificationChannels(engineId: string) {
|
||||
return useQuery({
|
||||
queryKey: engineKeys.notificationChannels(engineId),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(
|
||||
`${THIRDWEB_API_HOST}/v1/engine/${engineId}/notification-channels`,
|
||||
{ method: "GET" },
|
||||
);
|
||||
const res = await apiServerProxy<{
|
||||
data: EngineNotificationChannel[];
|
||||
}>({
|
||||
pathname: `/v1/engine/${engineId}/notification-channels`,
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Unexpected status ${res.status}: ${await res.text()}`);
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return json.data as EngineNotificationChannel[];
|
||||
const json = res.data;
|
||||
return json.data;
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1803,22 +1804,23 @@ export function useEngineCreateNotificationChannel(engineId: string) {
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (input: CreateNotificationChannelInput) => {
|
||||
const res = await fetch(
|
||||
`${THIRDWEB_API_HOST}/v1/engine/${engineId}/notification-channels`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(input),
|
||||
const res = await apiServerProxy<{
|
||||
data: EngineNotificationChannel;
|
||||
}>({
|
||||
pathname: `/v1/engine/${engineId}/notification-channels`,
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
);
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Unexpected status ${res.status}: ${await res.text()}`);
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return json.data as EngineNotificationChannel;
|
||||
const json = res.data;
|
||||
return json.data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
@@ -1833,16 +1835,14 @@ export function useEngineDeleteNotificationChannel(engineId: string) {
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (notificationChannelId: string) => {
|
||||
const res = await fetch(
|
||||
`${THIRDWEB_API_HOST}/v1/engine/${engineId}/notification-channels/${notificationChannelId}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
},
|
||||
);
|
||||
const res = await apiServerProxy({
|
||||
pathname: `/v1/engine/${engineId}/notification-channels/${notificationChannelId}`,
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Unexpected status ${res.status}: ${await res.text()}`);
|
||||
throw new Error(res.error);
|
||||
}
|
||||
res.body?.cancel();
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
|
||||
@@ -1,47 +1,36 @@
|
||||
import { getTokenBalancesFromMoralis } from "@/actions/getBalancesFromMoralis";
|
||||
import {
|
||||
queryOptions,
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import type {
|
||||
BalanceQueryRequest,
|
||||
BalanceQueryResponse,
|
||||
} from "pages/api/moralis/balances";
|
||||
import { toast } from "sonner";
|
||||
import { type ThirdwebContract, sendAndConfirmTransaction } from "thirdweb";
|
||||
import { distribute, distributeByToken } from "thirdweb/extensions/split";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import invariant from "tiny-invariant";
|
||||
|
||||
async function getSplitBalances(contract: ThirdwebContract) {
|
||||
const query = await fetch("/api/moralis/balances", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
chainId: contract.chain.id,
|
||||
address: contract.address,
|
||||
} as BalanceQueryRequest),
|
||||
});
|
||||
|
||||
if (query.status >= 400) {
|
||||
throw new Error(await query.json().then((r) => r.error));
|
||||
}
|
||||
return query.json() as Promise<BalanceQueryResponse>;
|
||||
}
|
||||
|
||||
function getQuery(contract: ThirdwebContract) {
|
||||
function getTokenBalancesQuery(contract: ThirdwebContract) {
|
||||
return queryOptions({
|
||||
queryKey: ["split-balances", contract.chain.id, contract.address],
|
||||
queryFn: () => getSplitBalances(contract),
|
||||
queryFn: async () => {
|
||||
const res = await getTokenBalancesFromMoralis({
|
||||
chainId: contract.chain.id,
|
||||
contractAddress: contract.address,
|
||||
});
|
||||
|
||||
if (!res.data) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
return res.data;
|
||||
},
|
||||
retry: false,
|
||||
});
|
||||
}
|
||||
|
||||
export function useSplitBalances(contract: ThirdwebContract) {
|
||||
return useQuery(getQuery(contract));
|
||||
return useQuery(getTokenBalancesQuery(contract));
|
||||
}
|
||||
|
||||
export function useSplitDistributeFunds(contract: ThirdwebContract) {
|
||||
@@ -53,8 +42,8 @@ export function useSplitDistributeFunds(contract: ThirdwebContract) {
|
||||
invariant(account, "No active account");
|
||||
const balances =
|
||||
// get the cached data if it exists, otherwise fetch it
|
||||
queryClient.getQueryData(getQuery(contract).queryKey) ||
|
||||
(await queryClient.fetchQuery(getQuery(contract)));
|
||||
queryClient.getQueryData(getTokenBalancesQuery(contract).queryKey) ||
|
||||
(await queryClient.fetchQuery(getTokenBalancesQuery(contract)));
|
||||
|
||||
const distributions = balances
|
||||
.filter((token) => token.display_balance !== "0.0")
|
||||
@@ -80,7 +69,7 @@ export function useSplitDistributeFunds(contract: ThirdwebContract) {
|
||||
return await Promise.all(distributions);
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries(getQuery(contract));
|
||||
queryClient.invalidateQueries(getTokenBalancesQuery(contract));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getWalletNFTs } from "@/actions/getWalletNFTs";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { WalletNFTApiReturn } from "pages/api/wallet/nfts/[chainId]";
|
||||
import invariant from "tiny-invariant";
|
||||
|
||||
export function useWalletNFTs(params: {
|
||||
chainId: number;
|
||||
@@ -8,10 +9,11 @@ export function useWalletNFTs(params: {
|
||||
return useQuery({
|
||||
queryKey: ["walletNfts", params.chainId, params.walletAddress],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(
|
||||
`/api/wallet/nfts/${params.chainId}?owner=${params.walletAddress}`,
|
||||
);
|
||||
return (await response.json()) as WalletNFTApiReturn;
|
||||
invariant(params.walletAddress, "walletAddress is required");
|
||||
return getWalletNFTs({
|
||||
chainId: params.chainId,
|
||||
owner: params.walletAddress,
|
||||
});
|
||||
},
|
||||
enabled: !!params.walletAddress,
|
||||
});
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ export function BuyFundsSection(props: { chain: ChainMetadata }) {
|
||||
return (
|
||||
<section>
|
||||
<SectionTitle title="Buy Funds" />
|
||||
<div className="flex justify-center rounded-lg border border-border bg-muted/30 px-4 py-10">
|
||||
<div className="flex justify-center rounded-lg border border-border bg-card px-4 py-10">
|
||||
<div className="flex max-w-[520px] flex-col items-center ">
|
||||
<div className="flex items-center">
|
||||
<ChainIcon
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ export function ChainOverviewSection(props: { chain: ChainMetadata }) {
|
||||
return (
|
||||
<section>
|
||||
<SectionTitle title="Chain Overview" />
|
||||
<div className="grid grid-cols-1 gap-6 rounded-lg border bg-muted/50 p-4 md:grid-cols-2 md:p-6 lg:grid-cols-3 lg:gap-8">
|
||||
<div className="grid grid-cols-1 gap-6 rounded-lg border bg-card p-4 md:grid-cols-2 md:p-6 lg:grid-cols-3 lg:gap-8">
|
||||
{/* Info */}
|
||||
{chain.infoURL && (
|
||||
<PrimaryInfoItem title="Info">
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ export function ClaimChainSection() {
|
||||
return (
|
||||
<section>
|
||||
<SectionTitle title="Claim Chain" />
|
||||
<div className="rounded-lg border bg-muted/50 p-4">
|
||||
<div className="rounded-lg border bg-card p-4">
|
||||
<p className="mb-1"> This chain page is unclaimed </p>
|
||||
<p className="mb-5 text-muted-foreground text-sm">
|
||||
Are you the owner of this ecosystem? Claim this page to add additional
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ export async function FaucetSection(props: {
|
||||
return (
|
||||
<section>
|
||||
<SectionTitle title="Faucet" />
|
||||
<div className="flex justify-center rounded-lg border border-border bg-muted/50 px-4 py-10">
|
||||
<div className="flex justify-center rounded-lg border border-border bg-card px-4 py-10">
|
||||
<div className="flex w-full max-w-[520px] flex-col items-center ">
|
||||
<div className="flex items-center">
|
||||
<ChainIcon
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ export function SupportedProductsSection(props: {
|
||||
return (
|
||||
<div
|
||||
key={product.id}
|
||||
className="relative flex gap-3 rounded-lg border bg-muted/50 p-4 pr-8 transition-colors duration-300 hover:bg-muted"
|
||||
className="relative flex gap-3 rounded-lg border bg-card p-4 pr-8 transition-colors hover:border-active-border"
|
||||
>
|
||||
<CircleCheckIcon className="absolute top-4 right-4 size-5 text-success-text" />
|
||||
<product.icon className="mt-0.5 size-5 shrink-0" />
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ export function ExplorersSection(props: {
|
||||
return (
|
||||
<div
|
||||
key={explorer.url}
|
||||
className="relative rounded-xl border bg-muted/50 p-4 hover:bg-muted"
|
||||
className="relative rounded-xl border bg-card p-4 transition-colors hover:border-active-border"
|
||||
>
|
||||
<ExternalLinkIcon className="absolute top-4 right-4 size-4 text-muted-foreground" />
|
||||
<h3 className="mb-1 font-semibold text-base capitalize">
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { MobileSidebar } from "@/components/blocks/MobileSidebar";
|
||||
import { Sidebar } from "@/components/blocks/Sidebar";
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
@@ -15,16 +13,12 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
ChevronDownIcon,
|
||||
MenuIcon,
|
||||
TicketCheckIcon,
|
||||
} from "lucide-react";
|
||||
import { ChevronDownIcon, TicketCheckIcon } from "lucide-react";
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { mapV4ChainToV5Chain } from "../../../../../contexts/map-chains";
|
||||
import { getAuthToken } from "../../../../api/lib/getAuthToken";
|
||||
import { StarButton } from "../../components/client/star-button";
|
||||
import { getChain, getChainMetadata } from "../../utils";
|
||||
import { AddChainToWallet } from "./components/client/add-chain-to-wallet";
|
||||
@@ -62,6 +56,7 @@ export default async function ChainPageLayout(props: {
|
||||
const params = await props.params;
|
||||
const { children } = props;
|
||||
const chain = await getChain(params.chain_id);
|
||||
const authToken = await getAuthToken();
|
||||
|
||||
if (params.chain_id !== chain.slug) {
|
||||
redirect(chain.slug);
|
||||
@@ -69,18 +64,6 @@ export default async function ChainPageLayout(props: {
|
||||
|
||||
const chainMetadata = await getChainMetadata(chain.chainId);
|
||||
|
||||
const sidebarLinks = [
|
||||
{
|
||||
href: `/${chain.slug}`,
|
||||
label: "Overview",
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
href: `/${chain.slug}/popular`,
|
||||
label: "Popular Contracts",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-14 border-border border-b pl-7">
|
||||
@@ -123,9 +106,7 @@ export default async function ChainPageLayout(props: {
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
<div className="container flex h-full flex-row">
|
||||
<Sidebar links={sidebarLinks} />
|
||||
|
||||
<div className="container flex h-full max-w-[1180px] flex-row">
|
||||
<div className="flex w-full flex-col pb-10">
|
||||
{/* Icon + Background */}
|
||||
<ChainHeader
|
||||
@@ -134,28 +115,6 @@ export default async function ChainPageLayout(props: {
|
||||
chain={chain}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end lg:hidden">
|
||||
<MobileSidebar
|
||||
links={sidebarLinks}
|
||||
trigger={
|
||||
<Button size="icon" variant="outline">
|
||||
<MenuIcon strokeWidth={1} />
|
||||
</Button>
|
||||
}
|
||||
footer={
|
||||
<div className="mt-5 border-t pt-6 pb-2">
|
||||
<Link
|
||||
href="/chainlist"
|
||||
className="flex items-center justify-start gap-2 px-2 text-muted-foreground text-sm hover:text-foreground"
|
||||
>
|
||||
<ArrowLeftIcon className="size-4" />
|
||||
View all chains
|
||||
</Link>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="h-4 md:h-8" />
|
||||
|
||||
<div className="flex flex-col gap-3 md:gap-2">
|
||||
@@ -173,11 +132,13 @@ export default async function ChainPageLayout(props: {
|
||||
</h1>
|
||||
|
||||
{/* Favorite */}
|
||||
<StarButton
|
||||
chainId={chain.chainId}
|
||||
iconClassName="size-5"
|
||||
className="p-1"
|
||||
/>
|
||||
{authToken && (
|
||||
<StarButton
|
||||
chainId={chain.chainId}
|
||||
iconClassName="size-5"
|
||||
className="p-1"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Gas Sponsored badge - Desktop */}
|
||||
{chainMetadata?.gasSponsored && (
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { fetchTopContracts } from "lib/search";
|
||||
import { ArrowRightIcon, CircleAlertIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { CircleAlertIcon } from "lucide-react";
|
||||
import { getRawAccount } from "../../../../account/settings/getAccount";
|
||||
import { TrendingContractSection } from "../../../trending/components/trending-table";
|
||||
import { getChain, getChainMetadata } from "../../utils";
|
||||
import { BuyFundsSection } from "./components/server/BuyFundsSection";
|
||||
import { ChainOverviewSection } from "./components/server/ChainOverviewSection";
|
||||
@@ -21,14 +17,7 @@ export default async function Page(props: {
|
||||
const chainMetadata = await getChainMetadata(chain.chainId);
|
||||
const isDeprecated = chain.status === "deprecated";
|
||||
|
||||
const [account, topContracts] = await Promise.all([
|
||||
getRawAccount(),
|
||||
fetchTopContracts({
|
||||
chainId: chain.chainId,
|
||||
perPage: 3,
|
||||
timeRange: "month",
|
||||
}),
|
||||
]);
|
||||
const account = await getRawAccount();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-10">
|
||||
@@ -65,31 +54,6 @@ export default async function Page(props: {
|
||||
<ExplorersSection explorers={chain.explorers} />
|
||||
)}
|
||||
|
||||
{topContracts.length > 0 && (
|
||||
<section>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h2 className="font-semibold text-xl tracking-tight">
|
||||
Popular Contracts
|
||||
</h2>
|
||||
<Button asChild variant="outline">
|
||||
<Link
|
||||
href={`/${chain.slug}/popular`}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
See All
|
||||
<ArrowRightIcon className="size-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<TrendingContractSection
|
||||
topContracts={topContracts}
|
||||
chainId={chain.chainId}
|
||||
showPagination={false}
|
||||
searchParams={undefined}
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{chain.services.filter((s) => s.enabled).length > 0 && (
|
||||
<SupportedProductsSection services={chain.services} />
|
||||
)}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import { type SortBy, fetchTopContracts } from "lib/search";
|
||||
import { CircleAlertIcon } from "lucide-react";
|
||||
import { TrendingContractSection } from "../../../../trending/components/trending-table";
|
||||
import { getChain } from "../../../utils";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{ chain_id: string }>;
|
||||
searchParams: Promise<{ page?: number; sortBy?: SortBy }>;
|
||||
}) {
|
||||
const searchParams = await props.searchParams;
|
||||
const params = await props.params;
|
||||
const chain = await getChain(params.chain_id);
|
||||
const topContracts = await fetchTopContracts({
|
||||
chainId: chain.chainId,
|
||||
page: searchParams.page,
|
||||
sortBy: searchParams.sortBy,
|
||||
perPage: 15,
|
||||
timeRange: "month",
|
||||
});
|
||||
|
||||
return (
|
||||
<section className="mt-2">
|
||||
<h2 className="mb-2 font-semibold text-2xl tracking-tighter">
|
||||
Popular Contracts
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Explore contracts on Ethereum and sort them by your preferred metrics
|
||||
</p>
|
||||
<div className="h-8" />
|
||||
{topContracts.length > 0 && (
|
||||
<TrendingContractSection
|
||||
topContracts={topContracts}
|
||||
chainId={chain.chainId}
|
||||
searchParams={searchParams}
|
||||
showPagination={true}
|
||||
/>
|
||||
)}
|
||||
{topContracts.length === 0 && (
|
||||
<div className="flex h-[200px] items-center justify-center rounded-lg border border-border text-lg text-muted-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<CircleAlertIcon className="size-5 text-destructive-text" />
|
||||
No contracts found
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -247,7 +247,7 @@ export const MarketplaceTable: React.FC<MarketplaceTableProps> = ({
|
||||
<Tr
|
||||
{...row.getRowProps()}
|
||||
role="group"
|
||||
className="hover:bg-muted/50"
|
||||
className="hover:bg-card"
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => setTokenRow(row.original)}
|
||||
borderBottomWidth={1}
|
||||
|
||||
+5
-1
@@ -4,6 +4,7 @@ import {
|
||||
toTokens,
|
||||
toUnits,
|
||||
} from "thirdweb";
|
||||
import { getContract } from "thirdweb/contract";
|
||||
import type { OverrideEntry } from "thirdweb/dist/types/utils/extensions/drops/types";
|
||||
import type { Prettify } from "thirdweb/dist/types/utils/type-utils";
|
||||
import { getContractMetadata } from "thirdweb/extensions/common";
|
||||
@@ -71,7 +72,10 @@ export async function getClaimPhasesInLegacyFormat(
|
||||
conditions.map(async (condition) => {
|
||||
const [currencyMetadata, metadata, contractMetadata] = await Promise.all([
|
||||
await ERC20Ext.getCurrencyMetadata({
|
||||
contract: { ...options.contract, address: condition.currency },
|
||||
contract: getContract({
|
||||
...options.contract,
|
||||
address: condition.currency,
|
||||
}),
|
||||
}).then((m) => ({
|
||||
...m,
|
||||
displayValue: toTokens(condition.pricePerToken, m.decimals),
|
||||
|
||||
+6
-6
@@ -160,7 +160,7 @@ const SnapshotViewerSheetContent: React.FC<SnapshotUploadProps> = ({
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full cursor-pointer rounded-md border border-border hover:border-primary",
|
||||
noCsv ? "bg-red-200" : "bg-card",
|
||||
noCsv ? "bg-red-200" : "bg-background",
|
||||
)}
|
||||
{...getRootProps()}
|
||||
>
|
||||
@@ -197,7 +197,7 @@ const SnapshotViewerSheetContent: React.FC<SnapshotUploadProps> = ({
|
||||
<a
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
download
|
||||
href="/snapshot-with-maxclaimable.csv"
|
||||
href="/assets/examples/snapshot-with-maxclaimable.csv"
|
||||
>
|
||||
<DownloadIcon className="inline size-3" /> Example
|
||||
snapshot
|
||||
@@ -212,7 +212,7 @@ const SnapshotViewerSheetContent: React.FC<SnapshotUploadProps> = ({
|
||||
<a
|
||||
download
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
href="/snapshot-with-overrides.csv"
|
||||
href="/assets/examples/snapshot-with-overrides.csv"
|
||||
>
|
||||
<DownloadIcon className="inline size-3" /> Example
|
||||
snapshot
|
||||
@@ -228,7 +228,7 @@ const SnapshotViewerSheetContent: React.FC<SnapshotUploadProps> = ({
|
||||
<a
|
||||
download
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
href="/snapshot.csv"
|
||||
href="/assets/examples/snapshot.csv"
|
||||
>
|
||||
<DownloadIcon className="inline size-3" /> Example
|
||||
snapshot
|
||||
@@ -243,7 +243,7 @@ const SnapshotViewerSheetContent: React.FC<SnapshotUploadProps> = ({
|
||||
<a
|
||||
download
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
href="/snapshot-with-maxclaimable.csv"
|
||||
href="/assets/examples/snapshot-with-maxclaimable.csv"
|
||||
>
|
||||
<DownloadIcon className="inline size-3" /> Example
|
||||
snapshot
|
||||
@@ -262,7 +262,7 @@ const SnapshotViewerSheetContent: React.FC<SnapshotUploadProps> = ({
|
||||
<a
|
||||
download
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
href="/snapshot-with-overrides.csv"
|
||||
href="/assets/examples/snapshot-with-overrides.csv"
|
||||
>
|
||||
<DownloadIcon className="inline size-3" /> Example
|
||||
snapshot
|
||||
|
||||
+3
-3
@@ -83,7 +83,7 @@ export const MetadataHeader: React.FC<MetadataHeaderProps> = ({
|
||||
{chain && (
|
||||
<Link
|
||||
href={`/${chain.slug}`}
|
||||
className="flex w-fit shrink-0 items-center gap-2 rounded-3xl border border-border bg-muted/50 px-2.5 py-1.5 hover:bg-muted"
|
||||
className="flex w-fit shrink-0 items-center gap-2 rounded-3xl border border-border bg-card px-2.5 py-1.5 hover:bg-accent"
|
||||
>
|
||||
<ChainIcon ipfsSrc={chain.icon?.url} className="size-4" />
|
||||
{cleanedChainName && (
|
||||
@@ -109,7 +109,7 @@ export const MetadataHeader: React.FC<MetadataHeaderProps> = ({
|
||||
<CopyAddressButton
|
||||
address={address}
|
||||
copyIconPosition="left"
|
||||
className="bg-muted/50 text-xs"
|
||||
className="bg-card text-xs"
|
||||
variant="outline"
|
||||
/>
|
||||
|
||||
@@ -138,7 +138,7 @@ function BadgeLink(props: {
|
||||
<Button
|
||||
variant="outline"
|
||||
asChild
|
||||
className="!h-auto gap-2 rounded-lg bg-muted/50 px-3 py-1.5 text-xs capitalize"
|
||||
className="!h-auto gap-2 rounded-lg bg-card px-3 py-1.5 text-xs capitalize"
|
||||
>
|
||||
<Link href={props.href} target="_blank">
|
||||
{props.name}
|
||||
|
||||
+14
-16
@@ -1,35 +1,33 @@
|
||||
import { isProd } from "@/constants/env";
|
||||
import { INSIGHT_SERVICE_API_KEY } from "@/constants/env";
|
||||
import { getVercelEnv } from "lib/vercel-utils";
|
||||
|
||||
const thirdwebDomain =
|
||||
getVercelEnv() !== "production" ? "thirdweb-dev" : "thirdweb";
|
||||
|
||||
export async function isAnalyticsSupportedForChain(
|
||||
chainId: number,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
if (!process.env.CHAINSAW_API_KEY) {
|
||||
throw new Error("Missing CHAINSAW_API_KEY env var");
|
||||
}
|
||||
|
||||
const res = await fetch(
|
||||
`https://chainsaw.${isProd ? "thirdweb" : "thirdweb-dev"}.com/service/chains/${chainId}`,
|
||||
`https://insight.${thirdwebDomain}.com/service/chains/${chainId}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
// pass the shared secret
|
||||
"x-service-api-key": process.env.CHAINSAW_API_KEY || "",
|
||||
// service api key required - because this is endpoint is internal
|
||||
"x-service-api-key": INSIGHT_SERVICE_API_KEY,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
// assume not supported if we get a non-200 response
|
||||
return false;
|
||||
}
|
||||
|
||||
const { data } = await res.json();
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.error("Error checking if analytics is supported for chain", e);
|
||||
}
|
||||
const json = (await res.json()) as { data: boolean };
|
||||
|
||||
return json.data;
|
||||
} catch (e) {
|
||||
console.error(`Error checking analytics support for chain ${chainId}`);
|
||||
console.error(e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
+9
-1
@@ -8,6 +8,8 @@ import { ContractAnalyticsPage } from "./ContractAnalyticsPage";
|
||||
|
||||
export function ContractAnalyticsPageClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
writeFnSelectorToNameRecord: Record<string, string>;
|
||||
eventSelectorToNameRecord: Record<string, string>;
|
||||
}) {
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
|
||||
@@ -23,5 +25,11 @@ export function ContractAnalyticsPageClient(props: {
|
||||
return <RedirectToContractOverview contract={props.contract} />;
|
||||
}
|
||||
|
||||
return <ContractAnalyticsPage contract={props.contract} />;
|
||||
return (
|
||||
<ContractAnalyticsPage
|
||||
contract={props.contract}
|
||||
writeFnSelectorToNameRecord={props.writeFnSelectorToNameRecord}
|
||||
eventSelectorToNameRecord={props.eventSelectorToNameRecord}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+166
-132
@@ -1,138 +1,117 @@
|
||||
"use client";
|
||||
import { ThirdwebBarChart } from "@/components/blocks/charts/bar-chart";
|
||||
import { useIsomorphicLayoutEffect } from "@/lib/useIsomorphicLayoutEffect";
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertIcon,
|
||||
AlertTitle,
|
||||
Flex,
|
||||
SimpleGrid,
|
||||
Skeleton,
|
||||
Stat,
|
||||
StatLabel,
|
||||
StatNumber,
|
||||
} from "@chakra-ui/react";
|
||||
import { Skeleton, Stat, StatLabel, StatNumber } from "@chakra-ui/react";
|
||||
import type { UseQueryResult } from "@tanstack/react-query";
|
||||
import {
|
||||
type AnalyticsQueryParams,
|
||||
type TotalQueryResult,
|
||||
useEventsAnalytics,
|
||||
useFunctionsAnalytics,
|
||||
useLogsAnalytics,
|
||||
useTotalLogsAnalytics,
|
||||
useTotalTransactionAnalytics,
|
||||
useTotalWalletsAnalytics,
|
||||
useTransactionAnalytics,
|
||||
useUniqueWalletsAnalytics,
|
||||
useContractEventAnalytics,
|
||||
useContractEventBreakdown,
|
||||
useContractFunctionBreakdown,
|
||||
useContractTransactionAnalytics,
|
||||
useContractUniqueWalletAnalytics,
|
||||
useTotalContractEvents,
|
||||
useTotalContractTransactionAnalytics,
|
||||
useTotalContractUniqueWallets,
|
||||
} from "data/analytics/hooks";
|
||||
import { Suspense, useMemo, useState } from "react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { Card, Heading } from "tw-components";
|
||||
import { Card } from "tw-components";
|
||||
import {
|
||||
DateRangeSelector,
|
||||
type Range,
|
||||
getLastNDaysRange,
|
||||
} from "../../../../../../components/analytics/date-range-selector";
|
||||
|
||||
interface ContractAnalyticsPageProps {
|
||||
contract: ThirdwebContract;
|
||||
writeFnSelectorToNameRecord: Record<string, string>;
|
||||
eventSelectorToNameRecord: Record<string, string>;
|
||||
}
|
||||
|
||||
export const ContractAnalyticsPage: React.FC<ContractAnalyticsPageProps> = ({
|
||||
contract,
|
||||
writeFnSelectorToNameRecord,
|
||||
eventSelectorToNameRecord,
|
||||
}) => {
|
||||
const [startDate] = useState(
|
||||
(() => {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - 30);
|
||||
return date;
|
||||
})(),
|
||||
);
|
||||
const [endDate] = useState(new Date());
|
||||
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
window?.scrollTo({ top: 0, behavior: "smooth" });
|
||||
}, []);
|
||||
const [range, setRange] = useState<Range>(() => getLastNDaysRange("last-30"));
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap={6}>
|
||||
{contract && (
|
||||
<>
|
||||
<Flex gap={10} direction="column">
|
||||
<Flex direction="column" gap={2}>
|
||||
<Alert status="info" borderRadius="md" mb={4}>
|
||||
<AlertIcon />
|
||||
<AlertTitle>Analytics is in beta.</AlertTitle>
|
||||
<AlertDescription>
|
||||
Some data may be partially inaccurate or incomplete.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<Heading as="h2" size="title.md">
|
||||
Analytics
|
||||
</Heading>
|
||||
<Flex gap={4}>
|
||||
<AnalyticsStat
|
||||
chainId={contract.chain.id}
|
||||
contractAddress={contract.address}
|
||||
// FIXME
|
||||
// eslint-disable-next-line react-compiler/react-compiler
|
||||
useTotal={useTotalWalletsAnalytics}
|
||||
label="Unique Wallets"
|
||||
/>
|
||||
<AnalyticsStat
|
||||
chainId={contract.chain.id}
|
||||
contractAddress={contract.address}
|
||||
// FIXME
|
||||
// eslint-disable-next-line react-compiler/react-compiler
|
||||
useTotal={useTotalTransactionAnalytics}
|
||||
label="Total Transactions"
|
||||
/>
|
||||
<AnalyticsStat
|
||||
chainId={contract.chain.id}
|
||||
contractAddress={contract.address}
|
||||
// FIXME
|
||||
// eslint-disable-next-line react-compiler/react-compiler
|
||||
useTotal={useTotalLogsAnalytics}
|
||||
label="Total Events"
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<SimpleGrid columns={{ base: 1, md: 1 }} gap={4}>
|
||||
<UniqueWalletsChart
|
||||
chainId={contract.chain.id}
|
||||
contractAddress={contract.address}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
/>
|
||||
<div>
|
||||
<h2 className="mb-3 font-semibold text-xl tracking-tight lg:text-2xl">
|
||||
Analytics
|
||||
</h2>
|
||||
|
||||
<TotalTransactionsChart
|
||||
chainId={contract.chain.id}
|
||||
contractAddress={contract.address}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
/>
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
|
||||
<AnalyticsStat
|
||||
chainId={contract.chain.id}
|
||||
contractAddress={contract.address}
|
||||
// FIXME
|
||||
// eslint-disable-next-line react-compiler/react-compiler
|
||||
useTotal={useTotalContractUniqueWallets}
|
||||
label="Unique Wallets"
|
||||
/>
|
||||
<AnalyticsStat
|
||||
chainId={contract.chain.id}
|
||||
contractAddress={contract.address}
|
||||
// FIXME
|
||||
// eslint-disable-next-line react-compiler/react-compiler
|
||||
useTotal={useTotalContractTransactionAnalytics}
|
||||
label="Total Transactions"
|
||||
/>
|
||||
<AnalyticsStat
|
||||
chainId={contract.chain.id}
|
||||
contractAddress={contract.address}
|
||||
// FIXME
|
||||
// eslint-disable-next-line react-compiler/react-compiler
|
||||
useTotal={useTotalContractEvents}
|
||||
label="Total Events"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TotalEventsChart
|
||||
chainId={contract.chain.id}
|
||||
contractAddress={contract.address}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
/>
|
||||
<div className="mt-8 mb-4">
|
||||
<DateRangeSelector range={range} setRange={setRange} />
|
||||
</div>
|
||||
|
||||
<FunctionBreakdownChart
|
||||
chainId={contract.chain.id}
|
||||
contractAddress={contract.address}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
/>
|
||||
<div className="flex flex-col gap-6">
|
||||
<UniqueWalletsChart
|
||||
chainId={contract.chain.id}
|
||||
contractAddress={contract.address}
|
||||
startDate={range.from}
|
||||
endDate={range.to}
|
||||
/>
|
||||
|
||||
<EventBreakdownChart
|
||||
chainId={contract.chain.id}
|
||||
contractAddress={contract.address}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
<TotalTransactionsChart
|
||||
chainId={contract.chain.id}
|
||||
contractAddress={contract.address}
|
||||
startDate={range.from}
|
||||
endDate={range.to}
|
||||
/>
|
||||
|
||||
<TotalEventsChart
|
||||
chainId={contract.chain.id}
|
||||
contractAddress={contract.address}
|
||||
startDate={range.from}
|
||||
endDate={range.to}
|
||||
/>
|
||||
|
||||
<FunctionBreakdownChart
|
||||
chainId={contract.chain.id}
|
||||
contractAddress={contract.address}
|
||||
startDate={range.from}
|
||||
endDate={range.to}
|
||||
writeFnSelectorToNameRecord={writeFnSelectorToNameRecord}
|
||||
/>
|
||||
|
||||
<EventBreakdownChart
|
||||
chainId={contract.chain.id}
|
||||
contractAddress={contract.address}
|
||||
startDate={range.from}
|
||||
endDate={range.to}
|
||||
eventSelectorToNameRecord={eventSelectorToNameRecord}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -143,26 +122,27 @@ type ChartProps = {
|
||||
endDate: Date;
|
||||
};
|
||||
function UniqueWalletsChart(props: ChartProps) {
|
||||
const { data } = useUniqueWalletsAnalytics(props);
|
||||
const analyticsQuery = useContractUniqueWalletAnalytics(props);
|
||||
|
||||
return (
|
||||
<ThirdwebBarChart
|
||||
title="Unique Wallets"
|
||||
description="The number of unique wallet addresses that have sent a transaction to this contract."
|
||||
data={data || []}
|
||||
data={analyticsQuery.data || []}
|
||||
isPending={analyticsQuery.isPending}
|
||||
config={{
|
||||
wallets: {
|
||||
count: {
|
||||
label: "Unique Wallets",
|
||||
color: "hsl(var(--chart-1))",
|
||||
},
|
||||
}}
|
||||
chartClassName="aspect[2] lg:aspect-[4.5]"
|
||||
chartClassName="aspect-[1.5] lg:aspect-[4.5]"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TotalTransactionsChart(props: ChartProps) {
|
||||
const { data } = useTransactionAnalytics({
|
||||
const analyticsQuery = useContractTransactionAnalytics({
|
||||
chainId: props.chainId,
|
||||
contractAddress: props.contractAddress,
|
||||
endDate: props.endDate,
|
||||
@@ -173,46 +153,74 @@ function TotalTransactionsChart(props: ChartProps) {
|
||||
<ThirdwebBarChart
|
||||
title="Total Transactions"
|
||||
description="The number of transactions that have been sent to this contract."
|
||||
data={data || []}
|
||||
data={analyticsQuery.data || []}
|
||||
isPending={analyticsQuery.isPending}
|
||||
config={{
|
||||
count: {
|
||||
label: "Transactions",
|
||||
color: "hsl(var(--chart-1))",
|
||||
},
|
||||
}}
|
||||
chartClassName="aspect[2] lg:aspect-[4.5]"
|
||||
chartClassName="aspect-[1.5] lg:aspect-[4.5]"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TotalEventsChart(props: ChartProps) {
|
||||
const { data } = useLogsAnalytics(props);
|
||||
const analyticsQuery = useContractEventAnalytics(props);
|
||||
|
||||
return (
|
||||
<ThirdwebBarChart
|
||||
title="Total Events"
|
||||
description="The number of on-chain events that have been emitted from this contract."
|
||||
data={data || []}
|
||||
data={analyticsQuery.data || []}
|
||||
isPending={analyticsQuery.isPending}
|
||||
config={{
|
||||
count: {
|
||||
label: "Events",
|
||||
color: "hsl(var(--chart-1))",
|
||||
},
|
||||
}}
|
||||
chartClassName="aspect[2] lg:aspect-[4.5]"
|
||||
chartClassName="aspect-[1.5] lg:aspect-[4.5]"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function FunctionBreakdownChart(props: ChartProps) {
|
||||
const { data } = useFunctionsAnalytics(props);
|
||||
function FunctionBreakdownChart(
|
||||
props: ChartProps & {
|
||||
writeFnSelectorToNameRecord: Record<string, string>;
|
||||
},
|
||||
) {
|
||||
const analyticsQuery = useContractFunctionBreakdown(props);
|
||||
|
||||
// replace function selector with function name
|
||||
const mappedQueryData = useMemo(() => {
|
||||
return analyticsQuery.data?.map((item) => {
|
||||
const modifiedItem = { time: item.time } as typeof item;
|
||||
|
||||
for (const key in item) {
|
||||
if (key === "time") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const name = props.writeFnSelectorToNameRecord[key];
|
||||
const value = item[key];
|
||||
if (name && value !== undefined) {
|
||||
modifiedItem[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return modifiedItem;
|
||||
});
|
||||
}, [analyticsQuery.data, props.writeFnSelectorToNameRecord]);
|
||||
|
||||
return (
|
||||
<ThirdwebBarChart
|
||||
title="Function Breakdown"
|
||||
description="The breakdown of calls to each write function from transactions."
|
||||
data={data || []}
|
||||
config={Object.keys(data?.[0] || {}).reduce(
|
||||
data={mappedQueryData || []}
|
||||
isPending={analyticsQuery.isPending}
|
||||
config={Object.keys(mappedQueryData?.[0] || {}).reduce(
|
||||
(acc, key) => {
|
||||
if (key === "time") {
|
||||
return acc;
|
||||
@@ -225,21 +233,47 @@ function FunctionBreakdownChart(props: ChartProps) {
|
||||
},
|
||||
{} as Record<string, { label: string; color: string }>,
|
||||
)}
|
||||
chartClassName="aspect[2] lg:aspect-[4.5]"
|
||||
chartClassName="aspect-[1.5] lg:aspect-[4.5]"
|
||||
showLegend
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function EventBreakdownChart(props: ChartProps) {
|
||||
const { data } = useEventsAnalytics(props);
|
||||
function EventBreakdownChart(
|
||||
props: ChartProps & {
|
||||
eventSelectorToNameRecord: Record<string, string>;
|
||||
},
|
||||
) {
|
||||
const analyticsQuery = useContractEventBreakdown(props);
|
||||
|
||||
// replace event selector with event name
|
||||
const mappedQueryData = useMemo(() => {
|
||||
return analyticsQuery.data?.map((item) => {
|
||||
const modifiedItem = { time: item.time } as typeof item;
|
||||
|
||||
for (const key in item) {
|
||||
if (key === "time") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const name = props.eventSelectorToNameRecord[key];
|
||||
const value = item[key];
|
||||
if (name && value !== undefined) {
|
||||
modifiedItem[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return modifiedItem;
|
||||
});
|
||||
}, [analyticsQuery.data, props.eventSelectorToNameRecord]);
|
||||
|
||||
return (
|
||||
<ThirdwebBarChart
|
||||
title="Event Breakdown"
|
||||
description="The breakdown of events emitted by this contract."
|
||||
data={data || []}
|
||||
config={Object.keys(data?.[0] || {}).reduce(
|
||||
data={mappedQueryData || []}
|
||||
isPending={analyticsQuery.isPending}
|
||||
config={Object.keys(mappedQueryData?.[0] || {}).reduce(
|
||||
(acc, key) => {
|
||||
if (key === "time") {
|
||||
return acc;
|
||||
@@ -252,7 +286,7 @@ function EventBreakdownChart(props: ChartProps) {
|
||||
},
|
||||
{} as Record<string, { label: string; color: string }>,
|
||||
)}
|
||||
chartClassName="aspect[2] lg:aspect-[4.5]"
|
||||
chartClassName="aspect-[1.5] lg:aspect-[4.5]"
|
||||
showLegend
|
||||
/>
|
||||
);
|
||||
|
||||
+51
-2
@@ -1,5 +1,7 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { localhost } from "thirdweb/chains";
|
||||
import { type ThirdwebContract, resolveContractAbi } from "thirdweb/contract";
|
||||
import { type Abi, toEventSelector, toFunctionSelector } from "thirdweb/utils";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { ContractAnalyticsPage } from "./ContractAnalyticsPage";
|
||||
@@ -18,8 +20,18 @@ export default async function Page(props: {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { eventSelectorToName, writeFnSelectorToName } = await getSelectors(
|
||||
info.contract,
|
||||
);
|
||||
|
||||
if (info.chainMetadata.chainId === localhost.id) {
|
||||
return <ContractAnalyticsPageClient contract={info.contract} />;
|
||||
return (
|
||||
<ContractAnalyticsPageClient
|
||||
contract={info.contract}
|
||||
writeFnSelectorToNameRecord={writeFnSelectorToName}
|
||||
eventSelectorToNameRecord={eventSelectorToName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { isAnalyticsSupported } = await getContractPageMetadata(info.contract);
|
||||
@@ -28,5 +40,42 @@ export default async function Page(props: {
|
||||
redirect(`/${params.chain_id}/${params.contractAddress}`);
|
||||
}
|
||||
|
||||
return <ContractAnalyticsPage contract={info.contract} />;
|
||||
return (
|
||||
<ContractAnalyticsPage
|
||||
contract={info.contract}
|
||||
writeFnSelectorToNameRecord={writeFnSelectorToName}
|
||||
eventSelectorToNameRecord={eventSelectorToName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
async function getSelectors(contract: ThirdwebContract) {
|
||||
try {
|
||||
const abi = await resolveContractAbi<Abi>(contract);
|
||||
const writeFnSelectorToName: Record<string, string> = {};
|
||||
const eventSelectorToName: Record<string, string> = {};
|
||||
|
||||
for (const item of abi) {
|
||||
if (item.type === "event") {
|
||||
eventSelectorToName[toEventSelector(item)] = item.name;
|
||||
} else if (
|
||||
// if write function
|
||||
item.type === "function" &&
|
||||
item.stateMutability !== "view" &&
|
||||
item.stateMutability !== "pure"
|
||||
) {
|
||||
writeFnSelectorToName[toFunctionSelector(item)] = item.name;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
writeFnSelectorToName,
|
||||
eventSelectorToName,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
writeFnSelectorToName: {},
|
||||
eventSelectorToName: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+244
-213
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CodeClient } from "@/components/ui/code/code.client";
|
||||
import {
|
||||
@@ -11,8 +12,8 @@ import { Flex, FormControl, Input, Select } from "@chakra-ui/react";
|
||||
import { LazyCreateAPIKeyDialog } from "components/settings/ApiKeys/Create/LazyCreateAPIKeyDialog";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useAllChainsData } from "hooks/chains/allChains";
|
||||
import { useClipboard } from "hooks/useClipboard";
|
||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import { AlertCircleIcon } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useMemo, useState } from "react";
|
||||
@@ -211,6 +212,7 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
|
||||
twAccount,
|
||||
}) => {
|
||||
const trackEvent = useTrack();
|
||||
const { theme } = useTheme();
|
||||
|
||||
const apiKeys = useApiKeys({
|
||||
isLoggedIn: !!twAccount,
|
||||
@@ -268,7 +270,7 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
|
||||
listingId: "0",
|
||||
directListingId: "0",
|
||||
englishAuctionId: "0",
|
||||
theme: "dark",
|
||||
theme: theme === "light" ? "light" : "dark",
|
||||
primaryColor: "purple",
|
||||
secondaryColor: "orange",
|
||||
},
|
||||
@@ -298,13 +300,12 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
|
||||
[iframeSrc],
|
||||
);
|
||||
|
||||
const { hasCopied, onCopy } = useClipboard(embedCode, 3000);
|
||||
const [showCreateAPIKeyModal, setShowCreateAPIKeyModal] = useState(false);
|
||||
const activeAccount = useActiveAccount();
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<Flex gap={8} direction="column">
|
||||
<div>
|
||||
<LazyCreateAPIKeyDialog
|
||||
prefill={{
|
||||
name: "Embed API Key",
|
||||
@@ -322,206 +323,223 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
|
||||
apiKeys.refetch();
|
||||
}}
|
||||
enableNebulaServiceByDefault={false}
|
||||
teamSlug={undefined}
|
||||
/>
|
||||
|
||||
<Flex gap={8} direction={{ base: "column", md: "row" }}>
|
||||
<Card className="flex w-full flex-col gap-5 md:w-1/2">
|
||||
<Heading size="title.sm">Configuration</Heading>
|
||||
<Alert variant="warning">
|
||||
<AlertCircleIcon className="size-5" />
|
||||
<AlertTitle>Deprecated</AlertTitle>
|
||||
<AlertDescription className="leading-relaxed">
|
||||
<span>
|
||||
thirdweb NFT Embeds are deprecated and not actively maintained
|
||||
</span>{" "}
|
||||
<br />
|
||||
<span>
|
||||
Use the{" "}
|
||||
<Link
|
||||
href="https://github.com/thirdweb-example/nft-minting-template"
|
||||
target="_blank"
|
||||
className="underline underline-offset-2 hover:text-foreground"
|
||||
>
|
||||
{" "}
|
||||
NFT Minting Template
|
||||
</Link>{" "}
|
||||
or build a custom NFT minting app using{" "}
|
||||
<Link
|
||||
href="https://portal.thirdweb.com/react/v5"
|
||||
className="underline underline-offset-2 hover:text-foreground"
|
||||
>
|
||||
thirdweb React SDK components
|
||||
</Link>{" "}
|
||||
instead
|
||||
</span>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="h-8" />
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
<div>
|
||||
<h2 className="mb-3 font-semibold text-xl tracking-tight">
|
||||
Configuration
|
||||
</h2>
|
||||
|
||||
<Card className="flex w-full flex-col gap-5 bg-card">
|
||||
{ercOrMarketplace === "marketplace" ? (
|
||||
<FormControl>
|
||||
<FormLabel>Listing ID</FormLabel>
|
||||
<Input type="number" {...register("listingId")} />
|
||||
<FormHelperText>
|
||||
The listing ID the embed should display
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
) : null}
|
||||
|
||||
{ercOrMarketplace === "marketplace-v3" ? (
|
||||
<FormControl>
|
||||
<FormLabel>Listing type</FormLabel>
|
||||
<Select {...register("listingType")}>
|
||||
<option value="direct-listing">Direct Listing</option>
|
||||
<option value="english-auction">English Auction</option>
|
||||
</Select>
|
||||
<FormHelperText>
|
||||
The type of listing the embed should display
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
) : null}
|
||||
|
||||
{ercOrMarketplace === "marketplace-v3" &&
|
||||
watch("listingType") === "direct-listing" ? (
|
||||
<FormControl>
|
||||
<FormLabel>Direct Listing ID</FormLabel>
|
||||
<Input type="number" {...register("directListingId")} />
|
||||
<FormHelperText>
|
||||
The direct listing ID the embed should display
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
) : null}
|
||||
|
||||
{ercOrMarketplace === "marketplace-v3" &&
|
||||
watch("listingType") === "english-auction" ? (
|
||||
<FormControl>
|
||||
<FormLabel>English Auction ID</FormLabel>
|
||||
<Input type="number" {...register("englishAuctionId")} />
|
||||
<FormHelperText>
|
||||
The english auction ID the embed should display
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
) : null}
|
||||
|
||||
{ercOrMarketplace === "erc1155" ? (
|
||||
<FormControl>
|
||||
<FormLabel>Token ID</FormLabel>
|
||||
<Input type="number" {...register("tokenId")} />
|
||||
<FormHelperText>
|
||||
The token ID the embed should display
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
) : null}
|
||||
|
||||
{ercOrMarketplace === "marketplace" ? (
|
||||
<FormControl>
|
||||
<FormLabel>Listing ID</FormLabel>
|
||||
<Input type="number" {...register("listingId")} />
|
||||
<FormHelperText>
|
||||
The listing ID the embed should display
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
) : null}
|
||||
<FormLabel>Client ID</FormLabel>
|
||||
{!activeAccount ? (
|
||||
<Button asChild className="w-full">
|
||||
<Link
|
||||
href={`/login${
|
||||
pathname ? `?next=${encodeURIComponent(pathname)}` : ""
|
||||
}`}
|
||||
>
|
||||
Sign in to create a client ID
|
||||
</Link>
|
||||
</Button>
|
||||
) : validApiKey ? (
|
||||
<Input
|
||||
readOnly
|
||||
disabled
|
||||
value={`${validApiKey?.name} - ${validApiKey?.key}`}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
trackEvent({
|
||||
category: "api-keys",
|
||||
action: "create",
|
||||
label: "attempt",
|
||||
fromEmbed: true,
|
||||
});
|
||||
|
||||
{ercOrMarketplace === "marketplace-v3" ? (
|
||||
<FormControl>
|
||||
<FormLabel>Listing type</FormLabel>
|
||||
<Select {...register("listingType")}>
|
||||
<option value="direct-listing">Direct Listing</option>
|
||||
<option value="english-auction">English Auction</option>
|
||||
</Select>
|
||||
<FormHelperText>
|
||||
The type of listing the embed should display
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
) : null}
|
||||
|
||||
{ercOrMarketplace === "marketplace-v3" &&
|
||||
watch("listingType") === "direct-listing" ? (
|
||||
<FormControl>
|
||||
<FormLabel>Direct Listing ID</FormLabel>
|
||||
<Input type="number" {...register("directListingId")} />
|
||||
<FormHelperText>
|
||||
The direct listing ID the embed should display
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
) : null}
|
||||
|
||||
{ercOrMarketplace === "marketplace-v3" &&
|
||||
watch("listingType") === "english-auction" ? (
|
||||
<FormControl>
|
||||
<FormLabel>English Auction ID</FormLabel>
|
||||
<Input type="number" {...register("englishAuctionId")} />
|
||||
<FormHelperText>
|
||||
The english auction ID the embed should display
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
) : null}
|
||||
|
||||
{ercOrMarketplace === "erc1155" ? (
|
||||
<FormControl>
|
||||
<FormLabel>Token ID</FormLabel>
|
||||
<Input type="number" {...register("tokenId")} />
|
||||
<FormHelperText>
|
||||
The token ID the embed should display
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
) : null}
|
||||
|
||||
<FormControl>
|
||||
<FormLabel>Client ID</FormLabel>
|
||||
{!activeAccount ? (
|
||||
<Button asChild className="w-full">
|
||||
<Link
|
||||
href={`/login${
|
||||
pathname ? `?next=${encodeURIComponent(pathname)}` : ""
|
||||
}`}
|
||||
setShowCreateAPIKeyModal(true);
|
||||
}}
|
||||
disabled={createKeyMutation.isPending}
|
||||
>
|
||||
Sign in to create a client ID
|
||||
</Link>
|
||||
</Button>
|
||||
) : validApiKey ? (
|
||||
<Input
|
||||
readOnly
|
||||
disabled
|
||||
value={`${validApiKey?.name} - ${validApiKey?.key}`}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
trackEvent({
|
||||
category: "api-keys",
|
||||
action: "create",
|
||||
label: "attempt",
|
||||
fromEmbed: true,
|
||||
});
|
||||
|
||||
setShowCreateAPIKeyModal(true);
|
||||
}}
|
||||
disabled={createKeyMutation.isPending}
|
||||
>
|
||||
Create Client ID
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<FormHelperText>
|
||||
You need a client ID to use embeds.{" "}
|
||||
<Link
|
||||
href="https://portal.thirdweb.com/account/api-keys"
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
</Link>
|
||||
.
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<FormLabel>RPC Url</FormLabel>
|
||||
<Input type="url" {...register("rpcUrl")} />
|
||||
<FormHelperText>
|
||||
RPC the embed should use to connect to the blockchain.
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
{ercOrMarketplace === "marketplace" ||
|
||||
ercOrMarketplace === "marketplace-v3" ? null : (
|
||||
<FormControl gap={4}>
|
||||
<Heading size="title.sm" my={4}>
|
||||
Gasless
|
||||
</Heading>
|
||||
<Select {...register("gasless")} mb={4}>
|
||||
<option value="false">Disabled</option>
|
||||
<option value="openZeppelin">OpenZeppelin Relayer</option>
|
||||
<option value="biconomy">Biconomy Relayer</option>
|
||||
</Select>
|
||||
{watch("gasless") === "openZeppelin" && (
|
||||
<FormControl>
|
||||
<FormLabel>OpenZeppelin Relayer URL</FormLabel>
|
||||
<Input type="url" {...register("relayUrl")} />
|
||||
</FormControl>
|
||||
)}
|
||||
{watch("gasless") === "biconomy" && (
|
||||
<Flex gap={4} flexDir="column">
|
||||
<FormControl>
|
||||
<FormLabel>Biconomy API key</FormLabel>
|
||||
<Input type="url" {...register("biconomyApiKey")} />
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel>Biconomy API ID</FormLabel>
|
||||
<Input type="url" {...register("biconomyApiId")} />
|
||||
</FormControl>
|
||||
</Flex>
|
||||
Create Client ID
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<FormHelperText>
|
||||
A relayer can be used to make the transaction gasless for the
|
||||
end user.{" "}
|
||||
You need a client ID to use embeds.{" "}
|
||||
<Link
|
||||
target="_blank"
|
||||
href="https://portal.thirdweb.com/account/api-keys"
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
href="https://blog.thirdweb.com/guides/setup-gasless-transactions"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
</Link>
|
||||
.
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||
<FormControl>
|
||||
<Heading size="title.sm" my={4}>
|
||||
Customization
|
||||
</Heading>
|
||||
<FormLabel>Theme</FormLabel>
|
||||
<Select {...register("theme")}>
|
||||
<option value="light">Light</option>
|
||||
<option value="dark">Dark</option>
|
||||
<option value="system">User system</option>
|
||||
</Select>
|
||||
<FormHelperText>
|
||||
Selecting system will make it so the embed would change depending
|
||||
on the user system's preferences.
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<FormLabel>Primary Color</FormLabel>
|
||||
<Select {...register("primaryColor")}>
|
||||
{ercOrMarketplace === "erc721" && (
|
||||
<option value="default">Default</option>
|
||||
)}
|
||||
{colorOptions.map((color) => (
|
||||
<option key={color} value={color}>
|
||||
{color[0]?.toUpperCase() + color.substring(1)}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<FormHelperText>
|
||||
Used for the main actions button backgrounds.
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
{ercOrMarketplace === "marketplace" ||
|
||||
ercOrMarketplace === "marketplace-v3" ? (
|
||||
<FormControl>
|
||||
<FormLabel>Secondary Color</FormLabel>
|
||||
<Select {...register("secondaryColor")}>
|
||||
<FormLabel>RPC Url</FormLabel>
|
||||
<Input type="url" {...register("rpcUrl")} />
|
||||
<FormHelperText>
|
||||
RPC the embed should use to connect to the blockchain.
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
{ercOrMarketplace === "marketplace" ||
|
||||
ercOrMarketplace === "marketplace-v3" ? null : (
|
||||
<FormControl gap={4}>
|
||||
<Heading size="title.sm" mb={2}>
|
||||
Gasless
|
||||
</Heading>
|
||||
<Select {...register("gasless")} mb={4}>
|
||||
<option value="false">Disabled</option>
|
||||
<option value="openZeppelin">OpenZeppelin Relayer</option>
|
||||
<option value="biconomy">Biconomy Relayer</option>
|
||||
</Select>
|
||||
{watch("gasless") === "openZeppelin" && (
|
||||
<FormControl>
|
||||
<FormLabel>OpenZeppelin Relayer URL</FormLabel>
|
||||
<Input type="url" {...register("relayUrl")} />
|
||||
</FormControl>
|
||||
)}
|
||||
{watch("gasless") === "biconomy" && (
|
||||
<Flex gap={4} flexDir="column">
|
||||
<FormControl>
|
||||
<FormLabel>Biconomy API key</FormLabel>
|
||||
<Input type="url" {...register("biconomyApiKey")} />
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel>Biconomy API ID</FormLabel>
|
||||
<Input type="url" {...register("biconomyApiId")} />
|
||||
</FormControl>
|
||||
</Flex>
|
||||
)}
|
||||
<FormHelperText>
|
||||
A relayer can be used to make the transaction gasless for the
|
||||
end user.{" "}
|
||||
<Link
|
||||
target="_blank"
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
href="https://blog.thirdweb.com/guides/setup-gasless-transactions"
|
||||
>
|
||||
Learn more
|
||||
</Link>
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||
<FormControl>
|
||||
<FormLabel>Theme</FormLabel>
|
||||
<Select {...register("theme")}>
|
||||
<option value="light">Light</option>
|
||||
<option value="dark">Dark</option>
|
||||
<option value="system">User system</option>
|
||||
</Select>
|
||||
<FormHelperText>
|
||||
Selecting system will make it so the embed would change
|
||||
depending on the user system's preferences.
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<FormLabel>Primary Color</FormLabel>
|
||||
<Select {...register("primaryColor")}>
|
||||
{ercOrMarketplace === "erc721" && (
|
||||
<option value="default">Default</option>
|
||||
)}
|
||||
{colorOptions.map((color) => (
|
||||
<option key={color} value={color}>
|
||||
{color[0]?.toUpperCase() + color.substring(1)}
|
||||
@@ -529,21 +547,38 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
|
||||
))}
|
||||
</Select>
|
||||
<FormHelperText>
|
||||
Use for secondary actions (like when the user is connected to
|
||||
the wrong network)
|
||||
Used for the main actions button backgrounds.
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
) : null}
|
||||
</Card>
|
||||
|
||||
<Card className="flex w-full flex-col gap-2 md:w-1/2">
|
||||
<Heading size="title.sm">Embed Code</Heading>
|
||||
<CodeClient code={embedCode} lang="html" />
|
||||
<Button
|
||||
className="w-auto gap-2"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
onCopy();
|
||||
{ercOrMarketplace === "marketplace" ||
|
||||
ercOrMarketplace === "marketplace-v3" ? (
|
||||
<FormControl>
|
||||
<FormLabel>Secondary Color</FormLabel>
|
||||
<Select {...register("secondaryColor")}>
|
||||
{colorOptions.map((color) => (
|
||||
<option key={color} value={color}>
|
||||
{color[0]?.toUpperCase() + color.substring(1)}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<FormHelperText>
|
||||
Use for secondary actions (like when the user is connected to
|
||||
the wrong network)
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
) : null}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="mb-3 font-semibold text-xl tracking-tight">
|
||||
Embed Code
|
||||
</h2>
|
||||
<CodeClient
|
||||
code={embedCode}
|
||||
lang="html"
|
||||
onCopy={() => {
|
||||
trackEvent({
|
||||
category: "embed",
|
||||
action: "click",
|
||||
@@ -552,19 +587,15 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
|
||||
chainId,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{hasCopied ? (
|
||||
<CheckIcon className="size-4" />
|
||||
) : (
|
||||
<CopyIcon className="size-4" />
|
||||
)}
|
||||
{hasCopied ? "Copied!" : "Copy to clipboard"}
|
||||
</Button>
|
||||
</Card>
|
||||
</Flex>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Heading size="title.sm">Preview</Heading>
|
||||
<div className="h-8" />
|
||||
|
||||
<h2 className="mb-3 font-semibold text-xl tracking-tight">Preview</h2>
|
||||
|
||||
<div className="overflow-hidden rounded-lg border border-border">
|
||||
{!validApiKey ? (
|
||||
<Text>You need to create a client ID to use embeds</Text>
|
||||
) : iframeSrc ? (
|
||||
@@ -577,6 +608,6 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</Flex>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+2
-2
@@ -35,7 +35,7 @@ export const ContractExplorerPage: React.FC<ContractExplorePageProps> = ({
|
||||
|
||||
const functions = getContractFunctionsFromAbi(abi);
|
||||
return (
|
||||
<div className="flex h-[70vh] flex-col">
|
||||
<div className="flex h-[70vh] flex-col ">
|
||||
{functions && functions.length > 0 ? (
|
||||
<ContractFunctionsOverview
|
||||
onlyFunctions
|
||||
@@ -44,7 +44,7 @@ export const ContractExplorerPage: React.FC<ContractExplorePageProps> = ({
|
||||
twAccount={twAccount}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center gap-2 text-center">
|
||||
<div className="flex flex-col items-center justify-center gap-2 bg-card text-center">
|
||||
<h2 className="font-semibold text-2xl tracking-tight">
|
||||
No callable functions discovered in ABI.
|
||||
</h2>
|
||||
|
||||
+33
-14
@@ -26,6 +26,7 @@ import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { TransactionButton } from "components/buttons/TransactionButton";
|
||||
import { addDays, fromUnixTime } from "date-fns";
|
||||
import { useAllChainsData } from "hooks/chains/allChains";
|
||||
@@ -119,22 +120,39 @@ function ClaimableModule(props: ModuleInstanceProps) {
|
||||
claimConditionQuery.data?.startTimestamp === 0 &&
|
||||
claimConditionQuery.data?.endTimestamp === 0;
|
||||
|
||||
const currencyContract = getContract({
|
||||
address: claimConditionQuery.data?.currency || "",
|
||||
chain: props.contract.chain,
|
||||
client: props.contract.client,
|
||||
});
|
||||
const claimConditionCurrency = claimConditionQuery.data?.currency;
|
||||
|
||||
const shouldFetchTokenDecimals =
|
||||
claimConditionQuery.data &&
|
||||
claimConditionQuery.data?.currency !== ZERO_ADDRESS;
|
||||
const shouldFetchCurrencyDecimals =
|
||||
!!claimConditionCurrency && claimConditionCurrency !== ZERO_ADDRESS;
|
||||
|
||||
const currencyDecimalsQuery = useReadContract(decimals, {
|
||||
contract: currencyContract,
|
||||
queryOptions: {
|
||||
enabled: shouldFetchTokenDecimals,
|
||||
const currencyDecimalsQuery = useQuery({
|
||||
queryKey: [
|
||||
"currency-decimals",
|
||||
{
|
||||
contractAddress: props.contract.address,
|
||||
chainId: props.contract.chain.id,
|
||||
claimConditionCurrency,
|
||||
},
|
||||
],
|
||||
queryFn: async () => {
|
||||
if (!claimConditionCurrency) {
|
||||
throw new Error();
|
||||
}
|
||||
const currencyContract = getContract({
|
||||
address: claimConditionCurrency,
|
||||
chain: props.contract.chain,
|
||||
client: props.contract.client,
|
||||
});
|
||||
|
||||
const decimalsVal = await decimals({
|
||||
contract: currencyContract,
|
||||
});
|
||||
|
||||
return decimalsVal;
|
||||
},
|
||||
enabled: shouldFetchCurrencyDecimals,
|
||||
});
|
||||
|
||||
const tokenDecimalsQuery = useReadContract(decimals, {
|
||||
contract: contract,
|
||||
queryOptions: {
|
||||
@@ -265,7 +283,7 @@ function ClaimableModule(props: ModuleInstanceProps) {
|
||||
// claim conditions data is present
|
||||
claimConditionQuery.data &&
|
||||
// token decimals is fetched if it should be fetched
|
||||
(shouldFetchTokenDecimals ? currencyDecimalsQuery.isFetched : true)
|
||||
(shouldFetchCurrencyDecimals ? currencyDecimalsQuery.isFetched : true)
|
||||
? {
|
||||
claimCondition: claimConditionQuery.data,
|
||||
currencyDecimals: currencyDecimalsQuery.data,
|
||||
@@ -276,7 +294,7 @@ function ClaimableModule(props: ModuleInstanceProps) {
|
||||
tokenId,
|
||||
isLoading:
|
||||
claimConditionQuery.isLoading ||
|
||||
(!!shouldFetchTokenDecimals && currencyDecimalsQuery.isLoading),
|
||||
(!!shouldFetchCurrencyDecimals && currencyDecimalsQuery.isLoading),
|
||||
}}
|
||||
isOwnerAccount={!!ownerAccount}
|
||||
name={props.contractInfo.name}
|
||||
@@ -565,6 +583,7 @@ function ClaimConditionSection(props: {
|
||||
onClick={() => setAddClaimConditionButtonClicked(true)}
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
disabled={!props.isOwnerAccount}
|
||||
>
|
||||
Add Claim Condition
|
||||
</Button>
|
||||
|
||||
+2
-2
@@ -4,13 +4,13 @@ import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
|
||||
import { Alert, AlertTitle } from "@/components/ui/alert";
|
||||
import type { Account as TWAccount } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { CircleSlash } from "lucide-react";
|
||||
import type { ContractOptions } from "thirdweb";
|
||||
import type { ThirdwebContract } from "thirdweb/contract";
|
||||
import type { Account } from "thirdweb/wallets";
|
||||
import { ModuleCard } from "./module-card";
|
||||
import { useAllModuleContractInfo } from "./moduleContractInfo";
|
||||
|
||||
export const InstalledModulesTable = (props: {
|
||||
contract: ContractOptions;
|
||||
contract: ThirdwebContract;
|
||||
installedModules: {
|
||||
data?: string[];
|
||||
isPending: boolean;
|
||||
|
||||
+10
-7
@@ -15,8 +15,9 @@ import { FormProvider, type UseFormReturn, useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
type Chain,
|
||||
type ContractOptions,
|
||||
type ThirdwebClient,
|
||||
type ThirdwebContract,
|
||||
getContract,
|
||||
sendTransaction,
|
||||
waitForReceipt,
|
||||
} from "thirdweb";
|
||||
@@ -47,7 +48,7 @@ type FormData = {
|
||||
};
|
||||
|
||||
type InstallModuleFormProps = {
|
||||
contract: ContractOptions;
|
||||
contract: ThirdwebContract;
|
||||
refetchModules: () => void;
|
||||
account: Account;
|
||||
installedModules: {
|
||||
@@ -180,11 +181,13 @@ export const InstallModuleForm = (props: InstallModuleFormProps) => {
|
||||
|
||||
return Promise.all(
|
||||
moduleAddress.map(async (address) => {
|
||||
const result = await resolveImplementation({
|
||||
client,
|
||||
address,
|
||||
chain: contract.chain,
|
||||
});
|
||||
const result = await resolveImplementation(
|
||||
getContract({
|
||||
client,
|
||||
address,
|
||||
chain: contract.chain,
|
||||
}),
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
throw new Error("Failed to fetch bytecode for module");
|
||||
|
||||
+3
-3
@@ -23,7 +23,7 @@ import { Suspense, useEffect, useState } from "react";
|
||||
import { ErrorBoundary, type FallbackProps } from "react-error-boundary";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
type ContractOptions,
|
||||
type ThirdwebContract,
|
||||
getContract,
|
||||
sendTransaction,
|
||||
waitForReceipt,
|
||||
@@ -35,7 +35,7 @@ import { useModuleContractInfo } from "./moduleContractInfo";
|
||||
|
||||
type ModuleCardProps = {
|
||||
moduleAddress: string;
|
||||
contract: ContractOptions;
|
||||
contract: ThirdwebContract;
|
||||
onRemoveModule: () => void;
|
||||
ownerAccount: Account | undefined;
|
||||
allModuleContractInfo: {
|
||||
@@ -204,7 +204,7 @@ export type ModuleCardUIProps = {
|
||||
|
||||
export function ModuleCardUI(props: ModuleCardUIProps) {
|
||||
return (
|
||||
<section className="rounded-lg border border-border bg-muted/50">
|
||||
<section className="rounded-lg border border-border bg-card">
|
||||
{/* Header */}
|
||||
<div className="relative p-4 lg:p-6">
|
||||
{/* Title */}
|
||||
|
||||
+5
-5
@@ -95,7 +95,7 @@ export const NFTGetAllTable: React.FC<ContractOverviewNFTGetAllProps> = ({
|
||||
Header: "Description",
|
||||
accessor: (row) => row.metadata.description,
|
||||
Cell: (cell: CellProps<NFT, string>) => {
|
||||
if (typeof cell.value !== "string") {
|
||||
if (cell.value && typeof cell.value !== "string") {
|
||||
return (
|
||||
<UnexpectedValueErrorMessage
|
||||
title="Invalid description"
|
||||
@@ -239,7 +239,7 @@ export const NFTGetAllTable: React.FC<ContractOverviewNFTGetAllProps> = ({
|
||||
<Flex gap={4} direction="column">
|
||||
<TableContainer
|
||||
maxW="100%"
|
||||
className="relative rounded-lg border border-border"
|
||||
className="relative rounded-lg border border-border bg-card"
|
||||
>
|
||||
{getNFTsQuery.isFetching && (
|
||||
<Spinner
|
||||
@@ -251,7 +251,7 @@ export const NFTGetAllTable: React.FC<ContractOverviewNFTGetAllProps> = ({
|
||||
/>
|
||||
)}
|
||||
<Table {...getTableProps()}>
|
||||
<Thead className="!bg-muted/50">
|
||||
<Thead className="!bg-background">
|
||||
{headerGroups.map((headerGroup, headerGroupIndex) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
|
||||
<Tr {...headerGroup.getHeaderGroupProps()} key={headerGroupIndex}>
|
||||
@@ -275,7 +275,7 @@ export const NFTGetAllTable: React.FC<ContractOverviewNFTGetAllProps> = ({
|
||||
<Tbody
|
||||
{...getTableBodyProps()}
|
||||
position="relative"
|
||||
className="!bg-background"
|
||||
className="!bg-card"
|
||||
>
|
||||
{page.map((row, rowIndex) => {
|
||||
const failedToLoad = !row.original.tokenURI;
|
||||
@@ -284,7 +284,7 @@ export const NFTGetAllTable: React.FC<ContractOverviewNFTGetAllProps> = ({
|
||||
<Tr
|
||||
{...row.getRowProps()}
|
||||
role="group"
|
||||
className="hover:bg-muted/50"
|
||||
className="bg-card hover:bg-accent/50"
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => {
|
||||
const tokenId = row.original.id;
|
||||
|
||||
+31
-16
@@ -3,9 +3,9 @@
|
||||
import { ThirdwebAreaChart } from "@/components/blocks/charts/area-chart";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
useLogsAnalytics,
|
||||
useTransactionAnalytics,
|
||||
useUniqueWalletsAnalytics,
|
||||
useContractEventAnalytics,
|
||||
useContractTransactionAnalytics,
|
||||
useContractUniqueWalletAnalytics,
|
||||
} from "data/analytics/hooks";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { ArrowRightIcon } from "lucide-react";
|
||||
@@ -76,31 +76,45 @@ type ChartProps = {
|
||||
endDate: Date;
|
||||
};
|
||||
|
||||
function getDayKey(date: Date) {
|
||||
return date.toISOString().split("T")[0];
|
||||
}
|
||||
|
||||
function OverviewAnalytics(props: ChartProps) {
|
||||
const wallets = useUniqueWalletsAnalytics(props);
|
||||
const transactions = useTransactionAnalytics(props);
|
||||
const events = useLogsAnalytics(props);
|
||||
const wallets = useContractUniqueWalletAnalytics(props);
|
||||
const transactions = useContractTransactionAnalytics(props);
|
||||
const events = useContractEventAnalytics(props);
|
||||
const isPending =
|
||||
wallets.isPending || transactions.isPending || events.isPending;
|
||||
|
||||
const mergedData = useMemo(() => {
|
||||
if (isPending) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const time = (wallets.data || transactions.data || events.data || []).map(
|
||||
(wallet) => wallet.time,
|
||||
);
|
||||
|
||||
return time.map((time) => {
|
||||
const wallet = wallets.data?.find((wallet) => wallet.time === time);
|
||||
const transaction = transactions.data?.find(
|
||||
(transaction) => transaction.time === time,
|
||||
const wallet = wallets.data?.find(
|
||||
(wallet) => getDayKey(wallet.time) === getDayKey(time),
|
||||
);
|
||||
const event = events.data?.find((event) => event.time === time);
|
||||
const transaction = transactions.data?.find(
|
||||
(transaction) => getDayKey(transaction.time) === getDayKey(time),
|
||||
);
|
||||
const event = events.data?.find((event) => {
|
||||
return getDayKey(event.time) === getDayKey(time);
|
||||
});
|
||||
|
||||
return {
|
||||
time,
|
||||
wallets: wallet?.wallets || 0,
|
||||
wallets: wallet?.count || 0,
|
||||
transactions: transaction?.count || 0,
|
||||
events: event?.count || 0,
|
||||
};
|
||||
});
|
||||
}, [wallets.data, transactions.data, events.data]);
|
||||
}, [wallets.data, transactions.data, events.data, isPending]);
|
||||
|
||||
return (
|
||||
<ThirdwebAreaChart
|
||||
@@ -110,17 +124,18 @@ function OverviewAnalytics(props: ChartProps) {
|
||||
color: "hsl(var(--chart-1))",
|
||||
},
|
||||
transactions: {
|
||||
label: "Total Transactions",
|
||||
label: "Transactions",
|
||||
color: "hsl(var(--chart-2))",
|
||||
},
|
||||
events: {
|
||||
label: "Total Events",
|
||||
label: "Events",
|
||||
color: "hsl(var(--chart-3))",
|
||||
},
|
||||
}}
|
||||
data={mergedData}
|
||||
data={mergedData || []}
|
||||
isPending={isPending}
|
||||
showLegend
|
||||
chartClassName="aspect[2] lg:aspect-[4.5]"
|
||||
chartClassName="aspect-[1.5] lg:aspect-[4.5]"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+1
-1
@@ -265,7 +265,7 @@ const ListingCards: React.FC<ListingCardsProps> = ({
|
||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-3 md:gap-6 max-sm:[&>*:nth-child(n+3)]:hidden">
|
||||
{listings.map((listing, index) => (
|
||||
<div
|
||||
className="relative rounded-lg border border-border bg-muted/50 hover:border-foreground hover:bg-muted"
|
||||
className="relative rounded-lg border border-border bg-card transition-colors hover:border-active-border"
|
||||
key={`${listing.creatorAddress}-${index}`}
|
||||
>
|
||||
<div className="relative aspect-square w-full overflow-hidden rounded-lg">
|
||||
|
||||
+3
-3
@@ -6,9 +6,9 @@ import { getBytecode, getContract } from "thirdweb/contract";
|
||||
import { getPublishedUriFromCompilerUri } from "thirdweb/extensions/thirdweb";
|
||||
import { getInstalledModules } from "thirdweb/modules";
|
||||
import { download } from "thirdweb/storage";
|
||||
import { extractIPFSUri } from "thirdweb/utils";
|
||||
import { extractIPFSUri, isValidENSName } from "thirdweb/utils";
|
||||
import { fetchPublishedContractsFromDeploy } from "../../../../../../../components/contract-components/fetchPublishedContractsFromDeploy";
|
||||
import { isEnsName, resolveEns } from "../../../../../../../lib/ens";
|
||||
import { resolveEns } from "../../../../../../../lib/ens";
|
||||
|
||||
type ModuleMetadataPickedKeys = {
|
||||
publisher: string;
|
||||
@@ -52,7 +52,7 @@ export async function getPublishedByCardProps(params: {
|
||||
|
||||
// get publisher address/ens
|
||||
let publisherAddressOrEns = publishedContractToShow.publisher;
|
||||
if (!isEnsName(publishedContractToShow.publisher)) {
|
||||
if (!isValidENSName(publishedContractToShow.publisher)) {
|
||||
try {
|
||||
const res = await resolveEns(publishedContractToShow.publisher);
|
||||
if (res.ensName) {
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ export function StatCard(props: {
|
||||
label: string;
|
||||
}) {
|
||||
return (
|
||||
<dl className="block rounded-lg border border-border bg-muted/50 p-4">
|
||||
<dl className="block rounded-lg border border-border bg-card p-4">
|
||||
<dt className="mb-1.5 text-sm md:text-base">{props.label}</dt>
|
||||
<SkeletonContainer
|
||||
loadedData={props.isPending ? undefined : props.value}
|
||||
|
||||
+6
-2
@@ -157,7 +157,7 @@ export const AirdropUpload: React.FC<AirdropUploadProps> = ({
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full cursor-pointer items-center justify-center rounded-md border border-border hover:border-primary",
|
||||
noCsv ? "bg-red-200" : "bg-card",
|
||||
noCsv ? "bg-red-200" : "bg-background",
|
||||
)}
|
||||
{...getRootProps()}
|
||||
>
|
||||
@@ -193,7 +193,11 @@ export const AirdropUpload: React.FC<AirdropUploadProps> = ({
|
||||
Files <em>must</em> contain one .csv file with an address and
|
||||
quantity column, if the quantity column is not provided, that
|
||||
record will be flagged as invalid.
|
||||
<Link download color="primary.500" href="/airdrop.csv">
|
||||
<Link
|
||||
download
|
||||
color="primary.500"
|
||||
href="/assets/examples/airdrop.csv"
|
||||
>
|
||||
Download an example CSV
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user