Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
942d20a5d9 | ||
|
|
739a2ae3c5 | ||
|
|
4158a796a8 | ||
|
|
a258e7eaf0 | ||
|
|
e386ac9476 | ||
|
|
0574eac02c | ||
|
|
52cbcd2d57 | ||
|
|
387891dde0 | ||
|
|
562c534d37 | ||
|
|
b882e29935 | ||
|
|
9ad1d3cbbf | ||
|
|
aab3da0729 | ||
|
|
78c9d2fe13 | ||
|
|
557d3e3dbd | ||
|
|
06d6504545 | ||
|
|
e201e3d423 | ||
|
|
c8ef941d8e | ||
|
|
39a5b5c8a3 | ||
|
|
6f381d768c | ||
|
|
0c5a51c5bf | ||
|
|
90428b4574 | ||
|
|
e24547bed1 | ||
|
|
3ff8c3fbb2 | ||
|
|
4bd03b06e7 | ||
|
|
b7158c2b54 | ||
|
|
5f26883fbd | ||
|
|
98b6198687 | ||
|
|
880148b793 | ||
|
|
ee1bc3eb07 | ||
|
|
e9ab8cb770 | ||
|
|
9c33981b72 | ||
|
|
c645b42684 | ||
|
|
7583074c06 | ||
|
|
fbafb65f5c | ||
|
|
6b286c7e83 | ||
|
|
159ffbfaee | ||
|
|
0c1bb6ccae | ||
|
|
dceb47de8b | ||
|
|
dfbdbd06e5 | ||
|
|
66bbedbe3a | ||
|
|
2fbf105d15 | ||
|
|
afcaa91873 | ||
|
|
51f955352d | ||
|
|
6b2a90ac89 | ||
|
|
cf53184bb1 | ||
|
|
815b9b1ce1 | ||
|
|
ae1baa5105 | ||
|
|
92266b7459 | ||
|
|
80ad84b803 | ||
|
|
633f0cadf1 | ||
|
|
91b0367b23 | ||
|
|
c86e13b522 | ||
|
|
bc42c2fdfb | ||
|
|
cad9c74bef | ||
|
|
6f2f92ddea | ||
|
|
97c9ab7e1c | ||
|
|
b73fa4b16f | ||
|
|
cbfe9ec459 | ||
|
|
fecd3a6c16 | ||
|
|
8aeb4d6308 | ||
|
|
f1cd2539d1 | ||
|
|
a322e16853 | ||
|
|
e50d77d28f | ||
|
|
7f3b91da37 | ||
|
|
41d50443d9 | ||
|
|
a8ac98bd49 | ||
|
|
0781da9dca | ||
|
|
c5dfad8dbb | ||
|
|
9663079062 | ||
|
|
1f6bb7c329 | ||
|
|
30e13e6b91 | ||
|
|
2dfc245d44 | ||
|
|
64d7bf358f | ||
|
|
a6e9c97c6f | ||
|
|
ee57ded902 | ||
|
|
6d3947ab0c | ||
|
|
632f1d138f | ||
|
|
18f0e575c4 | ||
|
|
e732b11e45 | ||
|
|
f77165e2d1 | ||
|
|
dfd64d2896 | ||
|
|
4e703fdb5d | ||
|
|
af88935680 | ||
|
|
c50c82aacd | ||
|
|
0fa8354dec | ||
|
|
769a927627 | ||
|
|
ceff873cba | ||
|
|
9a432af1cf | ||
|
|
ee6bd4f898 | ||
|
|
ccf6329810 | ||
|
|
821caa63d7 | ||
|
|
09e396d856 | ||
|
|
9573b2f9d2 | ||
|
|
b004a07e5d | ||
|
|
2396b4e461 | ||
|
|
ca6c913361 | ||
|
|
cf166e4c04 | ||
|
|
a51192ffbd | ||
|
|
6fd36aabc7 | ||
|
|
08cc48910d | ||
|
|
f03ded300f | ||
|
|
b651bae4ba | ||
|
|
66833a832c | ||
|
|
9d5828eeab | ||
|
|
b191658fcd | ||
|
|
a6b7e8d818 | ||
|
|
f4f93e6353 | ||
|
|
b693b78645 | ||
|
|
daceeed2ec | ||
|
|
5f7c726917 | ||
|
|
2405456e5f | ||
|
|
a76e6534c4 | ||
|
|
0fb586db52 | ||
|
|
f8ebbe54d9 | ||
|
|
ca756819db | ||
|
|
7217e774b2 | ||
|
|
5de72c5ecd | ||
|
|
60c5c26583 | ||
|
|
dc74c3b4b9 | ||
|
|
69a921d01f | ||
|
|
9abdb92028 | ||
|
|
6ed2fd13de | ||
|
|
a22457a69c | ||
|
|
ae675db8c8 | ||
|
|
54c9333fc9 | ||
|
|
541cf0b3e9 | ||
|
|
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 |
@@ -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.
|
||||
|
||||
-->
|
||||
|
||||
@@ -27,5 +27,9 @@
|
||||
},
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
}
|
||||
},
|
||||
"eslint.workingDirectories": [
|
||||
{ "pattern": "./packages/*/" },
|
||||
{ "pattern": "./apps/*/" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ module.exports = [
|
||||
// -- connect
|
||||
"/connect",
|
||||
"/connect/sign-in",
|
||||
"/connect/pay",
|
||||
"/connect/account-abstraction",
|
||||
"/connect/universal-bridge",
|
||||
// -- nebula
|
||||
"/nebula",
|
||||
// --insight
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
||||
|
||||
@@ -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.4",
|
||||
"@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.16",
|
||||
"@radix-ui/react-tooltip": "1.1.7",
|
||||
"@sentry/nextjs": "8.53.0",
|
||||
"@shazow/whatsabi": "^0.19.0",
|
||||
"@tanstack/react-query": "5.66.0",
|
||||
"@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,19 +62,18 @@
|
||||
"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-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",
|
||||
@@ -83,17 +82,18 @@
|
||||
"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",
|
||||
"responsive-rsc": "0.0.7",
|
||||
"server-only": "^0.0.1",
|
||||
"shiki": "1.27.0",
|
||||
"sonner": "^1.7.1",
|
||||
"sonner": "^1.7.4",
|
||||
"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:*",
|
||||
@@ -103,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.3",
|
||||
"@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.5.0",
|
||||
"@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.1",
|
||||
"@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.13.0",
|
||||
"@types/papaparse": "^5.3.15",
|
||||
"@types/pluralize": "^0.0.33",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/react": "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",
|
||||
"postcss": "8.5.1",
|
||||
"storybook": "8.5.2",
|
||||
"tailwindcss": "3.4.17",
|
||||
"typescript": "5.7.2"
|
||||
"typescript": "5.7.3"
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 583 KiB |
|
Before Width: | Height: | Size: 167 KiB |
|
Before Width: | Height: | Size: 295 KiB |
|
Before Width: | Height: | Size: 319 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 333 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 234 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 182 KiB |
|
Before Width: | Height: | Size: 258 KiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 221 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 956 KiB |
|
Before Width: | Height: | Size: 695 KiB |
|
Before Width: | Height: | Size: 620 KiB |
|
Before Width: | Height: | Size: 709 KiB |
|
Before Width: | Height: | Size: 906 KiB |
|
Before Width: | Height: | Size: 551 KiB |
|
Before Width: | Height: | Size: 601 KiB |
|
Before Width: | Height: | Size: 541 KiB |
|
Before Width: | Height: | Size: 268 KiB |
|
Before Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 204 KiB |
|
Before Width: | Height: | Size: 733 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 3.4 MiB |
|
Before Width: | Height: | Size: 333 KiB |
@@ -331,6 +331,18 @@ async function redirects() {
|
||||
destination: "/templates/:slug",
|
||||
permanent: false,
|
||||
},
|
||||
// redirect /account-abstraction to /connect/account-abstraction
|
||||
{
|
||||
source: "/account-abstraction",
|
||||
destination: "/connect/account-abstraction",
|
||||
permanent: false,
|
||||
},
|
||||
// redirect /connect/pay to /connect/universal-bridge
|
||||
{
|
||||
source: "/connect/pay",
|
||||
destination: "/connect/universal-bridge",
|
||||
permanent: false,
|
||||
},
|
||||
// PREVIOUS CAMPAIGNS
|
||||
{
|
||||
source: "/unlimited-wallets",
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { promises as fs } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
// after running logUnusedAssets.ts,
|
||||
// 1. validate the output
|
||||
// 2. If you think that a file should not be deleted, remove it from the output
|
||||
// 3. paste the output files in `filesToDelete` and run this script to delete them
|
||||
const filesToDelete: string[] = [];
|
||||
|
||||
async function deleteFiles() {
|
||||
for (const filePath of filesToDelete) {
|
||||
try {
|
||||
const fullPath = path.join(process.cwd(), filePath);
|
||||
await fs.unlink(fullPath);
|
||||
console.log(`Deleted: ${filePath}`);
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
console.warn(`File not found: ${filePath}`);
|
||||
} else {
|
||||
console.error(`Error deleting ${filePath}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("--- DONE ---");
|
||||
}
|
||||
|
||||
deleteFiles();
|
||||
@@ -26,9 +26,6 @@ const filesToIgnore = new Set([
|
||||
// macOS stuff
|
||||
"public/assets/product-pages/.DS_Store",
|
||||
"public/assets/.DS_Store",
|
||||
// pdfs
|
||||
"public/Thirdweb_Terms_of_Service.pdf",
|
||||
"public/thirdweb_Privacy_Policy_May_2022.pdf",
|
||||
]);
|
||||
|
||||
function getAllFilesInFolder(folderPath: string) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { API_SERVER_URL } from "../constants/env";
|
||||
export async function updateAccount(values: {
|
||||
name?: string;
|
||||
email?: string;
|
||||
image?: string | null;
|
||||
}) {
|
||||
const token = await getAuthToken();
|
||||
|
||||
|
||||
@@ -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[]> {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import "server-only";
|
||||
import { API_SERVER_URL, THIRDWEB_API_SECRET } from "../constants/env";
|
||||
|
||||
export async function getGasSponsoredChains() {
|
||||
if (!THIRDWEB_API_SECRET) {
|
||||
throw new Error("API_SERVER_SECRET is not set");
|
||||
}
|
||||
const res = await fetch(`${API_SERVER_URL}/v1/chains/gas-sponsored`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-service-api-key": THIRDWEB_API_SECRET,
|
||||
},
|
||||
next: {
|
||||
revalidate: 15 * 60, //15 minutes
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
console.error(
|
||||
"Failed to fetch gas sponsored chains",
|
||||
res.status,
|
||||
res.statusText,
|
||||
);
|
||||
res.body?.cancel();
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
return (await res.json()).data as number[];
|
||||
} catch (e) {
|
||||
console.error("Failed to parse gas sponsored chains", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -24,8 +24,9 @@ export type Team = {
|
||||
image?: string;
|
||||
billingPlan: "pro" | "growth" | "free" | "starter";
|
||||
billingStatus: "validPayment" | (string & {}) | null;
|
||||
supportPlan: "pro" | "growth" | "free" | "starter";
|
||||
billingEmail: string | null;
|
||||
growthTrialEligible: boolean | null;
|
||||
growthTrialEligible: false;
|
||||
enabledScopes: EnabledTeamScope[];
|
||||
};
|
||||
|
||||
@@ -47,6 +48,10 @@ export async function getTeamBySlug(slug: string) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getTeamById(id: string) {
|
||||
return getTeamBySlug(id);
|
||||
}
|
||||
|
||||
export async function getTeams() {
|
||||
const token = await getAuthToken();
|
||||
if (!token) {
|
||||
|
||||
@@ -28,7 +28,7 @@ export const Mobile: Story = {
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="container flex max-w-[1000px] flex-col gap-8 lg:p-10">
|
||||
<div className="container flex max-w-[1000px] flex-col gap-8 py-10">
|
||||
<BadgeContainer label="Base">
|
||||
<DangerSettingCard
|
||||
title="This is a title"
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { AsteriskIcon, InfoIcon } from "lucide-react";
|
||||
import type React from "react";
|
||||
|
||||
export function FormFieldSetup(props: {
|
||||
htmlFor?: string;
|
||||
label: string;
|
||||
label: React.ReactNode;
|
||||
errorMessage: React.ReactNode | undefined;
|
||||
children: React.ReactNode;
|
||||
tooltip?: React.ReactNode;
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
type ChartConfig,
|
||||
ChartContainer,
|
||||
@@ -15,8 +22,14 @@ import {
|
||||
EmptyChartState,
|
||||
LoadingChartState,
|
||||
} from "../../../../components/analytics/empty-chart-state";
|
||||
import { cn } from "../../../lib/utils";
|
||||
|
||||
type ThirdwebAreaChartProps<TConfig extends ChartConfig> = {
|
||||
header?: {
|
||||
title: string;
|
||||
description?: string;
|
||||
titleClassName?: string;
|
||||
};
|
||||
// chart config
|
||||
config: TConfig;
|
||||
data: Array<Record<keyof TConfig, number> & { time: number | string | Date }>;
|
||||
@@ -25,78 +38,99 @@ type ThirdwebAreaChartProps<TConfig extends ChartConfig> = {
|
||||
// chart className
|
||||
chartClassName?: string;
|
||||
isPending: boolean;
|
||||
hideLabel?: boolean;
|
||||
toolTipLabelFormatter?: (label: string, payload: unknown) => React.ReactNode;
|
||||
};
|
||||
|
||||
export function ThirdwebAreaChart<TConfig extends ChartConfig>(
|
||||
props: ThirdwebAreaChartProps<TConfig>,
|
||||
) {
|
||||
const configKeys = useMemo(() => Object.keys(props.config), [props.config]);
|
||||
return (
|
||||
<div className="rounded-lg border border-border px-4 pt-10 pb-4">
|
||||
<ChartContainer config={props.config} className={props.chartClassName}>
|
||||
{props.isPending ? (
|
||||
<LoadingChartState />
|
||||
) : props.data.length === 0 ? (
|
||||
<EmptyChartState />
|
||||
) : (
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
data={props.data}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={20}
|
||||
tickFormatter={(value) => formatDate(new Date(value), "MMM dd")}
|
||||
/>
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
|
||||
<defs>
|
||||
{configKeys.map((key) => (
|
||||
<linearGradient
|
||||
key={key}
|
||||
id={`fill_${key}`}
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop
|
||||
offset="5%"
|
||||
stopColor={`var(--color-${key})`}
|
||||
stopOpacity={0.8}
|
||||
/>
|
||||
<stop
|
||||
offset="95%"
|
||||
stopColor={`var(--color-${key})`}
|
||||
stopOpacity={0.1}
|
||||
/>
|
||||
</linearGradient>
|
||||
))}
|
||||
</defs>
|
||||
{configKeys.map((key) => (
|
||||
<Area
|
||||
key={key}
|
||||
dataKey={key}
|
||||
type="natural"
|
||||
fill={`url(#fill_${key})`}
|
||||
fillOpacity={0.4}
|
||||
stroke={`var(--color-${key})`}
|
||||
stackId="a"
|
||||
/>
|
||||
))}
|
||||
|
||||
{props.showLegend && (
|
||||
<ChartLegend content={<ChartLegendContent />} className="pt-8" />
|
||||
)}
|
||||
</AreaChart>
|
||||
)}
|
||||
</ChartContainer>
|
||||
</div>
|
||||
return (
|
||||
<Card>
|
||||
{props.header && (
|
||||
<CardHeader>
|
||||
<CardTitle className={cn("mb-2", props.header.titleClassName)}>
|
||||
{props.header.title}
|
||||
</CardTitle>
|
||||
{props.header.description && (
|
||||
<CardDescription>{props.header.description}</CardDescription>
|
||||
)}
|
||||
</CardHeader>
|
||||
)}
|
||||
|
||||
<CardContent className={cn(!props.header && "pt-6")}>
|
||||
<ChartContainer config={props.config} className={props.chartClassName}>
|
||||
{props.isPending ? (
|
||||
<LoadingChartState />
|
||||
) : props.data.length === 0 ? (
|
||||
<EmptyChartState />
|
||||
) : (
|
||||
<AreaChart accessibilityLayer data={props.data}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={20}
|
||||
tickFormatter={(value) => formatDate(new Date(value), "MMM dd")}
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
hideLabel={
|
||||
props.hideLabel !== undefined ? props.hideLabel : true
|
||||
}
|
||||
labelFormatter={props.toolTipLabelFormatter}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<defs>
|
||||
{configKeys.map((key) => (
|
||||
<linearGradient
|
||||
key={key}
|
||||
id={`fill_${key}`}
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop
|
||||
offset="5%"
|
||||
stopColor={`var(--color-${key})`}
|
||||
stopOpacity={0.8}
|
||||
/>
|
||||
<stop
|
||||
offset="95%"
|
||||
stopColor={`var(--color-${key})`}
|
||||
stopOpacity={0.1}
|
||||
/>
|
||||
</linearGradient>
|
||||
))}
|
||||
</defs>
|
||||
{configKeys.map((key) => (
|
||||
<Area
|
||||
key={key}
|
||||
dataKey={key}
|
||||
type="natural"
|
||||
fill={`url(#fill_${key})`}
|
||||
fillOpacity={0.4}
|
||||
stroke={`var(--color-${key})`}
|
||||
stackId="a"
|
||||
/>
|
||||
))}
|
||||
|
||||
{props.showLegend && (
|
||||
<ChartLegend
|
||||
content={<ChartLegendContent className="pt-8" />}
|
||||
/>
|
||||
)}
|
||||
</AreaChart>
|
||||
)}
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,8 +27,11 @@ import { cn } from "../../../lib/utils";
|
||||
|
||||
type ThirdwebBarChartProps<TConfig extends ChartConfig> = {
|
||||
// metadata
|
||||
title: string;
|
||||
description?: string;
|
||||
header?: {
|
||||
title: string;
|
||||
description?: string;
|
||||
titleClassName?: string;
|
||||
};
|
||||
// chart config
|
||||
config: TConfig;
|
||||
data: Array<Record<keyof TConfig, number> & { time: number | string | Date }>;
|
||||
@@ -39,7 +42,6 @@ type ThirdwebBarChartProps<TConfig extends ChartConfig> = {
|
||||
isPending: boolean;
|
||||
toolTipLabelFormatter?: (label: string, payload: unknown) => React.ReactNode;
|
||||
hideLabel?: boolean;
|
||||
titleClassName?: string;
|
||||
};
|
||||
|
||||
export function ThirdwebBarChart<TConfig extends ChartConfig>(
|
||||
@@ -52,15 +54,18 @@ export function ThirdwebBarChart<TConfig extends ChartConfig>(
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className={cn("mb-2", props.titleClassName)}>
|
||||
{props.title}
|
||||
</CardTitle>
|
||||
{props.description && (
|
||||
<CardDescription>{props.description}</CardDescription>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{props.header && (
|
||||
<CardHeader>
|
||||
<CardTitle className={cn("mb-2", props.header.titleClassName)}>
|
||||
{props.header.title}
|
||||
</CardTitle>
|
||||
{props.header.description && (
|
||||
<CardDescription>{props.header.description}</CardDescription>
|
||||
)}
|
||||
</CardHeader>
|
||||
)}
|
||||
|
||||
<CardContent className={cn(!props.header && "pt-6")}>
|
||||
<ChartContainer config={props.config} className={props.chartClassName}>
|
||||
{props.isPending ? (
|
||||
<LoadingChartState />
|
||||
@@ -87,7 +92,9 @@ export function ThirdwebBarChart<TConfig extends ChartConfig>(
|
||||
}
|
||||
/>
|
||||
{props.showLegend && (
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
<ChartLegend
|
||||
content={<ChartLegendContent className="pt-5" />}
|
||||
/>
|
||||
)}
|
||||
{configKeys.map((key, idx) => (
|
||||
<Bar
|
||||
|
||||
@@ -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";
|
||||
@@ -148,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>
|
||||
|
||||
@@ -2,27 +2,29 @@
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ClientOnly } from "components/ClientOnly/ClientOnly";
|
||||
import { Moon, Sun } from "lucide-react";
|
||||
import { MoonIcon, SunIcon } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { Skeleton } from "./ui/skeleton";
|
||||
|
||||
export function ColorModeToggle() {
|
||||
export function ToggleThemeButton() {
|
||||
const { setTheme, theme } = useTheme();
|
||||
|
||||
return (
|
||||
<ClientOnly ssr={<Skeleton className="size-10 border bg-accent" />}>
|
||||
<ClientOnly
|
||||
ssr={<Skeleton className="size-[34px] rounded-full border bg-accent" />}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setTheme(theme === "dark" ? "light" : "dark");
|
||||
}}
|
||||
aria-label="Toggle theme"
|
||||
className="h-auto w-auto rounded-full border border-border bg-background p-2"
|
||||
>
|
||||
{theme === "light" ? (
|
||||
<Sun strokeWidth={1} className="size-5" />
|
||||
<SunIcon className="size-4" />
|
||||
) : (
|
||||
<Moon strokeWidth={1} className="size-5" />
|
||||
<MoonIcon className="size-4" />
|
||||
)}
|
||||
</Button>
|
||||
</ClientOnly>
|
||||
|
||||
@@ -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?:
|
||||
|
||||
@@ -27,6 +27,7 @@ export function DatePickerWithRange(props: {
|
||||
header?: React.ReactNode;
|
||||
footer?: React.ReactNode;
|
||||
labelOverride?: string;
|
||||
popoverAlign?: "start" | "end" | "center";
|
||||
}) {
|
||||
const [screen, setScreen] = React.useState<"from" | "to">("from");
|
||||
const { from, to, setFrom, setTo } = props;
|
||||
@@ -65,7 +66,11 @@ export function DatePickerWithRange(props: {
|
||||
</PopoverTrigger>
|
||||
|
||||
{/* Popover */}
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<PopoverContent
|
||||
className="w-auto p-0"
|
||||
align={props.popoverAlign || "start"}
|
||||
sideOffset={10}
|
||||
>
|
||||
<DynamicHeight>
|
||||
<div>
|
||||
{!isValid && (
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import Link from "next/link";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
type LinkProps = React.ComponentProps<typeof Link>;
|
||||
|
||||
export function UnderlineLink(props: LinkProps) {
|
||||
return (
|
||||
<Link
|
||||
{...props}
|
||||
className={cn(
|
||||
"underline decoration-muted-foreground/50 decoration-dotted underline-offset-[5px] hover:text-foreground hover:decoration-foreground hover:decoration-solid",
|
||||
props.className,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef<
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 bg-black/80 data-[state=closed]:animate-out data-[state=open]:animate-in",
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80 data-[state=closed]:animate-out data-[state=open]:animate-in",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -43,7 +43,7 @@ const DialogContent = React.forwardRef<
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed grid w-full gap-4 rounded-t-xl border border-border bg-background p-6 shadow-lg duration-300 md:max-w-lg md:rounded-lg",
|
||||
"fixed z-50 grid w-full gap-4 rounded-t-xl border border-border bg-background p-6 shadow-lg duration-300 md:max-w-lg md:rounded-lg",
|
||||
// on mobile - put the dialog at the bottom of the screen, animate - slide up and fade in
|
||||
"right-0 bottom-0 left-0",
|
||||
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
||||
|
||||
@@ -4,6 +4,8 @@ export const DASHBOARD_THIRDWEB_CLIENT_ID =
|
||||
export const DASHBOARD_THIRDWEB_SECRET_KEY =
|
||||
process.env.DASHBOARD_SECRET_KEY || "";
|
||||
|
||||
export const THIRDWEB_API_SECRET = process.env.API_SERVER_SECRET || "";
|
||||
|
||||
export const IPFS_GATEWAY_URL =
|
||||
(process.env.NEXT_PUBLIC_IPFS_GATEWAY_URL as string) ||
|
||||
"https://{clientId}.ipfscdn.io/ipfs/{cid}/{path}";
|
||||
|
||||
@@ -117,7 +117,7 @@ function DashboardRouterTopProgressBarInner() {
|
||||
const width = isLoading ? progress : 100;
|
||||
return (
|
||||
<span
|
||||
className="fixed top-0 block h-[2px] bg-blue-500"
|
||||
className="fixed top-0 block h-[3px] bg-foreground"
|
||||
style={{
|
||||
opacity: isLoading ? "100" : "0",
|
||||
width: `${width}%`,
|
||||
|
||||
@@ -95,7 +95,8 @@ export const engineKeys = {
|
||||
backendWallet: (address: string, chainId: number) =>
|
||||
["backendWallet", address, chainId] as const,
|
||||
backendWalletBalance: (address: string, chainId: number) =>
|
||||
[...engineKeys.backendWallet(address, chainId), "balance"] as const,
|
||||
["backendWallet", "balance", address, chainId] as const,
|
||||
backendWalletBalanceAll: () => ["backendWallet", "balance"] as const,
|
||||
corsUrls: (instance: string) =>
|
||||
[...engineKeys.all, instance, "corsUrls"] as const,
|
||||
ipAllowlist: (instance: string) =>
|
||||
@@ -124,4 +125,6 @@ export const engineKeys = {
|
||||
[...engineKeys.all, engineId, "alerts"] as const,
|
||||
notificationChannels: (engineId: string) =>
|
||||
[...engineKeys.all, engineId, "notificationChannels"] as const,
|
||||
walletCredentials: (instance: string) =>
|
||||
[...engineKeys.all, instance, "walletCredentials"] as const,
|
||||
};
|
||||
|
||||
@@ -39,6 +39,7 @@ export type Account = {
|
||||
id: string;
|
||||
isStaff: boolean;
|
||||
creatorWalletAddress: string;
|
||||
image?: string | null;
|
||||
name?: string;
|
||||
email?: string;
|
||||
advancedEnabled: boolean;
|
||||
@@ -548,41 +549,6 @@ export function useResendEmailConfirmation() {
|
||||
});
|
||||
}
|
||||
|
||||
export function useApiKeys(props: {
|
||||
isLoggedIn: boolean;
|
||||
}) {
|
||||
const address = useActiveAccount()?.address;
|
||||
return useQuery({
|
||||
queryKey: apiKeys.keys(address || ""),
|
||||
queryFn: async () => {
|
||||
type Result = {
|
||||
data: ApiKey[];
|
||||
error?: { message: string };
|
||||
};
|
||||
|
||||
const res = await apiServerProxy<Result>({
|
||||
pathname: "/v1/keys",
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
const json = res.data;
|
||||
|
||||
if (json.error) {
|
||||
throw new Error(json.error.message);
|
||||
}
|
||||
return json.data;
|
||||
},
|
||||
enabled: !!address && props.isLoggedIn,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateApiKey() {
|
||||
const address = useActiveAccount()?.address;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -15,6 +15,7 @@ export function useDashboardContractMetadata(contract: ThirdwebContract) {
|
||||
return useQuery({
|
||||
queryKey: ["contract-metadata-header", contract.chain.id, contract.address],
|
||||
queryFn: () => fetchDashboardContractMetadata(contract),
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import type { ResultItem } from "app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/metrics/components/StatusCodes";
|
||||
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";
|
||||
@@ -93,7 +93,8 @@ type EngineFeature =
|
||||
| "CONTRACT_SUBSCRIPTIONS"
|
||||
| "IP_ALLOWLIST"
|
||||
| "HETEROGENEOUS_WALLET_TYPES"
|
||||
| "SMART_BACKEND_WALLETS";
|
||||
| "SMART_BACKEND_WALLETS"
|
||||
| "WALLET_CREDENTIALS";
|
||||
|
||||
interface EngineSystemHealth {
|
||||
status: string;
|
||||
@@ -469,12 +470,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");
|
||||
|
||||
@@ -860,6 +861,10 @@ export type SetWalletConfigInput =
|
||||
gcpKmsKeyRingId: string;
|
||||
gcpApplicationCredentialEmail: string;
|
||||
gcpApplicationCredentialPrivateKey: string;
|
||||
}
|
||||
| {
|
||||
type: "circle";
|
||||
circleApiKey: string;
|
||||
};
|
||||
|
||||
export function useEngineSetWalletConfig(params: {
|
||||
@@ -869,8 +874,8 @@ export function useEngineSetWalletConfig(params: {
|
||||
const { instanceUrl, authToken } = params;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<WalletConfigResponse, void, SetWalletConfigInput>({
|
||||
mutationFn: async (input) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: SetWalletConfigInput) => {
|
||||
invariant(instanceUrl, "instance is required");
|
||||
|
||||
const res = await fetch(`${instanceUrl}configuration/wallets`, {
|
||||
@@ -884,7 +889,7 @@ export function useEngineSetWalletConfig(params: {
|
||||
throw new Error(json.error.message);
|
||||
}
|
||||
|
||||
return json.result;
|
||||
return json.result as WalletConfigResponse;
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
@@ -894,10 +899,17 @@ export function useEngineSetWalletConfig(params: {
|
||||
});
|
||||
}
|
||||
|
||||
export type CreateBackendWalletInput = {
|
||||
type: EngineBackendWalletType;
|
||||
label?: string;
|
||||
};
|
||||
export type CreateBackendWalletInput =
|
||||
| {
|
||||
type: Exclude<EngineBackendWalletType, "circle" | "smart:circle">;
|
||||
label?: string;
|
||||
}
|
||||
| {
|
||||
type: "circle" | "smart:circle";
|
||||
label?: string;
|
||||
credentialId: string;
|
||||
isTestnet: boolean;
|
||||
};
|
||||
|
||||
export function useEngineCreateBackendWallet(params: {
|
||||
instanceUrl: string;
|
||||
@@ -1851,3 +1863,118 @@ export function useEngineDeleteNotificationChannel(engineId: string) {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export interface WalletCredential {
|
||||
id: string;
|
||||
type: string;
|
||||
label: string;
|
||||
isDefault: boolean | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface CreateWalletCredentialInput {
|
||||
type: "circle";
|
||||
label: string;
|
||||
entitySecret?: string;
|
||||
isDefault?: boolean;
|
||||
}
|
||||
|
||||
export function useEngineWalletCredentials(params: {
|
||||
instanceUrl: string;
|
||||
authToken: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}) {
|
||||
const { instanceUrl, authToken, page = 1, limit = 100 } = params;
|
||||
|
||||
return useQuery({
|
||||
queryKey: [...engineKeys.walletCredentials(instanceUrl), page, limit],
|
||||
queryFn: async () => {
|
||||
const res = await fetch(
|
||||
`${instanceUrl}wallet-credentials?page=${page}&limit=${limit}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: getEngineRequestHeaders(authToken),
|
||||
},
|
||||
);
|
||||
|
||||
const json = await res.json();
|
||||
return (json.result as WalletCredential[]) || [];
|
||||
},
|
||||
enabled: !!instanceUrl,
|
||||
});
|
||||
}
|
||||
|
||||
export function useEngineCreateWalletCredential(params: {
|
||||
instanceUrl: string;
|
||||
authToken: string;
|
||||
}) {
|
||||
const { instanceUrl, authToken } = params;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (input: CreateWalletCredentialInput) => {
|
||||
invariant(instanceUrl, "instance is required");
|
||||
|
||||
const res = await fetch(`${instanceUrl}wallet-credentials`, {
|
||||
method: "POST",
|
||||
headers: getEngineRequestHeaders(authToken),
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
if (json.error) {
|
||||
throw new Error(json.error.message);
|
||||
}
|
||||
|
||||
return json.result as WalletCredential;
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.walletCredentials(instanceUrl),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
interface UpdateWalletCredentialInput {
|
||||
label?: string;
|
||||
isDefault?: boolean;
|
||||
entitySecret?: string;
|
||||
}
|
||||
|
||||
export function useEngineUpdateWalletCredential(params: {
|
||||
instanceUrl: string;
|
||||
authToken: string;
|
||||
}) {
|
||||
const { instanceUrl, authToken } = params;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
id,
|
||||
...input
|
||||
}: UpdateWalletCredentialInput & { id: string }) => {
|
||||
invariant(instanceUrl, "instance is required");
|
||||
|
||||
const res = await fetch(`${instanceUrl}wallet-credentials/${id}`, {
|
||||
method: "PUT",
|
||||
headers: getEngineRequestHeaders(authToken),
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
if (json.error) {
|
||||
throw new Error(json.error.message);
|
||||
}
|
||||
|
||||
return json.result as WalletCredential;
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.walletCredentials(instanceUrl),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { addContractToMultiChainRegistry } from "components/contract-components/utils";
|
||||
import { MULTICHAIN_REGISTRY_CONTRACT } from "constants/contracts";
|
||||
import {
|
||||
DASHBOARD_ENGINE_RELAYER_URL,
|
||||
DASHBOARD_FORWARDER_ADDRESS,
|
||||
} from "constants/misc";
|
||||
import type { BasicContract } from "contract-ui/types/types";
|
||||
import { getAllMultichainRegistry } from "dashboard-extensions/common/read/getAllMultichainRegistry";
|
||||
import { useAllChainsData } from "hooks/chains/allChains";
|
||||
import { useMemo } from "react";
|
||||
import { sendAndConfirmTransaction } from "thirdweb";
|
||||
import { remove } from "thirdweb/extensions/thirdweb";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import invariant from "tiny-invariant";
|
||||
|
||||
export function useMultiChainRegContractList(walletAddress?: string) {
|
||||
return useQuery({
|
||||
queryKey: ["dashboard-registry", walletAddress, "multichain-contract-list"],
|
||||
queryFn: async () => {
|
||||
invariant(walletAddress, "walletAddress is required");
|
||||
const contracts = await getAllMultichainRegistry({
|
||||
contract: MULTICHAIN_REGISTRY_CONTRACT,
|
||||
address: walletAddress,
|
||||
});
|
||||
|
||||
return contracts;
|
||||
},
|
||||
|
||||
enabled: !!walletAddress,
|
||||
});
|
||||
}
|
||||
|
||||
interface Options {
|
||||
onlyMainnet?: boolean;
|
||||
}
|
||||
|
||||
export const useAllContractList = (
|
||||
walletAddress: string | undefined,
|
||||
{ onlyMainnet }: Options = { onlyMainnet: false },
|
||||
) => {
|
||||
const multiChainQuery = useMultiChainRegContractList(walletAddress);
|
||||
|
||||
// TODO - instead of using ALL chains, fetch only the ones used here
|
||||
const { idToChain } = useAllChainsData();
|
||||
const contractList = useMemo(() => {
|
||||
const data = multiChainQuery.data || [];
|
||||
|
||||
const mainnets: BasicContract[] = [];
|
||||
const testnets: BasicContract[] = [];
|
||||
|
||||
// biome-ignore lint/complexity/noForEach: FIXME
|
||||
data.forEach((net) => {
|
||||
const chn = idToChain.get(net.chainId);
|
||||
if (chn && chn.status !== "deprecated") {
|
||||
if (chn.testnet) {
|
||||
testnets.push(net);
|
||||
} else {
|
||||
mainnets.push(net);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
mainnets.sort((a, b) => a.chainId - b.chainId);
|
||||
|
||||
if (onlyMainnet) {
|
||||
return mainnets;
|
||||
}
|
||||
|
||||
testnets.sort((a, b) => a.chainId - b.chainId);
|
||||
return mainnets.concat(testnets);
|
||||
}, [multiChainQuery.data, onlyMainnet, idToChain]);
|
||||
|
||||
return {
|
||||
...multiChainQuery,
|
||||
data: contractList,
|
||||
};
|
||||
};
|
||||
|
||||
type RemoveContractParams = {
|
||||
contractAddress: string;
|
||||
chainId: number;
|
||||
};
|
||||
|
||||
export function useRemoveContractMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
const account = useActiveAccount();
|
||||
return useMutation({
|
||||
mutationFn: async (data: RemoveContractParams) => {
|
||||
invariant(data.chainId, "chainId not provided");
|
||||
invariant(account, "No wallet connected");
|
||||
const { contractAddress, chainId } = data;
|
||||
const transaction = remove({
|
||||
contract: MULTICHAIN_REGISTRY_CONTRACT,
|
||||
deployer: account.address,
|
||||
deployment: contractAddress,
|
||||
chainId: BigInt(chainId),
|
||||
});
|
||||
return await sendAndConfirmTransaction({
|
||||
transaction,
|
||||
account,
|
||||
gasless: {
|
||||
experimentalChainlessSupport: true,
|
||||
provider: "engine",
|
||||
relayerUrl: DASHBOARD_ENGINE_RELAYER_URL,
|
||||
relayerForwarderAddress: DASHBOARD_FORWARDER_ADDRESS,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
onSettled: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: ["dashboard-registry"],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
type AddContractParams = {
|
||||
contractAddress: string;
|
||||
chainId: number;
|
||||
};
|
||||
|
||||
export function useAddContractMutation() {
|
||||
const account = useActiveAccount();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (data: AddContractParams) => {
|
||||
invariant(account, "cannot add a contract without an address");
|
||||
|
||||
return await addContractToMultiChainRegistry(
|
||||
{
|
||||
address: data.contractAddress,
|
||||
chainId: data.chainId,
|
||||
},
|
||||
account,
|
||||
300000n,
|
||||
);
|
||||
},
|
||||
|
||||
onSettled: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: ["dashboard-registry"],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,13 +1,32 @@
|
||||
"use client";
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Form, FormControl, FormField, FormItem } from "@/components/ui/form";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
THIRDWEB_ENGINE_FAUCET_WALLET,
|
||||
TURNSTILE_SITE_KEY,
|
||||
} from "@/constants/env";
|
||||
import { useThirdwebClient } from "@/constants/thirdweb.client";
|
||||
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Turnstile } from "@marsidev/react-turnstile";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { CanClaimResponseType } from "app/api/testnet-faucet/can-claim/CanClaimResponseType";
|
||||
@@ -17,9 +36,21 @@ import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { toUnits } from "thirdweb";
|
||||
import {
|
||||
type ThirdwebClient,
|
||||
prepareTransaction,
|
||||
toUnits,
|
||||
toWei,
|
||||
} from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { useActiveAccount, useWalletBalance } from "thirdweb/react";
|
||||
import type { Chain } from "thirdweb/chains";
|
||||
import {
|
||||
useActiveAccount,
|
||||
useActiveWalletChain,
|
||||
useSendTransaction,
|
||||
useSwitchActiveWalletChain,
|
||||
useWalletBalance,
|
||||
} from "thirdweb/react";
|
||||
import { z } from "zod";
|
||||
import { isOnboardingComplete } from "../../../../../../login/onboarding/isOnboardingRequired";
|
||||
|
||||
@@ -137,6 +168,35 @@ export function FaucetButton({
|
||||
|
||||
const form = useForm<z.infer<typeof claimFaucetSchema>>();
|
||||
|
||||
// loading state
|
||||
if (faucetWalletBalanceQuery.isPending || canClaimFaucetQuery.isPending) {
|
||||
return (
|
||||
<Button variant="outline" className="w-full gap-2">
|
||||
Checking Faucet <Spinner className="size-3" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
// faucet is empty
|
||||
if (isFaucetEmpty) {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="mb-3 text-center text-muted-foreground text-sm">
|
||||
Faucet is empty right now
|
||||
</div>
|
||||
<SendFundsToFaucetModalButton
|
||||
chain={definedChain}
|
||||
isLoggedIn={!!twAccount}
|
||||
client={client}
|
||||
chainMeta={chain}
|
||||
onFaucetRefill={() => {
|
||||
faucetWalletBalanceQuery.refetch();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Force users to log in to claim the faucet
|
||||
if (!address || !twAccount) {
|
||||
return (
|
||||
@@ -164,24 +224,6 @@ export function FaucetButton({
|
||||
);
|
||||
}
|
||||
|
||||
// loading state
|
||||
if (faucetWalletBalanceQuery.isPending || canClaimFaucetQuery.isPending) {
|
||||
return (
|
||||
<Button variant="outline" className="w-full gap-2">
|
||||
Checking Faucet <Spinner className="size-3" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
// faucet is empty
|
||||
if (isFaucetEmpty) {
|
||||
return (
|
||||
<Button variant="outline" disabled className="!opacity-100 w-full">
|
||||
Faucet is empty right now
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
// Can not claim
|
||||
if (canClaimFaucetQuery.data && canClaimFaucetQuery.data.canClaim === false) {
|
||||
return (
|
||||
@@ -250,3 +292,165 @@ export function FaucetButton({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const faucetFormSchema = z.object({
|
||||
amount: z.coerce.number().refine((value) => value > 0, {
|
||||
message: "Amount must be greater than 0",
|
||||
}),
|
||||
});
|
||||
|
||||
function SendFundsToFaucetModalButton(props: {
|
||||
chain: Chain;
|
||||
isLoggedIn: boolean;
|
||||
client: ThirdwebClient;
|
||||
chainMeta: ChainMetadata;
|
||||
onFaucetRefill: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="default" className="w-full">
|
||||
Refill Faucet
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader className="mb-2">
|
||||
<DialogTitle>Refill Faucet</DialogTitle>
|
||||
<DialogDescription>Send funds to faucet wallet</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<SendFundsToFaucetModalContent {...props} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function SendFundsToFaucetModalContent(props: {
|
||||
chain: Chain;
|
||||
isLoggedIn: boolean;
|
||||
client: ThirdwebClient;
|
||||
chainMeta: ChainMetadata;
|
||||
onFaucetRefill: () => void;
|
||||
}) {
|
||||
const account = useActiveAccount();
|
||||
const activeChain = useActiveWalletChain();
|
||||
const switchActiveWalletChain = useSwitchActiveWalletChain();
|
||||
const sendTxMutation = useSendTransaction({
|
||||
payModal: false,
|
||||
});
|
||||
const switchChainMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
await switchActiveWalletChain(props.chain);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<z.infer<typeof faucetFormSchema>>({
|
||||
resolver: zodResolver(faucetFormSchema),
|
||||
defaultValues: {
|
||||
amount: 0.1,
|
||||
},
|
||||
});
|
||||
|
||||
function onSubmit(values: z.infer<typeof faucetFormSchema>) {
|
||||
const sendNativeTokenTx = prepareTransaction({
|
||||
chain: props.chain,
|
||||
client: props.client,
|
||||
to: THIRDWEB_ENGINE_FAUCET_WALLET,
|
||||
value: toWei(values.amount.toString()),
|
||||
});
|
||||
|
||||
const promise = sendTxMutation.mutateAsync(sendNativeTokenTx);
|
||||
|
||||
toast.promise(promise, {
|
||||
success: `Sent ${values.amount} ${props.chainMeta.nativeCurrency.symbol} to faucet`,
|
||||
error: `Failed to send ${values.amount} ${props.chainMeta.nativeCurrency.symbol} to faucet`,
|
||||
});
|
||||
|
||||
promise.then(() => {
|
||||
props.onFaucetRefill();
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="flex min-w-0 flex-col gap-5"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<p className="mb-2 text-foreground text-sm"> Faucet Wallet </p>
|
||||
<CopyTextButton
|
||||
copyIconPosition="right"
|
||||
variant="outline"
|
||||
className="w-full justify-between bg-card py-2 font-mono"
|
||||
textToCopy={THIRDWEB_ENGINE_FAUCET_WALLET}
|
||||
textToShow={THIRDWEB_ENGINE_FAUCET_WALLET}
|
||||
tooltip={undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="amount"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Amount</FormLabel>
|
||||
<FormControl>
|
||||
<div className="relative">
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
className="h-auto bg-card text-2xl md:text-2xl [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
|
||||
/>
|
||||
<div className="-translate-y-1/2 absolute top-1/2 right-4 text-muted-foreground text-sm">
|
||||
{props.chainMeta.nativeCurrency.symbol}
|
||||
</div>
|
||||
</div>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{!account && (
|
||||
<CustomConnectWallet
|
||||
chain={props.chain}
|
||||
loginRequired={false}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
connectButtonClassName="!w-full"
|
||||
detailsButtonClassName="!w-full"
|
||||
/>
|
||||
)}
|
||||
|
||||
{account && activeChain && (
|
||||
<div>
|
||||
{activeChain.id === props.chain.id ? (
|
||||
<Button
|
||||
key="submit"
|
||||
type="submit"
|
||||
className="mt-4 w-full gap-2"
|
||||
disabled={sendTxMutation.isPending}
|
||||
>
|
||||
{sendTxMutation.isPending && <Spinner className="size-4" />}
|
||||
Send funds to faucet
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
key="switch"
|
||||
className="mt-4 w-full gap-2"
|
||||
disabled={switchChainMutation.isPending}
|
||||
onClick={() => switchChainMutation.mutate()}
|
||||
>
|
||||
Switch to {props.chainMeta.name}{" "}
|
||||
{switchChainMutation.isPending && (
|
||||
<Spinner className="size-4" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ThirdwebClient, ThirdwebContract } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import type { MinimalTeamsAndProjects } from "../../../../../../components/contract-components/contract-deploy-form/add-to-project-card";
|
||||
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
|
||||
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
|
||||
import { getContractPageSidebarLinks } from "../_utils/getContractPageSidebarLinks";
|
||||
@@ -13,6 +14,8 @@ export function ContractPageLayoutClient(props: {
|
||||
chainMetadata: ChainMetadata;
|
||||
contract: ThirdwebContract;
|
||||
children: React.ReactNode;
|
||||
teamsAndProjects: MinimalTeamsAndProjects | undefined;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
const headerMetadataQuery = useQuery({
|
||||
|
||||
@@ -3,8 +3,9 @@ import type { SidebarLink } from "@/components/blocks/Sidebar";
|
||||
import { SidebarLayout } from "@/components/blocks/SidebarLayout";
|
||||
import type { DashboardContractMetadata } from "@3rdweb-sdk/react/hooks/useDashboardContractMetadata";
|
||||
import { DeprecatedAlert } from "components/shared/DeprecatedAlert";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ThirdwebClient, ThirdwebContract } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import type { MinimalTeamsAndProjects } from "../../../../../../components/contract-components/contract-deploy-form/add-to-project-card";
|
||||
import { ContractMetadata } from "./contract-metadata";
|
||||
import { PrimaryDashboardButton } from "./primary-dashboard-button";
|
||||
|
||||
@@ -14,6 +15,8 @@ export function ContractPageLayout(props: {
|
||||
children: React.ReactNode;
|
||||
sidebarLinks: SidebarLink[];
|
||||
dashboardContractMetadata: DashboardContractMetadata | undefined;
|
||||
client: ThirdwebClient;
|
||||
teamsAndProjects: MinimalTeamsAndProjects | undefined;
|
||||
externalLinks:
|
||||
| {
|
||||
name: string;
|
||||
@@ -27,6 +30,8 @@ export function ContractPageLayout(props: {
|
||||
sidebarLinks,
|
||||
dashboardContractMetadata,
|
||||
externalLinks,
|
||||
teamsAndProjects,
|
||||
client,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
@@ -49,6 +54,8 @@ export function ContractPageLayout(props: {
|
||||
chainSlug: chainMetadata.slug,
|
||||
contractAddress: contract.address,
|
||||
}}
|
||||
teamsAndProjects={teamsAndProjects}
|
||||
client={client}
|
||||
/>
|
||||
</div>
|
||||
<DeprecatedAlert chain={chainMetadata} />
|
||||
|
||||
@@ -1,19 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { UnderlineLink } from "@/components/ui/UnderlineLink";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { EVMContractInfo } from "@3rdweb-sdk/react";
|
||||
import {
|
||||
useAddContractMutation,
|
||||
useAllContractList,
|
||||
} from "@3rdweb-sdk/react/hooks/useRegistry";
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import type { EVMContractInfo } from "@3rdweb-sdk/react";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
import { CodeIcon, PlusIcon } from "lucide-react";
|
||||
import { CircleAlertIcon, ExternalLinkIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import type { Chain } from "thirdweb";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import type { Chain, ThirdwebClient } from "thirdweb";
|
||||
import {
|
||||
AddToProjectSelector,
|
||||
type MinimalTeamsAndProjects,
|
||||
} from "../../../../../../components/contract-components/contract-deploy-form/add-to-project-card";
|
||||
import { useAddContractToProject } from "../../../../../team/[team_slug]/[project_slug]/hooks/project-contracts";
|
||||
|
||||
const TRACKING_CATEGORY = "add_to_dashboard_upsell";
|
||||
|
||||
@@ -22,6 +34,8 @@ type AddToDashboardCardProps = {
|
||||
chain: Chain;
|
||||
contractInfo: EVMContractInfo;
|
||||
hideCodePageLink?: boolean;
|
||||
teamsAndProjects: MinimalTeamsAndProjects | undefined;
|
||||
client: ThirdwebClient;
|
||||
};
|
||||
|
||||
export const PrimaryDashboardButton: React.FC<AddToDashboardCardProps> = ({
|
||||
@@ -29,40 +43,13 @@ export const PrimaryDashboardButton: React.FC<AddToDashboardCardProps> = ({
|
||||
chain,
|
||||
contractInfo,
|
||||
hideCodePageLink,
|
||||
teamsAndProjects,
|
||||
client,
|
||||
}) => {
|
||||
const addContract = useAddContractMutation();
|
||||
const trackEvent = useTrack();
|
||||
const walletAddress = useActiveAccount()?.address;
|
||||
const pathname = usePathname();
|
||||
const registry = useAllContractList(walletAddress);
|
||||
|
||||
const { onSuccess: onAddSuccess, onError: onAddError } = useTxNotifications(
|
||||
"Successfully imported",
|
||||
"Failed to import",
|
||||
);
|
||||
|
||||
const isInRegistry =
|
||||
registry.isFetched &&
|
||||
registry.data?.find(
|
||||
(c) =>
|
||||
contractAddress &&
|
||||
// compare address...
|
||||
c.address.toLowerCase() === contractAddress.toLowerCase() &&
|
||||
// ... and chainId
|
||||
c.chainId === chain.id,
|
||||
) &&
|
||||
registry.isSuccess;
|
||||
|
||||
if (
|
||||
!walletAddress ||
|
||||
!contractAddress ||
|
||||
!chain ||
|
||||
pathname?.includes("payments")
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isInRegistry) {
|
||||
// if user is not logged in
|
||||
if (!teamsAndProjects) {
|
||||
if (hideCodePageLink) {
|
||||
return null;
|
||||
}
|
||||
@@ -84,54 +71,163 @@ export const PrimaryDashboardButton: React.FC<AddToDashboardCardProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
className="gap-2"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
if (!chain) {
|
||||
return;
|
||||
}
|
||||
trackEvent({
|
||||
category: TRACKING_CATEGORY,
|
||||
action: "add-to-dashboard",
|
||||
label: "attempt",
|
||||
contractAddress,
|
||||
});
|
||||
addContract.mutate(
|
||||
{
|
||||
contractAddress,
|
||||
chainId: chain.id,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
onAddSuccess();
|
||||
trackEvent({
|
||||
category: TRACKING_CATEGORY,
|
||||
action: "add-to-dashboard",
|
||||
label: "success",
|
||||
contractAddress,
|
||||
});
|
||||
},
|
||||
onError: (err) => {
|
||||
onAddError(err);
|
||||
trackEvent({
|
||||
category: TRACKING_CATEGORY,
|
||||
action: "add-to-dashboard",
|
||||
label: "error",
|
||||
contractAddress,
|
||||
error: err,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
}}
|
||||
>
|
||||
{addContract.isPending ? (
|
||||
<Spinner className="size-4" />
|
||||
) : (
|
||||
<PlusIcon className="size-4" />
|
||||
)}
|
||||
Import contract
|
||||
</Button>
|
||||
<AddToProjectButton
|
||||
contractAddress={contractAddress}
|
||||
teamsAndProjects={teamsAndProjects}
|
||||
chainId={chain.id.toString()}
|
||||
client={client}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
function AddToProjectButton(props: {
|
||||
contractAddress: string;
|
||||
teamsAndProjects: MinimalTeamsAndProjects;
|
||||
chainId: string;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="gap-2" variant="outline">
|
||||
<PlusIcon className="size-4" />
|
||||
Add To Project
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent className="gap-0 overflow-hidden p-0">
|
||||
<DialogHeader className="p-6">
|
||||
<DialogTitle>Add To Project</DialogTitle>
|
||||
<DialogDescription>
|
||||
Add the contract in a project's contract list on dashboard
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<AddToProjectModalContent
|
||||
teamsAndProjects={props.teamsAndProjects}
|
||||
chainId={props.chainId}
|
||||
contractAddress={props.contractAddress}
|
||||
client={props.client}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function AddToProjectModalContent(props: {
|
||||
teamsAndProjects: MinimalTeamsAndProjects;
|
||||
chainId: string;
|
||||
contractAddress: string;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
const trackEvent = useTrack();
|
||||
const addContractToProject = useAddContractToProject();
|
||||
|
||||
const [importSelection, setImportSelection] = useState({
|
||||
team: props.teamsAndProjects[0]?.team,
|
||||
project: props.teamsAndProjects[0]?.projects[0],
|
||||
});
|
||||
|
||||
const selectedTeam = props.teamsAndProjects.find(
|
||||
(t) => t.team.id === importSelection.team?.id,
|
||||
);
|
||||
|
||||
function handleImport(params: {
|
||||
teamId: string;
|
||||
projectId: string;
|
||||
}) {
|
||||
trackEvent({
|
||||
category: TRACKING_CATEGORY,
|
||||
action: "add-to-dashboard",
|
||||
label: "attempt",
|
||||
contractAddress: props.contractAddress,
|
||||
});
|
||||
addContractToProject.mutate(
|
||||
{
|
||||
contractAddress: props.contractAddress,
|
||||
teamId: params.teamId,
|
||||
projectId: params.projectId,
|
||||
chainId: props.chainId,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success("Contract added to the project successfully");
|
||||
trackEvent({
|
||||
category: TRACKING_CATEGORY,
|
||||
action: "add-to-dashboard",
|
||||
label: "success",
|
||||
contractAddress: props.contractAddress,
|
||||
});
|
||||
},
|
||||
onError: (err) => {
|
||||
if (err.message.includes("PROJECT_CONTRACT_ALREADY_EXISTS")) {
|
||||
toast.error("Contract is already added to the project");
|
||||
} else {
|
||||
toast.error("Failed to import contract");
|
||||
}
|
||||
trackEvent({
|
||||
category: TRACKING_CATEGORY,
|
||||
action: "add-to-dashboard",
|
||||
label: "error",
|
||||
contractAddress: props.contractAddress,
|
||||
error: err,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const isImportEnabled = !!importSelection.project && !!importSelection.team;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col gap-4 border-border border-t px-6 py-6">
|
||||
<AddToProjectSelector
|
||||
selection={importSelection}
|
||||
onSelectionChange={setImportSelection}
|
||||
teamsAndProjects={props.teamsAndProjects}
|
||||
client={props.client}
|
||||
/>
|
||||
|
||||
{/* No projects alert */}
|
||||
{selectedTeam?.projects.length === 0 && (
|
||||
<Alert variant="info">
|
||||
<CircleAlertIcon className="size-5" />
|
||||
<AlertTitle> Selected team has no projects </AlertTitle>
|
||||
<AlertDescription>
|
||||
<UnderlineLink
|
||||
href={`/team/${selectedTeam.team.slug}`}
|
||||
className="inline-flex items-center gap-2"
|
||||
target="_blank"
|
||||
>
|
||||
Create Project <ExternalLinkIcon className="size-3.5" />
|
||||
</UnderlineLink>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-3 flex justify-end border-border border-t bg-card p-6">
|
||||
<Button
|
||||
disabled={!isImportEnabled}
|
||||
className="gap-2"
|
||||
onClick={() => {
|
||||
if (!importSelection.team || !importSelection.project) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleImport({
|
||||
teamId: importSelection.team.id,
|
||||
projectId: importSelection.project.id,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{addContractToProject.isPending ? (
|
||||
<Spinner className="size-4" />
|
||||
) : (
|
||||
<PlusIcon className="size-4" />
|
||||
)}
|
||||
Add to Project
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,11 @@ export function getContractPageSidebarLinks(data: {
|
||||
hide: !data.metadata.isModularCore,
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
label: "Cross Chain (Beta)",
|
||||
href: `${layoutPrefix}/cross-chain`,
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
label: "Code Snippets",
|
||||
href: `${layoutPrefix}/code`,
|
||||
@@ -39,12 +44,6 @@ export function getContractPageSidebarLinks(data: {
|
||||
href: `${layoutPrefix}/events`,
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
label: "Embed",
|
||||
href: `${layoutPrefix}/embed`,
|
||||
hide: data.metadata.embedType === null,
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
label: "Analytics",
|
||||
href: `${layoutPrefix}/analytics`,
|
||||
|
||||
@@ -126,8 +126,11 @@ function UniqueWalletsChart(props: ChartProps) {
|
||||
|
||||
return (
|
||||
<ThirdwebBarChart
|
||||
title="Unique Wallets"
|
||||
description="The number of unique wallet addresses that have sent a transaction to this contract."
|
||||
header={{
|
||||
title: "Unique Wallets",
|
||||
description:
|
||||
"The number of unique wallet addresses that have sent a transaction to this contract.",
|
||||
}}
|
||||
data={analyticsQuery.data || []}
|
||||
isPending={analyticsQuery.isPending}
|
||||
config={{
|
||||
@@ -151,8 +154,11 @@ function TotalTransactionsChart(props: ChartProps) {
|
||||
|
||||
return (
|
||||
<ThirdwebBarChart
|
||||
title="Total Transactions"
|
||||
description="The number of transactions that have been sent to this contract."
|
||||
header={{
|
||||
title: "Total Transactions",
|
||||
description:
|
||||
"The number of transactions that have been sent to this contract.",
|
||||
}}
|
||||
data={analyticsQuery.data || []}
|
||||
isPending={analyticsQuery.isPending}
|
||||
config={{
|
||||
@@ -171,8 +177,11 @@ function TotalEventsChart(props: ChartProps) {
|
||||
|
||||
return (
|
||||
<ThirdwebBarChart
|
||||
title="Total Events"
|
||||
description="The number of on-chain events that have been emitted from this contract."
|
||||
header={{
|
||||
title: "Total Events",
|
||||
description:
|
||||
"The number of on-chain events that have been emitted from this contract.",
|
||||
}}
|
||||
data={analyticsQuery.data || []}
|
||||
isPending={analyticsQuery.isPending}
|
||||
config={{
|
||||
@@ -216,8 +225,11 @@ function FunctionBreakdownChart(
|
||||
|
||||
return (
|
||||
<ThirdwebBarChart
|
||||
title="Function Breakdown"
|
||||
description="The breakdown of calls to each write function from transactions."
|
||||
header={{
|
||||
title: "Function Breakdown",
|
||||
description:
|
||||
"The breakdown of calls to each write function from transactions.",
|
||||
}}
|
||||
data={mappedQueryData || []}
|
||||
isPending={analyticsQuery.isPending}
|
||||
config={Object.keys(mappedQueryData?.[0] || {}).reduce(
|
||||
@@ -269,8 +281,10 @@ function EventBreakdownChart(
|
||||
|
||||
return (
|
||||
<ThirdwebBarChart
|
||||
title="Event Breakdown"
|
||||
description="The breakdown of events emitted by this contract."
|
||||
header={{
|
||||
title: "Event Breakdown",
|
||||
description: "The breakdown of events emitted by this contract.",
|
||||
}}
|
||||
data={mappedQueryData || []}
|
||||
isPending={analyticsQuery.isPending}
|
||||
config={Object.keys(mappedQueryData?.[0] || {}).reduce(
|
||||
@@ -319,7 +333,7 @@ const AnalyticsStat: React.FC<AnalyticsStatProps> = ({
|
||||
|
||||
const AnalyticsSkeleton: React.FC<{ label: string }> = ({ label }) => {
|
||||
return (
|
||||
<Card as={Stat}>
|
||||
<Card as={Stat} className="bg-card">
|
||||
<StatLabel mb={{ base: 1, md: 0 }}>{label}</StatLabel>
|
||||
<Skeleton isLoaded={false}>
|
||||
<StatNumber>{0}</StatNumber>
|
||||
@@ -350,7 +364,7 @@ const AnalyticsData: React.FC<AnalyticsStatProps> = ({
|
||||
}, [totalQuery.data]);
|
||||
|
||||
return (
|
||||
<Card as={Stat}>
|
||||
<Card as={Stat} className="bg-card">
|
||||
<StatLabel mb={{ base: 1, md: 0 }}>{label}</StatLabel>
|
||||
<Skeleton isLoaded={totalQuery.isFetched}>
|
||||
<StatNumber>{data.toLocaleString()}</StatNumber>
|
||||
|
||||
@@ -0,0 +1,389 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import {
|
||||
type ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import { verifyContract } from "app/(dashboard)/(chain)/[chain_id]/[contractAddress]/sources/ContractSourcesPage";
|
||||
import {
|
||||
type DeployModalStep,
|
||||
DeployStatusModal,
|
||||
useDeployStatusModal,
|
||||
} from "components/contract-components/contract-deploy-form/deploy-context-modal";
|
||||
import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import {
|
||||
defineChain,
|
||||
eth_getCode,
|
||||
getRpcClient,
|
||||
prepareTransaction,
|
||||
sendAndConfirmTransaction,
|
||||
} from "thirdweb";
|
||||
import type {
|
||||
FetchDeployMetadataResult,
|
||||
ThirdwebContract,
|
||||
} from "thirdweb/contract";
|
||||
import {
|
||||
deployContractfromDeployMetadata,
|
||||
getOrDeployInfraForPublishedContract,
|
||||
} from "thirdweb/deploys";
|
||||
import { useActiveAccount, useSwitchActiveWalletChain } from "thirdweb/react";
|
||||
import { concatHex, padHex } from "thirdweb/utils";
|
||||
import { z } from "zod";
|
||||
import { SingleNetworkSelector } from "./single-network-selector";
|
||||
|
||||
type CrossChain = {
|
||||
id: number;
|
||||
network: string;
|
||||
chainId: number;
|
||||
status: "DEPLOYED" | "NOT_DEPLOYED";
|
||||
};
|
||||
|
||||
const formSchema = z.object({
|
||||
amounts: z.object({
|
||||
"84532": z.string(),
|
||||
"11155420": z.string(),
|
||||
"919": z.string(),
|
||||
"111557560": z.string(),
|
||||
"999999999": z.string(),
|
||||
"11155111": z.string(),
|
||||
"421614": z.string(),
|
||||
}),
|
||||
});
|
||||
type FormSchema = z.output<typeof formSchema>;
|
||||
|
||||
export function DataTable({
|
||||
data,
|
||||
coreMetadata,
|
||||
coreContract,
|
||||
modulesMetadata,
|
||||
initializeData,
|
||||
inputSalt,
|
||||
initCode,
|
||||
isDirectDeploy,
|
||||
}: {
|
||||
data: CrossChain[];
|
||||
coreMetadata: FetchDeployMetadataResult;
|
||||
coreContract: ThirdwebContract;
|
||||
modulesMetadata?: FetchDeployMetadataResult[];
|
||||
initializeData?: `0x${string}`;
|
||||
inputSalt?: `0x${string}`;
|
||||
initCode?: `0x${string}`;
|
||||
isDirectDeploy: boolean;
|
||||
}) {
|
||||
const activeAccount = useActiveAccount();
|
||||
const switchChain = useSwitchActiveWalletChain();
|
||||
const deployStatusModal = useDeployStatusModal();
|
||||
const { onError } = useTxNotifications(
|
||||
"Successfully deployed contract",
|
||||
"Failed to deploy contract",
|
||||
);
|
||||
|
||||
const addRowMutation = useMutation({
|
||||
mutationFn: async (chain: { chainId: number; name: string }) => {
|
||||
const code = await eth_getCode(
|
||||
getRpcClient({
|
||||
client: getThirdwebClient(),
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
chain: defineChain(chain.chainId),
|
||||
}),
|
||||
{ address: coreContract.address },
|
||||
);
|
||||
|
||||
const newRow: CrossChain = {
|
||||
id: chain.chainId,
|
||||
network: chain.name,
|
||||
chainId: chain.chainId,
|
||||
status: code?.length > 2 ? "DEPLOYED" : "NOT_DEPLOYED",
|
||||
};
|
||||
|
||||
if (!customChainData.some((row) => row.chainId === chain.chainId)) {
|
||||
setCustomChainData((prevData) => [...prevData, newRow]);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const [customChainData, setCustomChainData] = useState<CrossChain[]>(() => {
|
||||
try {
|
||||
const storedData = window.localStorage.getItem(
|
||||
`crosschain-${coreContract.address}`,
|
||||
);
|
||||
return storedData ? JSON.parse(storedData) : [];
|
||||
} catch (error) {
|
||||
console.error("Failed to read from localStorage", error);
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
try {
|
||||
window.localStorage.setItem(
|
||||
`crosschain-${coreContract.address}`,
|
||||
JSON.stringify(customChainData),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to write to localStorage", error);
|
||||
}
|
||||
}, [customChainData, coreContract.address]);
|
||||
|
||||
const mergedChainData = useMemo(() => {
|
||||
const chainMap = new Map<number, CrossChain>();
|
||||
for (const item of [...data, ...customChainData]) {
|
||||
chainMap.set(item.chainId, item); // Deduplicate by chainId
|
||||
}
|
||||
return Array.from(chainMap.values());
|
||||
}, [data, customChainData]);
|
||||
|
||||
const form = useForm<FormSchema>({
|
||||
resolver: zodResolver(formSchema),
|
||||
values: {
|
||||
amounts: {
|
||||
"84532": "", // Base
|
||||
"11155420": "", // OP testnet
|
||||
"919": "", // Mode Network
|
||||
"111557560": "", // Cyber
|
||||
"999999999": "", // Zora
|
||||
"11155111": "", // Sepolia
|
||||
"421614": "",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const columns: ColumnDef<CrossChain>[] = [
|
||||
{
|
||||
accessorKey: "network",
|
||||
header: "Network",
|
||||
cell: ({ row }) => {
|
||||
if (row.getValue("status") === "DEPLOYED") {
|
||||
return (
|
||||
<Link
|
||||
target="_blank"
|
||||
className="text-blue-500 underline"
|
||||
href={`/${row.getValue("chainId")}/${coreContract.address}`}
|
||||
>
|
||||
{row.getValue("network")}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
return row.getValue("network");
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "chainId",
|
||||
header: "Chain ID",
|
||||
},
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: "Status",
|
||||
cell: ({ row }) => {
|
||||
if (row.getValue("status") === "DEPLOYED") {
|
||||
return <p>Deployed</p>;
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => deployContract(row.getValue("chainId"))}
|
||||
>
|
||||
Deploy
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
data: mergedChainData,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
const deployContract = async (chainId: number) => {
|
||||
try {
|
||||
if (!activeAccount) {
|
||||
throw new Error("No active account");
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const chain = defineChain(chainId);
|
||||
const client = getThirdwebClient();
|
||||
const salt =
|
||||
inputSalt || concatHex(["0x03", padHex("0x", { size: 31 })]).toString();
|
||||
|
||||
await switchChain(chain);
|
||||
|
||||
const steps: DeployModalStep[] = [
|
||||
{
|
||||
type: "deploy",
|
||||
signatureCount: 1,
|
||||
},
|
||||
];
|
||||
|
||||
deployStatusModal.setViewContractLink("");
|
||||
deployStatusModal.open(steps);
|
||||
|
||||
let crosschainContractAddress: string | undefined;
|
||||
if (initCode && isDirectDeploy) {
|
||||
const tx = prepareTransaction({
|
||||
client,
|
||||
chain,
|
||||
to: "0x4e59b44847b379578588920cA78FbF26c0B4956C",
|
||||
data: initCode,
|
||||
});
|
||||
|
||||
await sendAndConfirmTransaction({
|
||||
transaction: tx,
|
||||
account: activeAccount,
|
||||
});
|
||||
|
||||
const code = await eth_getCode(
|
||||
getRpcClient({
|
||||
client,
|
||||
chain,
|
||||
}),
|
||||
{
|
||||
address: coreContract.address,
|
||||
},
|
||||
);
|
||||
|
||||
if (code && code.length > 2) {
|
||||
crosschainContractAddress = coreContract.address;
|
||||
}
|
||||
} else {
|
||||
crosschainContractAddress = await deployContractfromDeployMetadata({
|
||||
account: activeAccount,
|
||||
chain,
|
||||
client,
|
||||
deployMetadata: coreMetadata,
|
||||
isCrosschain: true,
|
||||
initializeData,
|
||||
salt,
|
||||
});
|
||||
|
||||
verifyContract({
|
||||
address: crosschainContractAddress as `0x${string}`,
|
||||
chain,
|
||||
client,
|
||||
});
|
||||
if (modulesMetadata) {
|
||||
for (const m of modulesMetadata) {
|
||||
await getOrDeployInfraForPublishedContract({
|
||||
chain,
|
||||
client,
|
||||
account: activeAccount,
|
||||
contractId: m.name,
|
||||
publisher: m.publisher,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
deployStatusModal.nextStep();
|
||||
deployStatusModal.setViewContractLink(
|
||||
`/${chain.id}/${crosschainContractAddress}`,
|
||||
);
|
||||
deployStatusModal.close();
|
||||
|
||||
setCustomChainData((prevData) =>
|
||||
prevData.map((row) =>
|
||||
row.chainId === chainId ? { ...row, status: "DEPLOYED" } : row,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
onError(e);
|
||||
console.error("failed to deploy contract", e);
|
||||
deployStatusModal.close();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form>
|
||||
<div
|
||||
style={{
|
||||
maxHeight: "500px",
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext(),
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<DeployStatusModal deployStatusModal={deployStatusModal} />
|
||||
</TableContainer>
|
||||
</div>
|
||||
|
||||
{addRowMutation.isPending && (
|
||||
<div className="my-4 animate-pulse text-center text-blue-500">
|
||||
Processing...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{addRowMutation.isError && (
|
||||
<div className="my-4 text-center text-red-500">
|
||||
Error while adding selected chain!
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4">
|
||||
<SingleNetworkSelector
|
||||
onAddRow={addRowMutation.mutate}
|
||||
isAddingRow={addRowMutation.isPending}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import Link from "next/link";
|
||||
|
||||
export function NoCrossChainPrompt() {
|
||||
return (
|
||||
<div>
|
||||
Multi-chain deployments are not available for this contract (or chain).
|
||||
<br />
|
||||
Please ensure the chain is supported or deploy a new contract to enable
|
||||
this functionality.
|
||||
<br />
|
||||
<br />
|
||||
<Link href="/explore" target="_blank" className="text-blue-500 underline">
|
||||
Explore contracts
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
import { fetchPublishedContractsFromDeploy } from "components/contract-components/fetchPublishedContractsFromDeploy";
|
||||
import { notFound } from "next/navigation";
|
||||
import {
|
||||
type ContractOptions,
|
||||
eth_getTransactionByHash,
|
||||
eth_getTransactionReceipt,
|
||||
getContractEvents,
|
||||
parseEventLogs,
|
||||
prepareEvent,
|
||||
} from "thirdweb";
|
||||
import { defineChain, getChainMetadata } from "thirdweb/chains";
|
||||
import {
|
||||
type FetchDeployMetadataResult,
|
||||
getContract,
|
||||
getDeployedCloneFactoryContract,
|
||||
resolveContractAbi,
|
||||
} from "thirdweb/contract";
|
||||
import { moduleInstalledEvent } from "thirdweb/modules";
|
||||
import { eth_getCode, getRpcClient } from "thirdweb/rpc";
|
||||
import type { TransactionReceipt } from "thirdweb/transaction";
|
||||
import { type AbiFunction, decodeFunctionData } from "thirdweb/utils";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { DataTable } from "./data-table";
|
||||
import { NoCrossChainPrompt } from "./no-crosschain-prompt";
|
||||
|
||||
export function getModuleInstallParams(mod: FetchDeployMetadataResult) {
|
||||
return (
|
||||
mod.abi
|
||||
.filter((a) => a.type === "function")
|
||||
.find((f) => f.name === "encodeBytesOnInstall")?.inputs || []
|
||||
);
|
||||
}
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
}>;
|
||||
}) {
|
||||
const ProxyDeployedEvent = prepareEvent({
|
||||
signature:
|
||||
"event ProxyDeployedV2(address indexed implementation, address indexed proxy, address indexed deployer, bytes32 inputSalt, bytes data, bytes extraData)",
|
||||
});
|
||||
|
||||
const params = await props.params;
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { contract } = info;
|
||||
|
||||
const isModularCore = (await getContractPageMetadata(contract)).isModularCore;
|
||||
|
||||
let twCloneFactoryContract: Readonly<
|
||||
ContractOptions<[], `0x${string}`>
|
||||
> | null = null;
|
||||
try {
|
||||
twCloneFactoryContract = await getDeployedCloneFactoryContract({
|
||||
chain: contract.chain,
|
||||
client: contract.client,
|
||||
});
|
||||
} catch {}
|
||||
|
||||
const originalCode = await eth_getCode(
|
||||
getRpcClient({
|
||||
client: contract.client,
|
||||
chain: contract.chain,
|
||||
}),
|
||||
{
|
||||
address: contract.address,
|
||||
},
|
||||
);
|
||||
|
||||
let initCode: `0x${string}` = "0x";
|
||||
let creationTxReceipt: TransactionReceipt | undefined;
|
||||
let isDirectDeploy = false;
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://contract.thirdweb-dev.com/creation/${contract.chain.id}/${contract.address}`,
|
||||
);
|
||||
const creationData = await res.json();
|
||||
|
||||
if (creationData.status === "1" && creationData.result[0]?.txHash) {
|
||||
const rpcClient = getRpcClient({
|
||||
client: contract.client,
|
||||
chain: contract.chain,
|
||||
});
|
||||
creationTxReceipt = await eth_getTransactionReceipt(rpcClient, {
|
||||
hash: creationData.result[0]?.txHash,
|
||||
});
|
||||
|
||||
const creationTx = await eth_getTransactionByHash(rpcClient, {
|
||||
hash: creationData.result[0]?.txHash,
|
||||
});
|
||||
|
||||
initCode = creationTx.input;
|
||||
isDirectDeploy =
|
||||
creationTx.to?.toLowerCase() ===
|
||||
"0x4e59b44847b379578588920cA78FbF26c0B4956C".toLowerCase();
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug(e);
|
||||
}
|
||||
|
||||
let initializeData: `0x${string}` | undefined;
|
||||
let inputSalt: `0x${string}` | undefined;
|
||||
let creationBlockNumber: bigint | undefined;
|
||||
|
||||
if (twCloneFactoryContract) {
|
||||
const events = await getContractEvents({
|
||||
contract: twCloneFactoryContract,
|
||||
events: [ProxyDeployedEvent],
|
||||
blockRange: 123456n,
|
||||
});
|
||||
const event = events.find(
|
||||
(e) =>
|
||||
e.args.proxy.toLowerCase() === params.contractAddress.toLowerCase(),
|
||||
);
|
||||
|
||||
initializeData = event?.args.data;
|
||||
inputSalt = event?.args.inputSalt;
|
||||
creationBlockNumber = event?.blockNumber;
|
||||
}
|
||||
|
||||
const defaultChains = [
|
||||
1, 137, 8453, 10, 42161, 11155111, 84532, 11155420, 421614,
|
||||
];
|
||||
|
||||
const chainsDeployedOn = (
|
||||
await Promise.all(
|
||||
defaultChains.map(async (c) => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const chain = defineChain(c);
|
||||
|
||||
try {
|
||||
const chainMetadata = await getChainMetadata(chain);
|
||||
|
||||
const rpcRequest = getRpcClient({
|
||||
client: contract.client,
|
||||
chain,
|
||||
});
|
||||
const code = await eth_getCode(rpcRequest, {
|
||||
address: params.contractAddress,
|
||||
});
|
||||
|
||||
return {
|
||||
id: chain.id,
|
||||
network: chainMetadata.name,
|
||||
chainId: chain.id,
|
||||
status:
|
||||
code === originalCode
|
||||
? ("DEPLOYED" as const)
|
||||
: ("NOT_DEPLOYED" as const),
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
id: chain.id,
|
||||
network: "",
|
||||
chainId: chain.id,
|
||||
status: "NOT_DEPLOYED" as const,
|
||||
};
|
||||
}
|
||||
}),
|
||||
)
|
||||
).filter((c) => c.status === "DEPLOYED");
|
||||
|
||||
let coreMetadata: FetchDeployMetadataResult | undefined;
|
||||
try {
|
||||
coreMetadata = (
|
||||
await fetchPublishedContractsFromDeploy({
|
||||
contract,
|
||||
client: contract.client,
|
||||
})
|
||||
).at(-1) as FetchDeployMetadataResult;
|
||||
} catch {}
|
||||
|
||||
if (!coreMetadata) {
|
||||
return NoCrossChainPrompt();
|
||||
}
|
||||
|
||||
let modulesMetadata: FetchDeployMetadataResult[] | undefined;
|
||||
|
||||
if (isModularCore) {
|
||||
let modules: string[] | undefined;
|
||||
const moduleEvent = moduleInstalledEvent();
|
||||
// extract module address in ModuleInstalled events from transaction receipt
|
||||
if (creationTxReceipt) {
|
||||
const decodedEvent = parseEventLogs({
|
||||
events: [moduleEvent],
|
||||
logs: creationTxReceipt.logs,
|
||||
});
|
||||
|
||||
modules = decodedEvent.map((e) => e.args.installedModule);
|
||||
}
|
||||
|
||||
// fetch events from contract
|
||||
if (!modules && creationBlockNumber) {
|
||||
const events = await getContractEvents({
|
||||
contract: contract,
|
||||
events: [moduleEvent],
|
||||
blockRange: 123456n,
|
||||
});
|
||||
|
||||
const filteredEvents = events.filter(
|
||||
(e) => e.blockNumber === creationBlockNumber,
|
||||
);
|
||||
|
||||
modules = filteredEvents.map((e) => e.args.installedModule);
|
||||
}
|
||||
|
||||
// if receipt not available, try extracting module address from initialize data
|
||||
if (!modules && initializeData) {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
const decodedData: any = await decodeFunctionData({
|
||||
contract,
|
||||
data: initializeData,
|
||||
});
|
||||
|
||||
const abi = await resolveContractAbi(contract).catch(() => undefined);
|
||||
|
||||
if (abi) {
|
||||
const initializeFunction = abi.find(
|
||||
(i: AbiFunction) => i.type === "function" && i.name === "initialize",
|
||||
) as unknown as AbiFunction;
|
||||
|
||||
const moduleIndex = initializeFunction.inputs.findIndex(
|
||||
(i) => i.name === "_modules" || i.name === "modules",
|
||||
);
|
||||
|
||||
modules = moduleIndex ? decodedData[moduleIndex] : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
modulesMetadata = modules
|
||||
? ((await Promise.all(
|
||||
modules.map(async (m) =>
|
||||
(
|
||||
await fetchPublishedContractsFromDeploy({
|
||||
contract: getContract({
|
||||
chain: contract.chain,
|
||||
client: contract.client,
|
||||
address: m,
|
||||
}),
|
||||
client: contract.client,
|
||||
})
|
||||
).at(-1),
|
||||
),
|
||||
)) as FetchDeployMetadataResult[])
|
||||
: undefined;
|
||||
}
|
||||
|
||||
if (!isDirectDeploy && !initializeData) {
|
||||
return NoCrossChainPrompt();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<h2 className="mb-1 font-bold text-2xl tracking-tight">
|
||||
Deploy Cross-chain
|
||||
</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Deterministically deploy your contracts on multiple networks.
|
||||
</p>
|
||||
</div>
|
||||
<div className="h-10" />
|
||||
<DataTable
|
||||
coreMetadata={coreMetadata}
|
||||
modulesMetadata={modulesMetadata}
|
||||
initializeData={initializeData}
|
||||
inputSalt={inputSalt}
|
||||
data={chainsDeployedOn}
|
||||
coreContract={contract}
|
||||
initCode={initCode}
|
||||
isDirectDeploy={isDirectDeploy}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { SelectWithSearch } from "@/components/blocks/select-with-search";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { ChainIcon } from "components/icons/ChainIcon";
|
||||
import { useAllChainsData } from "hooks/chains/allChains";
|
||||
import { useCallback, useMemo } from "react";
|
||||
|
||||
type Option = { label: string; value: string };
|
||||
|
||||
export function SingleNetworkSelector(props: {
|
||||
onAddRow: (chain: { chainId: number; name: string }) => void;
|
||||
isAddingRow: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
const { allChains, idToChain } = useAllChainsData();
|
||||
|
||||
const options = useMemo(() => {
|
||||
return allChains.map((chain) => ({
|
||||
label: chain.name,
|
||||
value: String(chain.chainId),
|
||||
}));
|
||||
}, [allChains]);
|
||||
|
||||
const searchFn = useCallback(
|
||||
(option: Option, searchValue: string) => {
|
||||
const chain = idToChain.get(Number(option.value));
|
||||
if (!chain) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Number.isInteger(Number.parseInt(searchValue))) {
|
||||
return String(chain.chainId).startsWith(searchValue);
|
||||
}
|
||||
return chain.name.toLowerCase().includes(searchValue.toLowerCase());
|
||||
},
|
||||
[idToChain],
|
||||
);
|
||||
|
||||
const renderOption = useCallback(
|
||||
(option: Option) => {
|
||||
const chain = idToChain.get(Number(option.value));
|
||||
if (!chain) {
|
||||
return option.label;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-between gap-4">
|
||||
<span className="flex grow gap-2 truncate text-left">
|
||||
<ChainIcon
|
||||
className="size-5"
|
||||
ipfsSrc={chain.icon?.url}
|
||||
loading="lazy"
|
||||
/>
|
||||
{chain.name}
|
||||
</span>
|
||||
<Badge variant="outline" className="gap-2 max-sm:hidden">
|
||||
<span className="text-muted-foreground">Chain ID</span>
|
||||
{chain.chainId}
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[idToChain],
|
||||
);
|
||||
|
||||
const handleChange = async (chainId: string) => {
|
||||
const chain = idToChain.get(Number(chainId));
|
||||
if (chain) {
|
||||
props.onAddRow({ chainId: chain.chainId, name: chain.name });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SelectWithSearch
|
||||
searchPlaceholder="Search by Name or Chain ID"
|
||||
value={undefined}
|
||||
options={options}
|
||||
onValueChange={handleChange}
|
||||
placeholder={
|
||||
allChains.length === 0
|
||||
? "Loading chains..."
|
||||
: "Select chain to deploy on"
|
||||
}
|
||||
overrideSearchFn={searchFn}
|
||||
renderOption={renderOption}
|
||||
className={props.className}
|
||||
disabled={props.isAddingRow || allChains.length === 0}
|
||||
closeOnSelect={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
|
||||
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
|
||||
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
|
||||
import { EmbedSetup } from "./embed-setup";
|
||||
|
||||
export function EmbedSetupClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
twAccount: Account | undefined;
|
||||
}) {
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
|
||||
if (metadataQuery.isPending) {
|
||||
return <LoadingPage />;
|
||||
}
|
||||
|
||||
if (metadataQuery.isError) {
|
||||
return <ErrorPage />;
|
||||
}
|
||||
|
||||
if (metadataQuery.data.embedType === null) {
|
||||
return <RedirectToContractOverview contract={props.contract} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<EmbedSetup
|
||||
contract={props.contract}
|
||||
ercOrMarketplace={metadataQuery.data.embedType}
|
||||
twAccount={props.twAccount}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,612 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CodeClient } from "@/components/ui/code/code.client";
|
||||
import {
|
||||
type Account,
|
||||
useApiKeys,
|
||||
useCreateApiKey,
|
||||
} from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { Flex, FormControl, Input, Select } from "@chakra-ui/react";
|
||||
import { LazyCreateAPIKeyDialog } from "components/settings/ApiKeys/Create/LazyCreateAPIKeyDialog";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useAllChainsData } from "hooks/chains/allChains";
|
||||
import { AlertCircleIcon } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import type { StoredChain } from "stores/chainStores";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import { Card, FormHelperText, FormLabel, Heading, Text } from "tw-components";
|
||||
|
||||
interface EmbedSetupProps {
|
||||
contract: ThirdwebContract;
|
||||
ercOrMarketplace: string;
|
||||
twAccount: Account | undefined;
|
||||
}
|
||||
|
||||
// MAKE SURE THIS IS v1 embed hashes!!
|
||||
const IPFS_URI = "bafybeigdie2yyiazou7grjowoevmuip6akk33nqb55vrpezqdwfssrxyfy";
|
||||
const ERC721_IPFS_URI =
|
||||
"bafybeicd3qfzelz4su7ng6n523virdsgobrc5pcbarhwqv3dj3drh645pi";
|
||||
|
||||
interface IframeSrcOptions {
|
||||
clientId: string;
|
||||
chain: string;
|
||||
tokenId?: string;
|
||||
listingId?: string;
|
||||
listingType?: string;
|
||||
directListingId?: string;
|
||||
englishAuctionId?: string;
|
||||
relayUrl?: string;
|
||||
theme?: string;
|
||||
primaryColor?: string;
|
||||
secondaryColor?: string;
|
||||
biconomyApiKey?: string;
|
||||
biconomyApiId?: string;
|
||||
}
|
||||
|
||||
const colorOptions = [
|
||||
"purple",
|
||||
"blue",
|
||||
"orange",
|
||||
"pink",
|
||||
"green",
|
||||
"red",
|
||||
"teal",
|
||||
"cyan",
|
||||
"yellow",
|
||||
];
|
||||
|
||||
const isValidUrl = (url: string | undefined) => {
|
||||
if (!url) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const buildIframeSrc = (
|
||||
contract: ThirdwebContract,
|
||||
ercOrMarketplace?: string,
|
||||
options?: IframeSrcOptions,
|
||||
): string => {
|
||||
const contractPath =
|
||||
ercOrMarketplace === "erc721" ? "" : `${ercOrMarketplace}.html`;
|
||||
const contractEmbedHash =
|
||||
ercOrMarketplace === "erc721" ? ERC721_IPFS_URI : IPFS_URI;
|
||||
|
||||
if (!contract || !options || !contractEmbedHash) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const {
|
||||
clientId,
|
||||
chain,
|
||||
tokenId,
|
||||
listingId,
|
||||
listingType,
|
||||
directListingId,
|
||||
englishAuctionId,
|
||||
relayUrl,
|
||||
theme,
|
||||
primaryColor,
|
||||
secondaryColor,
|
||||
biconomyApiKey,
|
||||
biconomyApiId,
|
||||
} = options;
|
||||
|
||||
const url = new URL(
|
||||
`https://embed.ipfscdn.io/ipfs/${contractEmbedHash}/${contractPath}`,
|
||||
);
|
||||
|
||||
url.searchParams.append("contract", contract.address);
|
||||
url.searchParams.append("chain", chain);
|
||||
url.searchParams.append("clientId", clientId);
|
||||
|
||||
if (tokenId !== undefined && ercOrMarketplace === "erc1155") {
|
||||
url.searchParams.append("tokenId", tokenId.toString());
|
||||
}
|
||||
if (listingId !== undefined && ercOrMarketplace === "marketplace") {
|
||||
url.searchParams.append("listingId", listingId.toString());
|
||||
}
|
||||
if (
|
||||
directListingId !== undefined &&
|
||||
ercOrMarketplace === "marketplace-v3" &&
|
||||
listingType === "direct-listing"
|
||||
) {
|
||||
url.searchParams.append("directListingId", directListingId.toString());
|
||||
}
|
||||
if (
|
||||
englishAuctionId !== undefined &&
|
||||
ercOrMarketplace === "marketplace-v3" &&
|
||||
listingType === "english-auction"
|
||||
) {
|
||||
url.searchParams.append("englishAuctionId", englishAuctionId.toString());
|
||||
}
|
||||
if (isValidUrl(relayUrl)) {
|
||||
url.searchParams.append("relayUrl", relayUrl || "");
|
||||
}
|
||||
if (biconomyApiKey) {
|
||||
url.searchParams.append("biconomyApiKey", biconomyApiKey);
|
||||
}
|
||||
if (biconomyApiId) {
|
||||
url.searchParams.append("biconomyApiId", biconomyApiId);
|
||||
}
|
||||
if (theme) {
|
||||
url.searchParams.append("theme", theme);
|
||||
}
|
||||
if (primaryColor && primaryColor !== "default") {
|
||||
url.searchParams.append("primaryColor", primaryColor);
|
||||
}
|
||||
if (secondaryColor && secondaryColor !== "orange") {
|
||||
url.searchParams.append("secondaryColor", secondaryColor);
|
||||
}
|
||||
return url.toString();
|
||||
};
|
||||
|
||||
// taken from @thirdweb-dev /chains
|
||||
// @internal
|
||||
function minimizeChain(
|
||||
chain: ChainMetadata,
|
||||
): Pick<
|
||||
ChainMetadata,
|
||||
| "name"
|
||||
| "chain"
|
||||
| "rpc"
|
||||
| "nativeCurrency"
|
||||
| "shortName"
|
||||
| "chainId"
|
||||
| "testnet"
|
||||
| "slug"
|
||||
| "icon"
|
||||
> {
|
||||
const [firstRpc] = chain.rpc;
|
||||
return {
|
||||
name: chain.name,
|
||||
chain: chain.chain,
|
||||
rpc: [firstRpc || ""],
|
||||
nativeCurrency: chain.nativeCurrency,
|
||||
shortName: chain.shortName,
|
||||
chainId: chain.chainId,
|
||||
testnet: chain.testnet,
|
||||
slug: chain.slug,
|
||||
icon: chain.icon,
|
||||
};
|
||||
}
|
||||
|
||||
// taken from @thirdweb-dev /chains
|
||||
// @internal
|
||||
type ChainConfiguration = {
|
||||
rpc?: string | string[];
|
||||
};
|
||||
|
||||
function configureChain(
|
||||
chain: ChainMetadata,
|
||||
chainConfig: ChainConfiguration,
|
||||
): ChainMetadata {
|
||||
let additionalRPCs: string[] = [];
|
||||
if (chainConfig?.rpc) {
|
||||
if (typeof chainConfig.rpc === "string") {
|
||||
additionalRPCs = [chainConfig.rpc];
|
||||
} else {
|
||||
additionalRPCs = chainConfig.rpc;
|
||||
}
|
||||
}
|
||||
// prepend additional RPCs to the chain's RPCs
|
||||
return { ...chain, rpc: [...additionalRPCs, ...chain.rpc] };
|
||||
}
|
||||
|
||||
export const EmbedSetup: React.FC<EmbedSetupProps> = ({
|
||||
contract,
|
||||
ercOrMarketplace,
|
||||
twAccount,
|
||||
}) => {
|
||||
const trackEvent = useTrack();
|
||||
const { theme } = useTheme();
|
||||
|
||||
const apiKeys = useApiKeys({
|
||||
isLoggedIn: !!twAccount,
|
||||
});
|
||||
const createKeyMutation = useCreateApiKey();
|
||||
|
||||
const validApiKey = (apiKeys.data || []).find(
|
||||
(apiKey) =>
|
||||
(apiKey.domains.includes("*") ||
|
||||
apiKey.domains.includes("embed.ipfscdn.io") ||
|
||||
apiKey.domains.includes("*.ipfscdn.io")) &&
|
||||
(apiKey.services || [])
|
||||
.find((service) => service.name === "storage")
|
||||
?.actions.includes("read") &&
|
||||
!!(apiKey.services || []).find((service) => service.name === "rpc"),
|
||||
);
|
||||
|
||||
const chainId = contract.chain.id;
|
||||
const { idToChain } = useAllChainsData();
|
||||
|
||||
const chain: StoredChain = (chainId ? idToChain.get(chainId) : undefined) || {
|
||||
name: "Unknown Chain",
|
||||
chainId: chainId || -1,
|
||||
rpc: [],
|
||||
chain: "unknown",
|
||||
nativeCurrency: {
|
||||
name: "Ethereum",
|
||||
symbol: "ETH",
|
||||
decimals: 18,
|
||||
},
|
||||
shortName: "unknown",
|
||||
slug: "unknown",
|
||||
testnet: false,
|
||||
stackType: "",
|
||||
};
|
||||
|
||||
const { register, watch } = useForm<{
|
||||
rpcUrl: string;
|
||||
relayUrl: string;
|
||||
tokenId: string;
|
||||
listingId: string;
|
||||
listingType: string;
|
||||
directListingId: string;
|
||||
englishAuctionId: string;
|
||||
theme: string;
|
||||
primaryColor: string;
|
||||
secondaryColor: string;
|
||||
biconomyApiKey: string;
|
||||
biconomyApiId: string;
|
||||
gasless: string;
|
||||
}>({
|
||||
defaultValues: {
|
||||
rpcUrl: chain?.rpc[0],
|
||||
tokenId: "0",
|
||||
listingId: "0",
|
||||
directListingId: "0",
|
||||
englishAuctionId: "0",
|
||||
theme: theme === "light" ? "light" : "dark",
|
||||
primaryColor: "purple",
|
||||
secondaryColor: "orange",
|
||||
},
|
||||
reValidateMode: "onChange",
|
||||
});
|
||||
|
||||
const configuredChainWithNewRpc = configureChain(chain, {
|
||||
rpc: watch("rpcUrl"),
|
||||
});
|
||||
const minimizedChain = minimizeChain(configuredChainWithNewRpc);
|
||||
|
||||
const iframeSrc = buildIframeSrc(contract, ercOrMarketplace, {
|
||||
chain: JSON.stringify(minimizedChain),
|
||||
clientId: validApiKey?.key || "",
|
||||
...watch(),
|
||||
});
|
||||
|
||||
const embedCode = useMemo(
|
||||
() =>
|
||||
`<iframe
|
||||
src="${iframeSrc}"
|
||||
width="100%"
|
||||
height="750px"
|
||||
style="max-width:100%;"
|
||||
frameborder="0"
|
||||
></iframe>`,
|
||||
[iframeSrc],
|
||||
);
|
||||
|
||||
const [showCreateAPIKeyModal, setShowCreateAPIKeyModal] = useState(false);
|
||||
const activeAccount = useActiveAccount();
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<LazyCreateAPIKeyDialog
|
||||
prefill={{
|
||||
name: "Embed API Key",
|
||||
domains: "embed.ipfscdn.io",
|
||||
}}
|
||||
open={showCreateAPIKeyModal}
|
||||
onOpenChange={setShowCreateAPIKeyModal}
|
||||
onCreateAndComplete={() => {
|
||||
trackEvent({
|
||||
category: "api-keys",
|
||||
action: "create",
|
||||
label: "success",
|
||||
fromEmbed: true,
|
||||
});
|
||||
apiKeys.refetch();
|
||||
}}
|
||||
enableNebulaServiceByDefault={false}
|
||||
/>
|
||||
|
||||
<Alert variant="warning">
|
||||
<AlertCircleIcon className="size-5" />
|
||||
<AlertTitle>Deprecated</AlertTitle>
|
||||
<AlertDescription className="leading-relaxed">
|
||||
<span>
|
||||
thirdweb NFT Embeds are deprecated and not actively maintained
|
||||
</span>{" "}
|
||||
<br />
|
||||
<span>
|
||||
Use the{" "}
|
||||
<Link
|
||||
href="https://github.com/thirdweb-example/nft-minting-template"
|
||||
target="_blank"
|
||||
className="underline underline-offset-2 hover:text-foreground"
|
||||
>
|
||||
{" "}
|
||||
NFT Minting Template
|
||||
</Link>{" "}
|
||||
or build a custom NFT minting app using{" "}
|
||||
<Link
|
||||
href="https://portal.thirdweb.com/react/v5"
|
||||
className="underline underline-offset-2 hover:text-foreground"
|
||||
>
|
||||
thirdweb React SDK components
|
||||
</Link>{" "}
|
||||
instead
|
||||
</span>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="h-8" />
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
<div>
|
||||
<h2 className="mb-3 font-semibold text-xl tracking-tight">
|
||||
Configuration
|
||||
</h2>
|
||||
|
||||
<Card className="flex w-full flex-col gap-5 bg-card">
|
||||
{ercOrMarketplace === "marketplace" ? (
|
||||
<FormControl>
|
||||
<FormLabel>Listing ID</FormLabel>
|
||||
<Input type="number" {...register("listingId")} />
|
||||
<FormHelperText>
|
||||
The listing ID the embed should display
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
) : null}
|
||||
|
||||
{ercOrMarketplace === "marketplace-v3" ? (
|
||||
<FormControl>
|
||||
<FormLabel>Listing type</FormLabel>
|
||||
<Select {...register("listingType")}>
|
||||
<option value="direct-listing">Direct Listing</option>
|
||||
<option value="english-auction">English Auction</option>
|
||||
</Select>
|
||||
<FormHelperText>
|
||||
The type of listing the embed should display
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
) : null}
|
||||
|
||||
{ercOrMarketplace === "marketplace-v3" &&
|
||||
watch("listingType") === "direct-listing" ? (
|
||||
<FormControl>
|
||||
<FormLabel>Direct Listing ID</FormLabel>
|
||||
<Input type="number" {...register("directListingId")} />
|
||||
<FormHelperText>
|
||||
The direct listing ID the embed should display
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
) : null}
|
||||
|
||||
{ercOrMarketplace === "marketplace-v3" &&
|
||||
watch("listingType") === "english-auction" ? (
|
||||
<FormControl>
|
||||
<FormLabel>English Auction ID</FormLabel>
|
||||
<Input type="number" {...register("englishAuctionId")} />
|
||||
<FormHelperText>
|
||||
The english auction ID the embed should display
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
) : null}
|
||||
|
||||
{ercOrMarketplace === "erc1155" ? (
|
||||
<FormControl>
|
||||
<FormLabel>Token ID</FormLabel>
|
||||
<Input type="number" {...register("tokenId")} />
|
||||
<FormHelperText>
|
||||
The token ID the embed should display
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
) : null}
|
||||
|
||||
<FormControl>
|
||||
<FormLabel>Client ID</FormLabel>
|
||||
{!activeAccount ? (
|
||||
<Button asChild className="w-full">
|
||||
<Link
|
||||
href={`/login${
|
||||
pathname ? `?next=${encodeURIComponent(pathname)}` : ""
|
||||
}`}
|
||||
>
|
||||
Sign in to create a client ID
|
||||
</Link>
|
||||
</Button>
|
||||
) : validApiKey ? (
|
||||
<Input
|
||||
readOnly
|
||||
disabled
|
||||
value={`${validApiKey?.name} - ${validApiKey?.key}`}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
trackEvent({
|
||||
category: "api-keys",
|
||||
action: "create",
|
||||
label: "attempt",
|
||||
fromEmbed: true,
|
||||
});
|
||||
|
||||
setShowCreateAPIKeyModal(true);
|
||||
}}
|
||||
disabled={createKeyMutation.isPending}
|
||||
>
|
||||
Create Client ID
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<FormHelperText>
|
||||
You need a client ID to use embeds.{" "}
|
||||
<Link
|
||||
href="https://portal.thirdweb.com/account/api-keys"
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
</Link>
|
||||
.
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<FormLabel>RPC Url</FormLabel>
|
||||
<Input type="url" {...register("rpcUrl")} />
|
||||
<FormHelperText>
|
||||
RPC the embed should use to connect to the blockchain.
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
{ercOrMarketplace === "marketplace" ||
|
||||
ercOrMarketplace === "marketplace-v3" ? null : (
|
||||
<FormControl gap={4}>
|
||||
<Heading size="title.sm" mb={2}>
|
||||
Gasless
|
||||
</Heading>
|
||||
<Select {...register("gasless")} mb={4}>
|
||||
<option value="false">Disabled</option>
|
||||
<option value="openZeppelin">OpenZeppelin Relayer</option>
|
||||
<option value="biconomy">Biconomy Relayer</option>
|
||||
</Select>
|
||||
{watch("gasless") === "openZeppelin" && (
|
||||
<FormControl>
|
||||
<FormLabel>OpenZeppelin Relayer URL</FormLabel>
|
||||
<Input type="url" {...register("relayUrl")} />
|
||||
</FormControl>
|
||||
)}
|
||||
{watch("gasless") === "biconomy" && (
|
||||
<Flex gap={4} flexDir="column">
|
||||
<FormControl>
|
||||
<FormLabel>Biconomy API key</FormLabel>
|
||||
<Input type="url" {...register("biconomyApiKey")} />
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel>Biconomy API ID</FormLabel>
|
||||
<Input type="url" {...register("biconomyApiId")} />
|
||||
</FormControl>
|
||||
</Flex>
|
||||
)}
|
||||
<FormHelperText>
|
||||
A relayer can be used to make the transaction gasless for the
|
||||
end user.{" "}
|
||||
<Link
|
||||
target="_blank"
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
href="https://blog.thirdweb.com/guides/setup-gasless-transactions"
|
||||
>
|
||||
Learn more
|
||||
</Link>
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||
<FormControl>
|
||||
<FormLabel>Theme</FormLabel>
|
||||
<Select {...register("theme")}>
|
||||
<option value="light">Light</option>
|
||||
<option value="dark">Dark</option>
|
||||
<option value="system">User system</option>
|
||||
</Select>
|
||||
<FormHelperText>
|
||||
Selecting system will make it so the embed would change
|
||||
depending on the user system's preferences.
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<FormLabel>Primary Color</FormLabel>
|
||||
<Select {...register("primaryColor")}>
|
||||
{ercOrMarketplace === "erc721" && (
|
||||
<option value="default">Default</option>
|
||||
)}
|
||||
{colorOptions.map((color) => (
|
||||
<option key={color} value={color}>
|
||||
{color[0]?.toUpperCase() + color.substring(1)}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<FormHelperText>
|
||||
Used for the main actions button backgrounds.
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
{ercOrMarketplace === "marketplace" ||
|
||||
ercOrMarketplace === "marketplace-v3" ? (
|
||||
<FormControl>
|
||||
<FormLabel>Secondary Color</FormLabel>
|
||||
<Select {...register("secondaryColor")}>
|
||||
{colorOptions.map((color) => (
|
||||
<option key={color} value={color}>
|
||||
{color[0]?.toUpperCase() + color.substring(1)}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<FormHelperText>
|
||||
Use for secondary actions (like when the user is connected to
|
||||
the wrong network)
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
) : null}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="mb-3 font-semibold text-xl tracking-tight">
|
||||
Embed Code
|
||||
</h2>
|
||||
<CodeClient
|
||||
code={embedCode}
|
||||
lang="html"
|
||||
onCopy={() => {
|
||||
trackEvent({
|
||||
category: "embed",
|
||||
action: "click",
|
||||
label: "copy-code",
|
||||
address: contract.address,
|
||||
chainId,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-8" />
|
||||
|
||||
<h2 className="mb-3 font-semibold text-xl tracking-tight">Preview</h2>
|
||||
|
||||
<div className="overflow-hidden rounded-lg border border-border">
|
||||
{!validApiKey ? (
|
||||
<Text>You need to create a client ID to use embeds</Text>
|
||||
) : iframeSrc ? (
|
||||
<iframe
|
||||
title="thirdweb embed"
|
||||
src={iframeSrc}
|
||||
width="100%"
|
||||
height="750px"
|
||||
frameBorder="0"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,42 +0,0 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { localhost } from "thirdweb/chains";
|
||||
import { getRawAccount } from "../../../../../account/settings/getAccount";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { EmbedSetupClient } from "./EmbedSetup.client";
|
||||
import { EmbedSetup } from "./embed-setup";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
}>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const account = await getRawAccount();
|
||||
|
||||
const { contract } = info;
|
||||
if (contract.chain.id === localhost.id) {
|
||||
return <EmbedSetupClient contract={contract} twAccount={account} />;
|
||||
}
|
||||
|
||||
const { embedType } = await getContractPageMetadata(contract);
|
||||
|
||||
if (embedType === null) {
|
||||
redirect(`/${params.chain_id}/${params.contractAddress}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<EmbedSetup
|
||||
contract={contract}
|
||||
ercOrMarketplace={embedType}
|
||||
twAccount={account}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
import { getProjects } from "@/api/projects";
|
||||
import { getTeams } from "@/api/team";
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { localhost } from "thirdweb/chains";
|
||||
import { getContractMetadata } from "thirdweb/extensions/common";
|
||||
import { isAddress, isContractDeployed } from "thirdweb/utils";
|
||||
import type { MinimalTeamsAndProjects } from "../../../../../components/contract-components/contract-deploy-form/add-to-project-card";
|
||||
import { resolveFunctionSelectors } from "../../../../../lib/selectors";
|
||||
import { shortenIfAddress } from "../../../../../utils/usedapp-external";
|
||||
import { ConfigureCustomChain } from "./_layout/ConfigureCustomChain";
|
||||
@@ -38,11 +42,16 @@ export default async function Layout(props: {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const client = getThirdwebClient();
|
||||
const teamsAndProjects = await getTeamsAndProjectsIfLoggedIn();
|
||||
|
||||
if (contract.chain.id === localhost.id) {
|
||||
return (
|
||||
<ContractPageLayoutClient
|
||||
chainMetadata={chainMetadata}
|
||||
contract={contract}
|
||||
teamsAndProjects={teamsAndProjects}
|
||||
client={client}
|
||||
>
|
||||
{props.children}
|
||||
</ContractPageLayoutClient>
|
||||
@@ -73,12 +82,43 @@ export default async function Layout(props: {
|
||||
sidebarLinks={sidebarLinks}
|
||||
dashboardContractMetadata={contractMetadata}
|
||||
externalLinks={externalLinks}
|
||||
teamsAndProjects={teamsAndProjects}
|
||||
client={client}
|
||||
>
|
||||
{props.children}
|
||||
</ContractPageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
async function getTeamsAndProjectsIfLoggedIn() {
|
||||
try {
|
||||
const teams = await getTeams();
|
||||
|
||||
if (!teams) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const teamsAndProjects: MinimalTeamsAndProjects = await Promise.all(
|
||||
teams.map(async (team) => ({
|
||||
team: {
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
slug: team.slug,
|
||||
image: team.image,
|
||||
},
|
||||
projects: (await getProjects(team.slug)).map((x) => ({
|
||||
id: x.id,
|
||||
name: x.name,
|
||||
})),
|
||||
})),
|
||||
);
|
||||
|
||||
return teamsAndProjects;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateMetadata(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
|
||||
@@ -26,6 +26,7 @@ import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { TransactionButton } from "components/buttons/TransactionButton";
|
||||
import { addDays, fromUnixTime } from "date-fns";
|
||||
import { useAllChainsData } from "hooks/chains/allChains";
|
||||
@@ -119,22 +120,39 @@ function ClaimableModule(props: ModuleInstanceProps) {
|
||||
claimConditionQuery.data?.startTimestamp === 0 &&
|
||||
claimConditionQuery.data?.endTimestamp === 0;
|
||||
|
||||
const currencyContract = getContract({
|
||||
address: claimConditionQuery.data?.currency || "",
|
||||
chain: props.contract.chain,
|
||||
client: props.contract.client,
|
||||
});
|
||||
const claimConditionCurrency = claimConditionQuery.data?.currency;
|
||||
|
||||
const shouldFetchTokenDecimals =
|
||||
claimConditionQuery.data &&
|
||||
claimConditionQuery.data?.currency !== ZERO_ADDRESS;
|
||||
const shouldFetchCurrencyDecimals =
|
||||
!!claimConditionCurrency && claimConditionCurrency !== ZERO_ADDRESS;
|
||||
|
||||
const currencyDecimalsQuery = useReadContract(decimals, {
|
||||
contract: currencyContract,
|
||||
queryOptions: {
|
||||
enabled: shouldFetchTokenDecimals,
|
||||
const currencyDecimalsQuery = useQuery({
|
||||
queryKey: [
|
||||
"currency-decimals",
|
||||
{
|
||||
contractAddress: props.contract.address,
|
||||
chainId: props.contract.chain.id,
|
||||
claimConditionCurrency,
|
||||
},
|
||||
],
|
||||
queryFn: async () => {
|
||||
if (!claimConditionCurrency) {
|
||||
throw new Error();
|
||||
}
|
||||
const currencyContract = getContract({
|
||||
address: claimConditionCurrency,
|
||||
chain: props.contract.chain,
|
||||
client: props.contract.client,
|
||||
});
|
||||
|
||||
const decimalsVal = await decimals({
|
||||
contract: currencyContract,
|
||||
});
|
||||
|
||||
return decimalsVal;
|
||||
},
|
||||
enabled: shouldFetchCurrencyDecimals,
|
||||
});
|
||||
|
||||
const tokenDecimalsQuery = useReadContract(decimals, {
|
||||
contract: contract,
|
||||
queryOptions: {
|
||||
@@ -265,7 +283,7 @@ function ClaimableModule(props: ModuleInstanceProps) {
|
||||
// claim conditions data is present
|
||||
claimConditionQuery.data &&
|
||||
// token decimals is fetched if it should be fetched
|
||||
(shouldFetchTokenDecimals ? currencyDecimalsQuery.isFetched : true)
|
||||
(shouldFetchCurrencyDecimals ? currencyDecimalsQuery.isFetched : true)
|
||||
? {
|
||||
claimCondition: claimConditionQuery.data,
|
||||
currencyDecimals: currencyDecimalsQuery.data,
|
||||
@@ -276,7 +294,7 @@ function ClaimableModule(props: ModuleInstanceProps) {
|
||||
tokenId,
|
||||
isLoading:
|
||||
claimConditionQuery.isLoading ||
|
||||
(!!shouldFetchTokenDecimals && currencyDecimalsQuery.isLoading),
|
||||
(!!shouldFetchCurrencyDecimals && currencyDecimalsQuery.isLoading),
|
||||
}}
|
||||
isOwnerAccount={!!ownerAccount}
|
||||
name={props.contractInfo.name}
|
||||
@@ -565,6 +583,7 @@ function ClaimConditionSection(props: {
|
||||
onClick={() => setAddClaimConditionButtonClicked(true)}
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
disabled={!props.isOwnerAccount}
|
||||
>
|
||||
Add Claim Condition
|
||||
</Button>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import { fetchPublishedContractVersions } from "components/contract-components/fetch-contracts-with-versions";
|
||||
import {
|
||||
fetchLatestPublishedContractVersion,
|
||||
fetchPublishedContractVersions,
|
||||
} from "components/contract-components/fetch-contracts-with-versions";
|
||||
import { isAddress } from "thirdweb";
|
||||
import type { FetchDeployMetadataResult } from "thirdweb/contract";
|
||||
import { resolveAddress } from "thirdweb/extensions/ens";
|
||||
import invariant from "tiny-invariant";
|
||||
import type { ModuleMeta } from "./install-module-params";
|
||||
@@ -14,16 +18,24 @@ export async function getModuleInstalledParams(ext: ModuleMeta) {
|
||||
client: getThirdwebClient(),
|
||||
name: ext.publisherAddress,
|
||||
});
|
||||
const allPublishedModules = await fetchPublishedContractVersions(
|
||||
publisherAddress,
|
||||
ext.moduleName,
|
||||
);
|
||||
|
||||
// find the version we want
|
||||
const publishedModule =
|
||||
ext.moduleVersion === "latest"
|
||||
? allPublishedModules[0]
|
||||
: allPublishedModules.find((v) => v.version === ext.moduleVersion);
|
||||
let publishedModule: FetchDeployMetadataResult | undefined = undefined;
|
||||
|
||||
if (ext.moduleVersion === "latest") {
|
||||
publishedModule = await fetchLatestPublishedContractVersion(
|
||||
publisherAddress,
|
||||
ext.moduleName,
|
||||
);
|
||||
} else {
|
||||
const allPublishedModules = await fetchPublishedContractVersions(
|
||||
publisherAddress,
|
||||
ext.moduleName,
|
||||
);
|
||||
|
||||
publishedModule = allPublishedModules.find(
|
||||
(v) => v.version === ext.moduleVersion,
|
||||
);
|
||||
}
|
||||
|
||||
invariant(
|
||||
publishedModule,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -135,7 +135,7 @@ function OverviewAnalytics(props: ChartProps) {
|
||||
data={mergedData || []}
|
||||
isPending={isPending}
|
||||
showLegend
|
||||
chartClassName="aspect-[1.5] lg:aspect-[4.5]"
|
||||
chartClassName="aspect-[1.5] lg:aspect-[3.5]"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||