Compare commits
163
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0299034fc1 | ||
|
|
861e623a1b | ||
|
|
6a335bfa77 | ||
|
|
cb994dcb8f | ||
|
|
4d38a7e8fe | ||
|
|
89e6af6baf | ||
|
|
b345cc115e | ||
|
|
3d3c6f3d45 | ||
|
|
ff3a79c884 | ||
|
|
16482cfa6a | ||
|
|
e8a25d70b9 | ||
|
|
44a7460c83 | ||
|
|
0731831169 | ||
|
|
91750eddc6 | ||
|
|
2fc083f1f2 | ||
|
|
75ae536e61 | ||
|
|
41e59a9c20 | ||
|
|
fd97406fa2 | ||
|
|
c336de3bfb | ||
|
|
c420099dc3 | ||
|
|
dcb412fc27 | ||
|
|
4705a599a2 | ||
|
|
f7331f15a2 | ||
|
|
a5d6e769db | ||
|
|
97b8926024 | ||
|
|
655df037dd | ||
|
|
cbde32e974 | ||
|
|
8ed5ef3f81 | ||
|
|
e14a1349e1 | ||
|
|
acd8b9e535 | ||
|
|
52d451ff70 | ||
|
|
c03616666b | ||
|
|
bb7a8988ef | ||
|
|
8d0ffedd41 | ||
|
|
5edfb81ec5 | ||
|
|
f115b4d2db | ||
|
|
0e4390b2c7 | ||
|
|
aec8d88af2 | ||
|
|
80207ae240 | ||
|
|
ec7bc2bb1e | ||
|
|
12475027d1 | ||
|
|
9479d252b4 | ||
|
|
5139857db9 | ||
|
|
ad4341a7ad | ||
|
|
ebd58f49b6 | ||
|
|
3862bc7a94 | ||
|
|
5277eddb70 | ||
|
|
6c180b5c8a | ||
|
|
3489ecec98 | ||
|
|
f9b34ff2f1 | ||
|
|
f3abea3276 | ||
|
|
adc738e536 | ||
|
|
dd2fb1b13d | ||
|
|
69fdef07f5 | ||
|
|
362c086bf6 | ||
|
|
f9b609e7a2 | ||
|
|
916ecf494c | ||
|
|
d1ca9af2f4 | ||
|
|
317211a48e | ||
|
|
6cd64e7368 | ||
|
|
005acce714 | ||
|
|
cde2dd941f | ||
|
|
7130f9a929 | ||
|
|
1e0b1422f3 | ||
|
|
70a519ada4 | ||
|
|
678c00e322 | ||
|
|
5ab59ff400 | ||
|
|
73629e9f0e | ||
|
|
23029db9ef | ||
|
|
526b6b54cf | ||
|
|
f31116ea5a | ||
|
|
c1f7aaadc1 | ||
|
|
1ad109f3bc | ||
|
|
376bdb23d2 | ||
|
|
dcd6b99e67 | ||
|
|
88533d9113 | ||
|
|
6c3a8d36a7 | ||
|
|
a31ac0c065 | ||
|
|
7b72e88611 | ||
|
|
4b9a506fdf | ||
|
|
77da352366 | ||
|
|
72cc27a69a | ||
|
|
46d1dab11e | ||
|
|
a1c432ca17 | ||
|
|
243c661929 | ||
|
|
b64ffe369a | ||
|
|
30fafd16db | ||
|
|
99901dcfd7 | ||
|
|
f5072d882e | ||
|
|
e7f7a32dd4 | ||
|
|
d2098ad0bf | ||
|
+1 |
d6239785cf | ||
|
|
20fdd5b6d7 | ||
|
|
438c019f01 | ||
|
|
61044013c8 | ||
|
|
e6213db8af | ||
|
|
c46b2597be | ||
|
|
9ff97688ce | ||
|
|
b4f5cc7b5a | ||
|
|
256850d1c6 | ||
|
|
acb2b9f85f | ||
|
|
6ac62ffe96 | ||
|
|
80f4bd4e53 | ||
|
|
a3072c4335 | ||
|
|
3277cf6e8e | ||
|
|
77938c4d5c | ||
|
|
3080a9baba | ||
|
|
6cf92c6b11 | ||
|
+1 |
89ccc80d9c | ||
|
|
713ebe0a2d | ||
|
|
bfdac10dcc | ||
|
+1 |
64964da22e | ||
|
|
260c2fd370 | ||
|
|
c61e258ec3 | ||
|
|
fdd344184b | ||
|
|
0c853e8709 | ||
|
|
7d47b4cde0 | ||
|
|
feae304da2 | ||
|
|
8f605d989c | ||
|
|
d0c2ac63a9 | ||
|
|
170f14fb54 | ||
|
|
da2a2a0e86 | ||
|
|
e62fd3fe40 | ||
|
|
99a7cce624 | ||
|
|
61152dd098 | ||
|
|
05519fcdde | ||
|
|
7257747520 | ||
|
|
e71e7d70a6 | ||
|
|
8d47864967 | ||
|
|
4a8adc143f | ||
|
|
db538473cf | ||
|
|
2057546934 | ||
|
|
ba52e68eab | ||
|
|
0eff64824b | ||
|
|
ae7407dca4 | ||
|
|
a443b309dc | ||
|
|
ae2ff743c0 | ||
|
|
dfa8603593 | ||
|
|
9f1ca54c26 | ||
|
|
7df81b7323 | ||
|
|
ab74fcdaf8 | ||
|
|
1191d5dad9 | ||
|
|
20b5ba943a | ||
|
|
f0f8da8fab | ||
|
|
da3fc99c0b | ||
|
|
fb4c190eb3 | ||
|
|
1c31a1e4df | ||
|
|
04c10f82fd | ||
|
|
ab173886ac | ||
|
|
df06bec699 | ||
|
|
6000dacfbb | ||
|
|
fc6535069b | ||
|
|
b15c3c0747 | ||
|
|
1985de89aa | ||
|
|
2b87b7bb75 | ||
|
|
d99a985ab4 | ||
|
|
e74574ccf5 | ||
|
|
98d05d6fb6 | ||
|
|
f4ac3c2a3a | ||
|
|
a3a4bf4361 | ||
|
|
e8ec54e6e2 | ||
|
|
58e343c10c | ||
|
|
bad9db9760 |
@@ -14,7 +14,7 @@ We use [Turborepo](https://turbo.build/repo/docs) to manage the repository, and
|
||||
|
||||
We use [pnpm](https://pnpm.io) for package management across the repo. `pnpm` is similar to `npm` or `yarn` but with more efficient disk space usage.
|
||||
|
||||
**With the v5 SDK, we've consolidated everything into a single project at [/packages/thirdweb](./packages/thirdweb). You can still find the legacy packages at [/legacy_packages](./legacy_packages).**
|
||||
**With the v5 SDK, we've consolidated everything into a single project at [/packages/thirdweb](../packages/thirdweb). You can still find the legacy packages at [/legacy_packages](../legacy_packages).**
|
||||
|
||||
This single package provides a performant & lightweight SDK to interact with any EVM chain across Node, React, and React Native. Learn more about how to use the thirdweb SDK in our [documentation](https://portal.thirdweb.com/typescript/v5).
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
- run: pnpm test
|
||||
|
||||
- name: Code Coverage
|
||||
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
|
||||
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
|
||||
with:
|
||||
directory: packages/
|
||||
flags: packages
|
||||
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
|
||||
uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
|
||||
uses: github/codeql-action/autobuild@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
@@ -72,4 +72,4 @@ jobs:
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
|
||||
uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
|
||||
- name: Create release Pull Request or publish to NPM
|
||||
id: changesets
|
||||
uses: changesets/action@746c25e23caa47dceb6a48ee85b4cbc5a9f5f293 #v1.4.0
|
||||
uses: changesets/action@e0145edc7d9d8679003495b11f87bd8ef63c0cba #v1.4.0
|
||||
with:
|
||||
publish: pnpm release
|
||||
version: pnpm version-packages
|
||||
|
||||
+2
-1
@@ -31,4 +31,5 @@ storybook-static
|
||||
.aider*
|
||||
|
||||
tsconfig.tsbuildinfo
|
||||
.cursor
|
||||
.cursor
|
||||
apps/dashboard/node-compile-cache
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
🤖 Codex Agent Guidelines for thirdweb-dev/js
|
||||
|
||||
Welcome, AI copilots! This guide captures the coding standards, architectural decisions, and workflow conventions that every automated agent (and human contributor!) must follow. Unless a rule explicitly targets a sub‑project, it applies repo‑wide.
|
||||
|
||||
⸻
|
||||
|
||||
1. GitHub Workflow & Etiquette
|
||||
|
||||
- Pull‑request titles must start with the affected workspace in brackets (e.g. [SDK], [Dashboard], [Portal], [Playground]).
|
||||
- Begin the PR description with a one‑sentence summary, then add a checklist of changes and reference issues with Fixes #123.
|
||||
- Keep commits small and topical – one logical change per commit.
|
||||
- Branch names should follow area/brief-topic (e.g. sdk/fix-gas-estimate). Avoid personal names.
|
||||
- Request at least one core maintainer review. Do not self‑merge unless you are the sole owner of that package.
|
||||
- All CI checks (type‑check, Biome, tests) must pass before merging.
|
||||
|
||||
⸻
|
||||
|
||||
2. Formatting & Linting
|
||||
|
||||
- Biome governs formatting and linting; its rules live in biome.json.
|
||||
- Run pnpm biome check --apply before committing.
|
||||
- Avoid editor‑specific configs; rely on the shared settings.
|
||||
|
||||
⸻
|
||||
|
||||
3. TypeScript Style Guide
|
||||
|
||||
- Write idiomatic TypeScript: explicit function declarations and return types.
|
||||
- Limit each file to one stateless, single‑responsibility function for clarity and testability.
|
||||
- Re‑use shared types from @/types or local types.ts barrels.
|
||||
- Prefer type aliases over interface except for nominal shapes.
|
||||
- Avoid any and unknown unless unavoidable; narrow generics whenever possible.
|
||||
- Choose composition over inheritance; leverage utility types (Partial, Pick, etc.).
|
||||
|
||||
⸻
|
||||
|
||||
4. Testing Strategy
|
||||
|
||||
- Co‑locate tests: foo.ts ↔ foo.test.ts.
|
||||
- Use real function invocations with stub data; avoid brittle mocks.
|
||||
- For network interactions, use Mock Service Worker (MSW) to intercept fetch/HTTP calls, mocking only scenarios that are hard to reproduce.
|
||||
- Keep tests deterministic and side‑effect free; Jest is pre‑configured.
|
||||
|
||||
⸻
|
||||
|
||||
5. packages/thirdweb
|
||||
|
||||
5.1 Public API Surface
|
||||
|
||||
- Export everything via the exports/ directory, grouped by feature.
|
||||
- Every public symbol must have comprehensive TSDoc:
|
||||
- Include at least one @example block that compiles.
|
||||
- Tag with one custom annotation (@beta, @internal, @experimental, etc.).
|
||||
- Comment only ambiguous logic; avoid restating TypeScript in prose.
|
||||
|
||||
5.2 Performance
|
||||
|
||||
- Lazy‑load heavy dependencies inside async paths to keep the initial bundle lean:
|
||||
|
||||
`const { jsPDF } = await import("jspdf");`
|
||||
|
||||
⸻
|
||||
|
||||
6. apps/dashboard & apps/playground
|
||||
|
||||
6.1 Core UI Toolkit
|
||||
|
||||
- Import primitives from @/components/ui/\_ (e.g. Button, Input, Select, Tabs, Card, Sidebar, Badge, Separator).
|
||||
- Use NavLink for internal navigation so active states are handled automatically.
|
||||
- Group feature‑specific components under feature/components/\_ and expose a barrel index.ts when necessary.
|
||||
|
||||
6.2 Styling Conventions
|
||||
|
||||
- Tailwind CSS is the styling system – no inline styles or CSS modules.
|
||||
- Merge class names with cn() from @/lib/utils to keep conditional logic readable.
|
||||
- Stick to design tokens: backgrounds (bg-card), borders (border-border), muted text (text-muted-foreground), etc.
|
||||
- Expose a className prop on the root element of every component for overrides.
|
||||
|
||||
6.3 Component Patterns
|
||||
|
||||
- Server Components (run on the Node edge):
|
||||
-- Read cookies/headers with next/headers.
|
||||
-- Access server‑only environment variables or secrets.
|
||||
-- Perform heavy data fetching that should not ship to the client.
|
||||
-- Implement redirect logic with redirect() from next/navigation.
|
||||
-- Start files with import "server-only"; to prevent client bundling.
|
||||
- Client Components (run in the browser):
|
||||
-- Begin files with 'use client'; before imports.
|
||||
-- Handle interactive UI relying on React hooks (useState, useEffect, React Query, wallet hooks).
|
||||
-- Access browser APIs (localStorage, window, IntersectionObserver, etc.).
|
||||
-- Support fast transitions where data is prefetched on the client.
|
||||
|
||||
6.4 Data Fetching Guidelines
|
||||
|
||||
- Server Side
|
||||
-- Always call getAuthToken() to retrieve the JWT from cookies.
|
||||
-- Inject the token as an Authorization: Bearer header – never embed it in the URL.
|
||||
-- Return typed results (Project[], User[], …) – avoid any.
|
||||
- Client Side
|
||||
-- Wrap calls in React Query (@tanstack/react-query).
|
||||
-- Use descriptive, stable queryKeys for cache hits.
|
||||
-- Configure staleTime / cacheTime based on freshness requirements (default ≥ 60 s).
|
||||
-- Keep tokens secret by calling internal API routes or server actions.
|
||||
|
||||
⸻
|
||||
|
||||
7. Performance & Bundle Size
|
||||
|
||||
- Track bundle budgets via package.json#size-limit.
|
||||
- Lazy‑import optional features; avoid top‑level side‑effects.
|
||||
- De‑duplicate dependencies across packages through pnpm workspace hoisting.
|
||||
|
||||
⸻
|
||||
|
||||
8. Documentation & Developer Experience
|
||||
|
||||
- Each change in packages/\* should contain a changeset for the appropriate package, with the appropriate version bump
|
||||
|
||||
- patch for changes that don't impact the public API
|
||||
- minor for any new/modified public API
|
||||
|
||||
- Surface breaking changes prominently in PR descriptions.
|
||||
- For new UI components, add Storybook stories (\*.stories.tsx) alongside the code.
|
||||
@@ -27,10 +27,7 @@ module.exports = [
|
||||
// -- engine
|
||||
"/engine",
|
||||
// -- solutions pages --
|
||||
"/solutions/gaming",
|
||||
"/solutions/consumer-apps",
|
||||
"/solutions/defi",
|
||||
"/solutions/ecosystem",
|
||||
"/solutions/:solution_slug",
|
||||
// -- campaigns --
|
||||
// "/unlimited-wallets", -- OFF for now
|
||||
// -- TPP --
|
||||
|
||||
+22
-22
@@ -40,8 +40,8 @@
|
||||
"@radix-ui/react-popover": "^1.1.10",
|
||||
"@radix-ui/react-progress": "^1.1.4",
|
||||
"@radix-ui/react-radio-group": "^1.3.3",
|
||||
"@radix-ui/react-select": "^2.2.2",
|
||||
"@radix-ui/react-separator": "^1.1.4",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.0",
|
||||
"@radix-ui/react-switch": "^1.2.2",
|
||||
"@radix-ui/react-tooltip": "1.2.3",
|
||||
@@ -71,13 +71,13 @@
|
||||
"ioredis": "^5.6.1",
|
||||
"ipaddr.js": "^2.2.0",
|
||||
"lucide-react": "0.503.0",
|
||||
"next": "15.3.1",
|
||||
"next": "15.3.2",
|
||||
"next-plausible": "^3.12.4",
|
||||
"next-themes": "^0.4.6",
|
||||
"nextjs-toploader": "^1.6.12",
|
||||
"nuqs": "^2.4.3",
|
||||
"p-limit": "^6.2.0",
|
||||
"papaparse": "^5.5.2",
|
||||
"papaparse": "^5.5.3",
|
||||
"pluralize": "^8.0.0",
|
||||
"posthog-js": "1.67.1",
|
||||
"prettier": "3.5.3",
|
||||
@@ -89,7 +89,7 @@
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-error-boundary": "^5.0.0",
|
||||
"react-hook-form": "7.55.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-markdown": "10.1.0",
|
||||
"react-table": "^7.8.0",
|
||||
"recharts": "2.15.3",
|
||||
"remark-gfm": "4.0.1",
|
||||
@@ -105,30 +105,30 @@
|
||||
"thirdweb": "workspace:*",
|
||||
"tiny-invariant": "^1.3.3",
|
||||
"use-debounce": "^10.0.4",
|
||||
"zod": "3.24.3"
|
||||
"zod": "3.25.24"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chakra-ui/cli": "^2.4.1",
|
||||
"@chromatic-com/storybook": "3.2.6",
|
||||
"@next/bundle-analyzer": "15.3.1",
|
||||
"@next/eslint-plugin-next": "15.3.1",
|
||||
"@next/bundle-analyzer": "15.3.2",
|
||||
"@next/eslint-plugin-next": "15.3.2",
|
||||
"@playwright/test": "1.52.0",
|
||||
"@storybook/addon-essentials": "8.6.12",
|
||||
"@storybook/addon-interactions": "8.6.12",
|
||||
"@storybook/addon-links": "8.6.12",
|
||||
"@storybook/addon-onboarding": "8.6.12",
|
||||
"@storybook/addon-viewport": "8.6.12",
|
||||
"@storybook/blocks": "8.6.12",
|
||||
"@storybook/nextjs": "8.6.12",
|
||||
"@storybook/react": "8.6.12",
|
||||
"@storybook/test": "8.6.12",
|
||||
"@storybook/addon-essentials": "8.6.14",
|
||||
"@storybook/addon-interactions": "8.6.14",
|
||||
"@storybook/addon-links": "8.6.14",
|
||||
"@storybook/addon-onboarding": "8.6.14",
|
||||
"@storybook/addon-viewport": "8.6.14",
|
||||
"@storybook/blocks": "8.6.14",
|
||||
"@storybook/nextjs": "8.6.14",
|
||||
"@storybook/react": "8.6.14",
|
||||
"@storybook/test": "8.6.14",
|
||||
"@types/color": "4.2.0",
|
||||
"@types/node": "22.14.1",
|
||||
"@types/papaparse": "^5.3.15",
|
||||
"@types/papaparse": "^5.3.16",
|
||||
"@types/pluralize": "^0.0.33",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/react": "19.1.2",
|
||||
"@types/react-dom": "19.1.2",
|
||||
"@types/react": "19.1.4",
|
||||
"@types/react-dom": "19.1.5",
|
||||
"@types/react-table": "^7.7.20",
|
||||
"@types/spdx-correct": "^3.1.3",
|
||||
"@types/swagger-ui-react": "^5.18.0",
|
||||
@@ -140,10 +140,10 @@
|
||||
"eslint-config-biome": "1.9.4",
|
||||
"eslint-plugin-react-compiler": "19.0.0-beta-e993439-20250405",
|
||||
"eslint-plugin-storybook": "0.12.0",
|
||||
"knip": "5.50.5",
|
||||
"knip": "5.56.0",
|
||||
"next-sitemap": "^4.2.3",
|
||||
"postcss": "8.5.3",
|
||||
"storybook": "8.6.12",
|
||||
"storybook": "8.6.14",
|
||||
"tailwindcss": "3.4.17",
|
||||
"typescript": "5.8.3"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
|
||||
export async function revalidatePathAction(
|
||||
path: string,
|
||||
type: "page" | "layout",
|
||||
) {
|
||||
revalidatePath(path, type);
|
||||
}
|
||||
@@ -48,15 +48,15 @@ async function fetchAnalytics(
|
||||
);
|
||||
}
|
||||
// client id DEBUG OVERRIDE
|
||||
// ANALYTICS_SERVICE_URL.searchParams.delete("projectId");
|
||||
// ANALYTICS_SERVICE_URL.searchParams.delete("teamId");
|
||||
// ANALYTICS_SERVICE_URL.searchParams.append(
|
||||
// analyticsServiceUrl.searchParams.delete("projectId");
|
||||
// analyticsServiceUrl.searchParams.delete("teamId");
|
||||
// analyticsServiceUrl.searchParams.append(
|
||||
// "teamId",
|
||||
// "team_clmb33q9w00gn1x0u2ri8z0k0",
|
||||
// "team_cm0lde33r02344w129k5hm2xz",
|
||||
// );
|
||||
// ANALYTICS_SERVICE_URL.searchParams.append(
|
||||
// analyticsServiceUrl.searchParams.append(
|
||||
// "projectId",
|
||||
// "prj_clyqwud5y00u1na7nzxnzlz7o",
|
||||
// "prj_cm4rqwx9b002qrnsnr37wqpo6",
|
||||
// );
|
||||
|
||||
return fetch(analyticsServiceUrl, {
|
||||
@@ -377,7 +377,7 @@ export async function getEcosystemWalletUsage(args: {
|
||||
|
||||
export async function getUniversalBridgeUsage(args: {
|
||||
teamId: string;
|
||||
projectId: string;
|
||||
projectId?: string;
|
||||
from?: Date;
|
||||
to?: Date;
|
||||
period?: "day" | "week" | "month" | "year" | "all";
|
||||
@@ -395,11 +395,10 @@ export async function getUniversalBridgeUsage(args: {
|
||||
console.error(
|
||||
`Failed to fetch universal bridge stats: ${res?.status} - ${res.statusText} - ${reason}`,
|
||||
);
|
||||
return null;
|
||||
return [];
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
return json.data as UniversalBridgeStats[];
|
||||
}
|
||||
|
||||
@@ -430,6 +429,5 @@ export async function getUniversalBridgeWalletUsage(args: {
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
return json.data as UniversalBridgeWalletStats[];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
"use server";
|
||||
|
||||
import { getAuthToken } from "app/(app)/api/lib/getAuthToken";
|
||||
import { THIRDWEB_INSIGHT_API_DOMAIN } from "constants/urls";
|
||||
|
||||
export interface WebhookResponse {
|
||||
id: string;
|
||||
name: string;
|
||||
team_id: string;
|
||||
project_id: string;
|
||||
webhook_url: string;
|
||||
webhook_secret: string;
|
||||
filters: WebhookFilters;
|
||||
suspended_at: string | null;
|
||||
suspended_reason: string | null;
|
||||
disabled: boolean;
|
||||
created_at: string;
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
export interface WebhookFilters {
|
||||
"v1.events"?: {
|
||||
chain_ids?: string[];
|
||||
addresses?: string[];
|
||||
signatures?: Array<{
|
||||
sig_hash: string;
|
||||
abi?: string;
|
||||
params?: Record<string, unknown>;
|
||||
}>;
|
||||
};
|
||||
"v1.transactions"?: {
|
||||
chain_ids?: string[];
|
||||
from_addresses?: string[];
|
||||
to_addresses?: string[];
|
||||
signatures?: Array<{
|
||||
sig_hash: string;
|
||||
abi?: string;
|
||||
params?: Record<string, unknown>;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateWebhookPayload {
|
||||
name: string;
|
||||
webhook_url: string;
|
||||
filters: WebhookFilters;
|
||||
}
|
||||
|
||||
interface WebhooksListResponse {
|
||||
data: WebhookResponse[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface WebhookSingleResponse {
|
||||
data: WebhookResponse | null;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface TestWebhookPayload {
|
||||
webhook_url: string;
|
||||
type?: "event" | "transaction";
|
||||
}
|
||||
|
||||
interface TestWebhookResponse {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export async function createWebhook(
|
||||
payload: CreateWebhookPayload,
|
||||
clientId: string,
|
||||
): Promise<WebhookSingleResponse> {
|
||||
try {
|
||||
const authToken = await getAuthToken();
|
||||
const response = await fetch(
|
||||
`https://${THIRDWEB_INSIGHT_API_DOMAIN}/v1/webhooks`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": clientId,
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
return {
|
||||
data: null,
|
||||
error: `Failed to create webhook: ${errorText}`,
|
||||
};
|
||||
}
|
||||
|
||||
return (await response.json()) as WebhookSingleResponse;
|
||||
} catch (error) {
|
||||
return {
|
||||
data: null,
|
||||
error: `Network or parsing error: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function getWebhooks(
|
||||
clientId: string,
|
||||
): Promise<WebhooksListResponse> {
|
||||
try {
|
||||
const authToken = await getAuthToken();
|
||||
const response = await fetch(
|
||||
`https://${THIRDWEB_INSIGHT_API_DOMAIN}/v1/webhooks`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"x-client-id": clientId,
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
return {
|
||||
data: [],
|
||||
error: `Failed to get webhooks: ${errorText}`,
|
||||
};
|
||||
}
|
||||
|
||||
return (await response.json()) as WebhooksListResponse;
|
||||
} catch (error) {
|
||||
return {
|
||||
data: [],
|
||||
error: `Network or parsing error: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteWebhook(
|
||||
webhookId: string,
|
||||
clientId: string,
|
||||
): Promise<WebhookSingleResponse> {
|
||||
try {
|
||||
const authToken = await getAuthToken();
|
||||
const response = await fetch(
|
||||
`https://${THIRDWEB_INSIGHT_API_DOMAIN}/v1/webhooks/${encodeURIComponent(webhookId)}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"x-client-id": clientId,
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
return {
|
||||
data: null,
|
||||
error: `Failed to delete webhook: ${errorText}`,
|
||||
};
|
||||
}
|
||||
|
||||
return (await response.json()) as WebhookSingleResponse;
|
||||
} catch (error) {
|
||||
return {
|
||||
data: null,
|
||||
error: `Network or parsing error: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function testWebhook(
|
||||
payload: TestWebhookPayload,
|
||||
clientId: string,
|
||||
): Promise<TestWebhookResponse> {
|
||||
try {
|
||||
const authToken = await getAuthToken();
|
||||
const response = await fetch(
|
||||
`https://${THIRDWEB_INSIGHT_API_DOMAIN}/v1/webhooks/test`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": clientId,
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to test webhook: ${errorText}`,
|
||||
};
|
||||
}
|
||||
|
||||
return (await response.json()) as TestWebhookResponse;
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Network or parsing error: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -84,7 +84,7 @@ export async function getDefaultTeam() {
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getLastVisitedTeamOrDefaultTeam() {
|
||||
export async function getLastVisitedTeam() {
|
||||
const token = await getAuthToken();
|
||||
if (!token) {
|
||||
return null;
|
||||
@@ -100,5 +100,5 @@ export async function getLastVisitedTeamOrDefaultTeam() {
|
||||
}
|
||||
}
|
||||
|
||||
return getDefaultTeam();
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import "server-only";
|
||||
|
||||
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
|
||||
import { UB_BASE_URL } from "./constants";
|
||||
|
||||
type PaymentLink = {
|
||||
clientId: string;
|
||||
title?: string;
|
||||
imageUrl?: string;
|
||||
receiver: string;
|
||||
destinationToken: {
|
||||
address: string;
|
||||
symbol: string;
|
||||
decimals: number;
|
||||
chainId: number;
|
||||
};
|
||||
amount: bigint | undefined;
|
||||
purchaseData: unknown;
|
||||
};
|
||||
|
||||
export async function getPaymentLink(props: {
|
||||
paymentId: string;
|
||||
}) {
|
||||
const res = await fetch(`${UB_BASE_URL}/v1/links/${props.paymentId}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-secret-key": DASHBOARD_THIRDWEB_SECRET_KEY,
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(text);
|
||||
}
|
||||
|
||||
const { data } = await res.json();
|
||||
return {
|
||||
clientId: data.clientId,
|
||||
title: data.title,
|
||||
imageUrl: data.imageUrl,
|
||||
receiver: data.receiver,
|
||||
destinationToken: data.destinationToken,
|
||||
amount: data.amount ? BigInt(data.amount) : undefined,
|
||||
purchaseData: data.purchaseData,
|
||||
} as PaymentLink;
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
"use server";
|
||||
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
|
||||
import type { ProjectResponse } from "@thirdweb-dev/service-utils";
|
||||
import { getAuthToken } from "app/(app)/api/lib/getAuthToken";
|
||||
import { UB_BASE_URL } from "./constants";
|
||||
|
||||
export type TokenMetadata = {
|
||||
@@ -37,3 +39,33 @@ export async function getUniversalBridgeTokens(props: {
|
||||
const json = await res.json();
|
||||
return json.data as Array<TokenMetadata>;
|
||||
}
|
||||
|
||||
export async function addUniversalBridgeTokenRoute(props: {
|
||||
chainId: number;
|
||||
tokenAddress: string;
|
||||
project: ProjectResponse;
|
||||
}) {
|
||||
const authToken = await getAuthToken();
|
||||
const url = new URL(`${UB_BASE_URL}/v1/tokens`);
|
||||
|
||||
const res = await fetch(url.toString(), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"x-client-id": props.project.publishableKey,
|
||||
} as Record<string, string>,
|
||||
body: JSON.stringify({
|
||||
chainId: props.chainId,
|
||||
tokenAddress: props.tokenAddress,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(text);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return json.data as Array<TokenMetadata>;
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ export function MultiNetworkSelector(props: {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Number.isInteger(Number.parseInt(searchValue))) {
|
||||
if (Number.isInteger(Number(searchValue))) {
|
||||
return String(chain.chainId).startsWith(searchValue);
|
||||
}
|
||||
return chain.name.toLowerCase().includes(searchValue.toLowerCase());
|
||||
@@ -181,7 +181,7 @@ export function SingleNetworkSelector(props: {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Number.isInteger(Number.parseInt(searchValue))) {
|
||||
if (Number.isInteger(Number(searchValue))) {
|
||||
return String(chain.chainId).startsWith(searchValue);
|
||||
}
|
||||
return chain.name.toLowerCase().includes(searchValue.toLowerCase());
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type React from "react";
|
||||
|
||||
export function RouteDiscoveryCard(
|
||||
props: React.PropsWithChildren<{
|
||||
bottomText: React.ReactNode;
|
||||
header?: {
|
||||
description: string | undefined;
|
||||
title: string;
|
||||
};
|
||||
errorText: string | undefined;
|
||||
noPermissionText: string | undefined;
|
||||
saveButton?: {
|
||||
onClick?: () => void;
|
||||
disabled: boolean;
|
||||
isPending: boolean;
|
||||
type?: "submit";
|
||||
variant?:
|
||||
| "ghost"
|
||||
| "default"
|
||||
| "primary"
|
||||
| "destructive"
|
||||
| "outline"
|
||||
| "secondary";
|
||||
className?: string;
|
||||
label?: string;
|
||||
};
|
||||
}>,
|
||||
) {
|
||||
return (
|
||||
<div className="relative rounded-lg border border-border bg-card">
|
||||
<div
|
||||
className={cn(
|
||||
"relative border-border border-b px-4 py-6 lg:px-6",
|
||||
props.noPermissionText && "cursor-not-allowed",
|
||||
)}
|
||||
>
|
||||
{props.header && (
|
||||
<>
|
||||
<h3 className="font-semibold text-xl tracking-tight">
|
||||
{props.header.title}
|
||||
</h3>
|
||||
{props.header.description && (
|
||||
<p className="mt-1.5 mb-4 text-foreground text-sm">
|
||||
{props.header.description}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{props.children}
|
||||
</div>
|
||||
|
||||
<div className="flex min-h-[60px] items-center justify-between gap-2 px-4 py-3 lg:px-6">
|
||||
{props.noPermissionText ? (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{props.noPermissionText}
|
||||
</p>
|
||||
) : props.errorText ? (
|
||||
<p className="text-destructive-text text-sm">{props.errorText}</p>
|
||||
) : (
|
||||
<p className="text-muted-foreground text-sm">{props.bottomText}</p>
|
||||
)}
|
||||
|
||||
{props.saveButton && !props.noPermissionText && (
|
||||
<Button
|
||||
size="sm"
|
||||
className={cn("gap-2", props.saveButton.className)}
|
||||
onClick={props.saveButton.onClick}
|
||||
disabled={props.saveButton.disabled || props.saveButton.isPending}
|
||||
variant={props.saveButton.variant || "outline"}
|
||||
type={props.saveButton.type}
|
||||
>
|
||||
{props.saveButton.isPending && <Spinner className="size-3" />}
|
||||
{props.saveButton.label ||
|
||||
(props.saveButton.isPending ? "Submit" : "Submit Token")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -137,7 +137,7 @@ function RenderSidebarGroup(props: {
|
||||
}
|
||||
|
||||
if ("separator" in link) {
|
||||
return <SidebarSeparator className="my-1" />;
|
||||
return <SidebarSeparator key={Math.random()} className="my-1" />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -35,14 +35,28 @@ function Variant(props: {
|
||||
label: string;
|
||||
selectedChainId?: number;
|
||||
}) {
|
||||
const [tokenAddress, setTokenAddress] = useState<string>("");
|
||||
const [token, setToken] = useState<
|
||||
| {
|
||||
address: string;
|
||||
chainId: number;
|
||||
}
|
||||
| undefined
|
||||
>(undefined);
|
||||
|
||||
return (
|
||||
<BadgeContainer label={props.label}>
|
||||
<TokenSelector
|
||||
tokenAddress={tokenAddress}
|
||||
addNativeTokenIfMissing={false}
|
||||
showCheck={false}
|
||||
selectedToken={token}
|
||||
chainId={props.selectedChainId}
|
||||
client={storybookThirdwebClient}
|
||||
onChange={setTokenAddress}
|
||||
onChange={(v) => {
|
||||
setToken({
|
||||
address: v.address,
|
||||
chainId: v.chainId,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import {
|
||||
NATIVE_TOKEN_ADDRESS,
|
||||
type ThirdwebClient,
|
||||
getAddress,
|
||||
} from "thirdweb";
|
||||
import { shortenAddress } from "thirdweb/utils";
|
||||
import { useAllChainsData } from "../../../hooks/chains/allChains";
|
||||
import { useTokensData } from "../../../hooks/tokens/tokens";
|
||||
import { replaceIpfsUrl } from "../../../lib/sdk";
|
||||
import { fallbackChainIcon } from "../../../utils/chain-icons";
|
||||
import type { TokenMetadata } from "../../api/universal-bridge/tokens";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { Badge } from "../ui/badge";
|
||||
import { Img } from "./Img";
|
||||
@@ -11,42 +17,92 @@ import { SelectWithSearch } from "./select-with-search";
|
||||
|
||||
type Option = { label: string; value: string };
|
||||
|
||||
const checksummedNativeTokenAddress = getAddress(NATIVE_TOKEN_ADDRESS);
|
||||
|
||||
export function TokenSelector(props: {
|
||||
tokenAddress: string | undefined;
|
||||
onChange: (tokenAddress: string) => void;
|
||||
selectedToken: { chainId: number; address: string } | undefined;
|
||||
onChange: (token: TokenMetadata) => void;
|
||||
className?: string;
|
||||
popoverContentClassName?: string;
|
||||
chainId?: number;
|
||||
side?: "left" | "right" | "top" | "bottom";
|
||||
disableChainId?: boolean;
|
||||
disableAddress?: boolean;
|
||||
align?: "center" | "start" | "end";
|
||||
placeholder?: string;
|
||||
client: ThirdwebClient;
|
||||
disabled?: boolean;
|
||||
enabled?: boolean;
|
||||
showCheck: boolean;
|
||||
addNativeTokenIfMissing: boolean;
|
||||
}) {
|
||||
const { tokens, isFetching } = useTokensData({
|
||||
const tokensQuery = useTokensData({
|
||||
chainId: props.chainId,
|
||||
enabled: props.enabled,
|
||||
});
|
||||
|
||||
const { idToChain } = useAllChainsData();
|
||||
|
||||
const tokens = useMemo(() => {
|
||||
if (!tokensQuery.data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (props.addNativeTokenIfMissing) {
|
||||
const hasNativeToken = tokensQuery.data.some(
|
||||
(token) => token.address === checksummedNativeTokenAddress,
|
||||
);
|
||||
|
||||
if (!hasNativeToken && props.chainId) {
|
||||
return [
|
||||
{
|
||||
name:
|
||||
idToChain.get(props.chainId)?.nativeCurrency.name ??
|
||||
"Native Token",
|
||||
symbol:
|
||||
idToChain.get(props.chainId)?.nativeCurrency.symbol ?? "ETH",
|
||||
decimals: 18,
|
||||
chainId: props.chainId,
|
||||
address: checksummedNativeTokenAddress,
|
||||
} satisfies TokenMetadata,
|
||||
...tokensQuery.data,
|
||||
];
|
||||
}
|
||||
}
|
||||
return tokensQuery.data;
|
||||
}, [
|
||||
tokensQuery.data,
|
||||
props.chainId,
|
||||
props.addNativeTokenIfMissing,
|
||||
idToChain,
|
||||
]);
|
||||
|
||||
const addressChainToToken = useMemo(() => {
|
||||
const value = new Map<string, TokenMetadata>();
|
||||
for (const token of tokens) {
|
||||
value.set(`${token.chainId}:${token.address}`, token);
|
||||
}
|
||||
return value;
|
||||
}, [tokens]);
|
||||
|
||||
const options = useMemo(() => {
|
||||
return tokens.allTokens.map((token) => {
|
||||
return {
|
||||
label: token.symbol,
|
||||
value: `${token.chainId}:${token.address}`,
|
||||
};
|
||||
});
|
||||
}, [tokens.allTokens]);
|
||||
return (
|
||||
tokens.map((token) => {
|
||||
return {
|
||||
label: token.symbol,
|
||||
value: `${token.chainId}:${token.address}`,
|
||||
};
|
||||
}) || []
|
||||
);
|
||||
}, [tokens]);
|
||||
|
||||
const searchFn = useCallback(
|
||||
(option: Option, searchValue: string) => {
|
||||
const token = tokens.addressChainToToken.get(option.value);
|
||||
const token = addressChainToToken.get(option.value);
|
||||
if (!token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Number.isInteger(Number.parseInt(searchValue))) {
|
||||
if (Number.isInteger(Number(searchValue))) {
|
||||
return String(token.chainId).startsWith(searchValue);
|
||||
}
|
||||
return (
|
||||
@@ -55,12 +111,12 @@ export function TokenSelector(props: {
|
||||
token.address.toLowerCase().includes(searchValue.toLowerCase())
|
||||
);
|
||||
},
|
||||
[tokens],
|
||||
[addressChainToToken],
|
||||
);
|
||||
|
||||
const renderOption = useCallback(
|
||||
(option: Option) => {
|
||||
const token = tokens.addressChainToToken.get(option.value);
|
||||
const token = addressChainToToken.get(option.value);
|
||||
if (!token) {
|
||||
return option.label;
|
||||
}
|
||||
@@ -87,8 +143,8 @@ export function TokenSelector(props: {
|
||||
{token.symbol}
|
||||
</span>
|
||||
|
||||
{!props.disableChainId && (
|
||||
<Badge variant="outline" className="gap-2 max-sm:hidden">
|
||||
{!props.disableAddress && (
|
||||
<Badge variant="outline" className="gap-2 py-1 max-sm:hidden">
|
||||
<span className="text-muted-foreground">Address</span>
|
||||
{shortenAddress(token.address, 4)}
|
||||
</Badge>
|
||||
@@ -96,27 +152,37 @@ export function TokenSelector(props: {
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[tokens, props.disableChainId, props.client],
|
||||
[addressChainToToken, props.disableAddress, props.client],
|
||||
);
|
||||
|
||||
const selectedValue = props.selectedToken
|
||||
? `${props.selectedToken.chainId}:${props.selectedToken.address}`
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<SelectWithSearch
|
||||
searchPlaceholder="Search by name or symbol"
|
||||
value={props.tokenAddress}
|
||||
value={selectedValue}
|
||||
options={options}
|
||||
onValueChange={(tokenAddress) => {
|
||||
props.onChange(tokenAddress);
|
||||
const token = addressChainToToken.get(tokenAddress);
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
props.onChange(token);
|
||||
}}
|
||||
closeOnSelect={true}
|
||||
showCheck={false}
|
||||
showCheck={props.showCheck}
|
||||
placeholder={
|
||||
isFetching ? "Loading Tokens..." : props.placeholder || "Select Token"
|
||||
tokensQuery.isPending
|
||||
? "Loading Tokens..."
|
||||
: props.placeholder || "Select Token"
|
||||
}
|
||||
overrideSearchFn={searchFn}
|
||||
renderOption={renderOption}
|
||||
className={props.className}
|
||||
popoverContentClassName={props.popoverContentClassName}
|
||||
disabled={isFetching || props.disabled}
|
||||
disabled={tokensQuery.isPending || props.disabled}
|
||||
side={props.side}
|
||||
align={props.align}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { ArrowRightIcon, RocketIcon, StarIcon } from "lucide-react";
|
||||
import { BadgeContainer } from "../../../stories/utils";
|
||||
import { UpsellBannerCard } from "./UpsellBannerCard";
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="container flex max-w-4xl flex-col gap-8 py-10">
|
||||
<BadgeContainer label="Green with icon (default)">
|
||||
<UpsellBannerCard
|
||||
title="Unlock more with thirdweb"
|
||||
description="Upgrade to increase limits and access advanced features."
|
||||
cta={{
|
||||
text: "View plans",
|
||||
icon: <ArrowRightIcon className="size-4" />,
|
||||
link: "#",
|
||||
}}
|
||||
trackingCategory="storybook"
|
||||
trackingLabel="green"
|
||||
icon={<RocketIcon className="size-5" />}
|
||||
accentColor="green"
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Blue accent">
|
||||
<UpsellBannerCard
|
||||
title="Need more storage?"
|
||||
description="Add additional space to your account."
|
||||
cta={{
|
||||
text: "Upgrade storage",
|
||||
icon: <ArrowRightIcon className="size-4" />,
|
||||
link: "#",
|
||||
}}
|
||||
trackingCategory="storybook"
|
||||
trackingLabel="blue"
|
||||
icon={<StarIcon className="size-5" />}
|
||||
accentColor="blue"
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Purple without leading icon">
|
||||
<UpsellBannerCard
|
||||
title="Join the beta"
|
||||
description="Get early access to experimental features."
|
||||
cta={{
|
||||
text: "Request access",
|
||||
icon: <ArrowRightIcon className="size-4" />,
|
||||
link: "#",
|
||||
}}
|
||||
trackingCategory="storybook"
|
||||
trackingLabel="purple"
|
||||
accentColor="purple"
|
||||
/>
|
||||
</BadgeContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const meta = {
|
||||
title: "blocks/Banners/UpsellBannerCard",
|
||||
component: Story,
|
||||
parameters: {
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof Story>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
@@ -0,0 +1,119 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { TrackedLinkTW } from "@/components/ui/tracked-link";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type React from "react";
|
||||
|
||||
const ACCENT = {
|
||||
green: {
|
||||
border: "border-green-600 dark:border-green-700",
|
||||
bgFrom: "from-green-50 dark:from-green-900/20",
|
||||
blur: "bg-green-600",
|
||||
title: "text-green-900 dark:text-green-200",
|
||||
desc: "text-green-800 dark:text-green-300",
|
||||
iconBg: "bg-green-600 text-white",
|
||||
btn: "bg-green-600 text-white hover:bg-green-700",
|
||||
},
|
||||
blue: {
|
||||
border: "border-blue-600 dark:border-blue-700",
|
||||
bgFrom: "from-blue-50 dark:from-blue-900/20",
|
||||
blur: "bg-blue-600",
|
||||
title: "text-blue-900 dark:text-blue-200",
|
||||
desc: "text-blue-800 dark:text-blue-300",
|
||||
iconBg: "bg-blue-600 text-white",
|
||||
btn: "bg-blue-600 text-white hover:bg-blue-700",
|
||||
},
|
||||
purple: {
|
||||
border: "border-purple-600 dark:border-purple-700",
|
||||
bgFrom: "from-purple-50 dark:from-purple-900/20",
|
||||
blur: "bg-purple-600",
|
||||
title: "text-purple-900 dark:text-purple-200",
|
||||
desc: "text-purple-800 dark:text-purple-300",
|
||||
iconBg: "bg-purple-600 text-white",
|
||||
btn: "bg-purple-600 text-white hover:bg-purple-700",
|
||||
},
|
||||
} as const;
|
||||
|
||||
type UpsellBannerCardProps = {
|
||||
title: React.ReactNode;
|
||||
description: React.ReactNode;
|
||||
cta: {
|
||||
text: React.ReactNode;
|
||||
icon?: React.ReactNode;
|
||||
link: string;
|
||||
};
|
||||
trackingCategory: string;
|
||||
trackingLabel: string;
|
||||
accentColor?: keyof typeof ACCENT;
|
||||
icon?: React.ReactNode;
|
||||
};
|
||||
|
||||
export function UpsellBannerCard(props: UpsellBannerCardProps) {
|
||||
const color = ACCENT[props.accentColor || "green"];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative overflow-hidden rounded-lg border bg-gradient-to-r p-5",
|
||||
color.border,
|
||||
color.bgFrom,
|
||||
)}
|
||||
>
|
||||
{/* Decorative blur */}
|
||||
<div
|
||||
className={cn(
|
||||
"-right-10 -top-10 pointer-events-none absolute size-28 rounded-full opacity-20 blur-2xl",
|
||||
color.blur,
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="relative flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-start gap-3">
|
||||
{props.icon ? (
|
||||
<div
|
||||
className={cn(
|
||||
"mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-full",
|
||||
color.iconBg,
|
||||
)}
|
||||
>
|
||||
{props.icon}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div>
|
||||
<h3
|
||||
className={cn(
|
||||
"font-semibold text-lg tracking-tight",
|
||||
color.title,
|
||||
)}
|
||||
>
|
||||
{props.title}
|
||||
</h3>
|
||||
<p className={cn("mt-0.5 text-sm", color.desc)}>
|
||||
{props.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
asChild
|
||||
size="sm"
|
||||
className={cn(
|
||||
"mt-2 gap-2 hover:translate-y-0 hover:shadow-inner sm:mt-0",
|
||||
color.btn,
|
||||
)}
|
||||
>
|
||||
<TrackedLinkTW
|
||||
href={props.cta.link}
|
||||
category={props.trackingCategory}
|
||||
label={props.trackingLabel}
|
||||
>
|
||||
{props.cta.text}
|
||||
{props.cta.icon && <span className="ml-2">{props.cta.icon}</span>}
|
||||
</TrackedLinkTW>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
"use client";
|
||||
|
||||
import { XIcon } from "lucide-react";
|
||||
import { useLocalStorage } from "../../../hooks/useLocalStorage";
|
||||
import { Button } from "../ui/button";
|
||||
|
||||
export function DismissibleAlert(props: {
|
||||
title: React.ReactNode;
|
||||
description: React.ReactNode;
|
||||
localStorageId: string;
|
||||
}) {
|
||||
const [isVisible, setIsVisible] = useLocalStorage(
|
||||
props.localStorageId,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
return (
|
||||
<div className="relative rounded-lg border border-border bg-card p-4">
|
||||
<Button
|
||||
onClick={() => setIsVisible(false)}
|
||||
className="absolute top-4 right-4 h-auto w-auto p-1 text-muted-foreground"
|
||||
aria-label="Close alert"
|
||||
variant="ghost"
|
||||
>
|
||||
<XIcon className="size-5" />
|
||||
</Button>
|
||||
<div>
|
||||
<h2 className="mb-0.5 font-semibold">{props.title} </h2>
|
||||
<div className="text-muted-foreground text-sm">{props.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type Segment = {
|
||||
label: string;
|
||||
percent: number;
|
||||
color: string;
|
||||
};
|
||||
|
||||
type DistributionBarChartProps = {
|
||||
segments: Segment[];
|
||||
title: string;
|
||||
};
|
||||
export function DistributionBarChart(props: DistributionBarChartProps) {
|
||||
const totalPercentage = props.segments.reduce(
|
||||
(sum, segment) => sum + segment.percent,
|
||||
0,
|
||||
);
|
||||
|
||||
const invalidTotalPercentage = totalPercentage !== 100;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<h3 className="font-medium text-sm">{props.title}</h3>
|
||||
<div
|
||||
className={cn(
|
||||
"font-medium text-muted-foreground text-sm",
|
||||
invalidTotalPercentage && "text-red-500",
|
||||
)}
|
||||
>
|
||||
Total: {totalPercentage}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bar */}
|
||||
<div className="flex h-3 overflow-hidden rounded-lg">
|
||||
{props.segments.map((segment) => {
|
||||
return (
|
||||
<div
|
||||
key={segment.label}
|
||||
className="flex h-full items-center justify-center transition-all duration-200"
|
||||
style={{
|
||||
width: `${segment.percent}%`,
|
||||
backgroundColor: segment.color,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Legends */}
|
||||
<div className="mt-3 flex flex-col gap-1 lg:flex-row lg:gap-6">
|
||||
{props.segments.map((segment) => {
|
||||
return (
|
||||
<div key={segment.label} className="flex items-center gap-1.5">
|
||||
<div
|
||||
className="size-3 rounded-full"
|
||||
style={{
|
||||
backgroundColor: segment.color,
|
||||
}}
|
||||
/>
|
||||
<p className="text-sm">
|
||||
{segment.label}: {segment.percent}%
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { MultiStepStatus } from "./multi-step-status";
|
||||
|
||||
const meta = {
|
||||
title: "Blocks/MultiStepStatus",
|
||||
component: MultiStepStatus,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="container w-full max-w-md py-10">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
} satisfies Meta<typeof MultiStepStatus>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
export const AllStates: Story = {
|
||||
args: {
|
||||
steps: [
|
||||
{
|
||||
status: "completed",
|
||||
label: "Connect Wallet",
|
||||
retryLabel: "Failed to connect wallet",
|
||||
execute: async () => {
|
||||
await sleep(1000);
|
||||
},
|
||||
},
|
||||
{
|
||||
status: "pending",
|
||||
label: "Sign Message",
|
||||
retryLabel: "Failed to sign message",
|
||||
execute: async () => {
|
||||
await sleep(1000);
|
||||
},
|
||||
},
|
||||
{
|
||||
status: "error",
|
||||
label: "Approve Transaction",
|
||||
retryLabel: "Transaction approval failed",
|
||||
execute: async () => {
|
||||
await sleep(1000);
|
||||
},
|
||||
},
|
||||
{
|
||||
status: "idle",
|
||||
label: "Confirm Transaction",
|
||||
retryLabel: "Transaction confirmation failed",
|
||||
execute: async () => {
|
||||
await sleep(1000);
|
||||
},
|
||||
},
|
||||
{
|
||||
status: "idle",
|
||||
label: "Finalize",
|
||||
retryLabel: "Finalization failed",
|
||||
execute: async () => {
|
||||
await sleep(1000);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
AlertCircleIcon,
|
||||
CircleCheckIcon,
|
||||
CircleIcon,
|
||||
RefreshCwIcon,
|
||||
} from "lucide-react";
|
||||
import { DynamicHeight } from "../../ui/DynamicHeight";
|
||||
import { Spinner } from "../../ui/Spinner/Spinner";
|
||||
|
||||
export type MultiStepState = {
|
||||
status: "idle" | "pending" | "completed" | "error";
|
||||
retryLabel: string;
|
||||
label: string;
|
||||
execute: () => Promise<void>;
|
||||
};
|
||||
|
||||
export function MultiStepStatus(props: {
|
||||
steps: MultiStepState[];
|
||||
}) {
|
||||
return (
|
||||
<DynamicHeight>
|
||||
<div className="space-y-4">
|
||||
{props.steps.map((step) => (
|
||||
<div key={step.label} className="flex items-start space-x-3 ">
|
||||
{step.status === "completed" ? (
|
||||
<CircleCheckIcon className="mt-0.5 size-5 flex-shrink-0 text-green-500" />
|
||||
) : step.status === "pending" ? (
|
||||
<Spinner className="mt-0.5 size-5 flex-shrink-0 text-foreground" />
|
||||
) : step.status === "error" ? (
|
||||
<AlertCircleIcon className="mt-0.5 size-5 flex-shrink-0 text-red-500" />
|
||||
) : (
|
||||
<CircleIcon className="mt-0.5 size-5 flex-shrink-0 text-muted-foreground/70" />
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<p
|
||||
className={`font-medium ${
|
||||
step.status === "pending"
|
||||
? "text-foreground"
|
||||
: step.status === "completed"
|
||||
? "text-green-500"
|
||||
: step.status === "error"
|
||||
? "text-red-500"
|
||||
: "text-muted-foreground/70"
|
||||
}`}
|
||||
>
|
||||
{step.label}
|
||||
</p>
|
||||
|
||||
{step.status === "error" && (
|
||||
<div className="mt-1 space-y-2">
|
||||
<p className="mb-1 text-red-500 text-sm">{step.retryLabel}</p>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="gap-2"
|
||||
onClick={() => step.execute()}
|
||||
>
|
||||
<RefreshCwIcon className="size-4" />
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</DynamicHeight>
|
||||
);
|
||||
}
|
||||
@@ -42,7 +42,7 @@ export function CopyTextButton(props: {
|
||||
variant={props.variant || "outline"}
|
||||
aria-label={props.tooltip}
|
||||
className={cn(
|
||||
"flex h-auto w-auto gap-2 rounded-lg px-2.5 py-1.5 font-normal text-foreground",
|
||||
"flex h-auto w-auto gap-2 rounded-lg px-1.5 py-0.5 font-normal text-foreground",
|
||||
props.className,
|
||||
)}
|
||||
onClick={(e) => {
|
||||
|
||||
@@ -12,6 +12,7 @@ export type ChartConfig = {
|
||||
[k in string]: {
|
||||
label?: React.ReactNode;
|
||||
icon?: React.ComponentType;
|
||||
isCurrency?: boolean;
|
||||
} & (
|
||||
| { color?: string; theme?: never }
|
||||
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
||||
|
||||
@@ -11,6 +11,7 @@ interface ImageUploadProps {
|
||||
variant?: React.ComponentProps<typeof Button>["variant"];
|
||||
className?: string;
|
||||
multiple?: boolean;
|
||||
accept: string;
|
||||
}
|
||||
|
||||
export function ImageUploadButton(props: ImageUploadProps) {
|
||||
@@ -34,7 +35,7 @@ export function ImageUploadButton(props: ImageUploadProps) {
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple={props.multiple}
|
||||
accept="image/*"
|
||||
accept={props.accept}
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
aria-label="Upload image"
|
||||
|
||||
@@ -54,11 +54,11 @@ export function ToolTipLabel(props: {
|
||||
align={props.align}
|
||||
sideOffset={10}
|
||||
className={cn(
|
||||
"max-w-[400px] whitespace-normal leading-relaxed",
|
||||
"max-w-[400px] whitespace-normal p-0 leading-relaxed",
|
||||
props.contentClassName,
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-1.5 p-2 text-sm">
|
||||
<div className="flex items-center gap-1.5 p-4 text-sm">
|
||||
{props.leftIcon}
|
||||
{props.label}
|
||||
{props.rightIcon}
|
||||
|
||||
@@ -36,14 +36,6 @@ if (API_SERVER_SECRET) {
|
||||
|
||||
export const THIRDWEB_ENGINE_URL = process.env.THIRDWEB_ENGINE_URL || "";
|
||||
|
||||
if (THIRDWEB_ENGINE_URL) {
|
||||
experimental_taintUniqueValue(
|
||||
"Do not pass THIRDWEB_ENGINE_URL to the client",
|
||||
process,
|
||||
THIRDWEB_ENGINE_URL,
|
||||
);
|
||||
}
|
||||
|
||||
export const THIRDWEB_ACCESS_TOKEN = process.env.THIRDWEB_ACCESS_TOKEN || "";
|
||||
|
||||
if (THIRDWEB_ACCESS_TOKEN) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
NEXT_PUBLIC_IPFS_GATEWAY_URL,
|
||||
} from "@/constants/public-envs";
|
||||
import {
|
||||
THIRDWEB_BRIDGE_URL,
|
||||
THIRDWEB_BUNDLER_DOMAIN,
|
||||
THIRDWEB_INAPP_WALLET_DOMAIN,
|
||||
THIRDWEB_INSIGHT_API_DOMAIN,
|
||||
@@ -35,6 +36,7 @@ export function getConfiguredThirdwebClient(options: {
|
||||
social: THIRDWEB_SOCIAL_API_DOMAIN,
|
||||
bundler: THIRDWEB_BUNDLER_DOMAIN,
|
||||
insight: THIRDWEB_INSIGHT_API_DOMAIN,
|
||||
bridge: THIRDWEB_BRIDGE_URL,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -39,9 +39,10 @@ export function getFiltersFromSearchParams(params: {
|
||||
};
|
||||
|
||||
const defaultInterval =
|
||||
differenceInCalendarDays(range.to, range.from) > 30
|
||||
? "week"
|
||||
: ("day" as const);
|
||||
params.interval ??
|
||||
(differenceInCalendarDays(range.to, range.from) > 30
|
||||
? ("week" as const)
|
||||
: ("day" as const));
|
||||
|
||||
return {
|
||||
range,
|
||||
@@ -50,6 +51,6 @@ export function getFiltersFromSearchParams(params: {
|
||||
? ("day" as const)
|
||||
: params.interval === "week"
|
||||
? ("week" as const)
|
||||
: defaultInterval,
|
||||
: (defaultInterval as "day" | "week"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ export type Account = {
|
||||
image?: string | null;
|
||||
name?: string;
|
||||
email?: string;
|
||||
advancedEnabled: boolean;
|
||||
emailConfirmedAt?: string;
|
||||
unconfirmedEmail?: string;
|
||||
emailConfirmationWalletAddress?: string;
|
||||
@@ -41,7 +40,6 @@ export type Account = {
|
||||
billing: "email" | "none";
|
||||
updates: "email" | "none";
|
||||
};
|
||||
// TODO - add image URL
|
||||
};
|
||||
|
||||
interface UpdateAccountNotificationsInput {
|
||||
@@ -53,27 +51,6 @@ interface ConfirmEmailInput {
|
||||
confirmationToken: string;
|
||||
}
|
||||
|
||||
interface UsageStorage {
|
||||
sumFileSizeBytes: number;
|
||||
}
|
||||
|
||||
export interface UsageBillableByService {
|
||||
usage: {
|
||||
storage: UsageStorage;
|
||||
};
|
||||
limits: {
|
||||
storage: number;
|
||||
};
|
||||
rateLimits: {
|
||||
storage: number;
|
||||
rpc: number;
|
||||
};
|
||||
rateLimitedAt: {
|
||||
storage?: string;
|
||||
rpc?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface BillingProduct {
|
||||
name: string;
|
||||
id: string;
|
||||
@@ -345,13 +322,14 @@ export type RotateSecretKeyAPIReturnType = {
|
||||
};
|
||||
};
|
||||
|
||||
export async function rotateSecretKeyClient(projectId: string) {
|
||||
export async function rotateSecretKeyClient(params: {
|
||||
teamId: string;
|
||||
projectId: string;
|
||||
}) {
|
||||
const res = await apiServerProxy<RotateSecretKeyAPIReturnType>({
|
||||
pathname: "/v2/keys/rotate-secret-key",
|
||||
pathname: `/v1/teams/${params.teamId}/projects/${params.projectId}/rotate-secret-key`,
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
projectId,
|
||||
}),
|
||||
body: JSON.stringify({}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
|
||||
@@ -11,8 +11,8 @@ import type { EngineBackendWalletType } from "lib/engine";
|
||||
import { useState } from "react";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import invariant from "tiny-invariant";
|
||||
import type { ResultItem } from "../../../app/(app)/team/[team_slug]/[project_slug]/engine/dedicated/(instance)/[engineId]/metrics/components/StatusCodes";
|
||||
import type { EngineStatus } from "../../../app/(app)/team/[team_slug]/[project_slug]/engine/dedicated/(instance)/[engineId]/overview/components/transactions-table";
|
||||
import type { ResultItem } from "../../../app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/metrics/components/StatusCodes";
|
||||
import type { EngineStatus } from "../../../app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/overview/components/transactions-table";
|
||||
import { engineKeys } from "../cache-keys";
|
||||
|
||||
// Engine instances
|
||||
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { getSDKTheme } from "app/(app)/components/sdk-component-theme";
|
||||
import { useV5DashboardChain } from "lib/v5-adapter";
|
||||
import { useTheme } from "next-themes";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { PayEmbed } from "thirdweb/react";
|
||||
|
||||
export function UniversalBridgeEmbed({
|
||||
chainId,
|
||||
client,
|
||||
}: { chainId?: number; client: ThirdwebClient }) {
|
||||
const { theme } = useTheme();
|
||||
const chain = useV5DashboardChain(chainId || 1);
|
||||
|
||||
return (
|
||||
<PayEmbed
|
||||
client={client}
|
||||
payOptions={{
|
||||
mode: "fund_wallet",
|
||||
prefillBuy: {
|
||||
chain,
|
||||
amount: "0.01",
|
||||
},
|
||||
}}
|
||||
theme={getSDKTheme(theme === "light" ? "light" : "dark")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import type { Metadata } from "next";
|
||||
import { getClientThirdwebClient } from "../../../../../@/constants/thirdweb-client.client";
|
||||
import { UniversalBridgeEmbed } from "./components/client/UniversalBridgeEmbed";
|
||||
|
||||
const title = "Universal Bridge: Swap, Bridge, and On-Ramp";
|
||||
const description =
|
||||
"Swap, bridge, and on-ramp to any EVM chain with thirdweb's Universal Bridge.";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title,
|
||||
description,
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
},
|
||||
};
|
||||
|
||||
export default async function RoutesPage({
|
||||
searchParams,
|
||||
}: { searchParams: Record<string, string | string[]> }) {
|
||||
const { chainId } = searchParams;
|
||||
const client = getClientThirdwebClient(undefined);
|
||||
return (
|
||||
<div className="relative mx-auto flex h-screen w-full flex-col items-center justify-center overflow-hidden border py-10">
|
||||
<main className="container z-10 flex justify-center">
|
||||
<UniversalBridgeEmbed
|
||||
chainId={chainId ? Number(chainId) : undefined}
|
||||
client={client}
|
||||
/>
|
||||
</main>
|
||||
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
alt=""
|
||||
src="/assets/login/background.svg"
|
||||
className="-bottom-12 -right-12 pointer-events-none absolute lg:right-0 lg:bottom-0"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 536 KiB |
+1
-1
@@ -47,7 +47,7 @@ export const SearchInput: React.FC = () => {
|
||||
<div className="group relative w-full">
|
||||
<SearchIcon className="-translate-y-1/2 absolute top-[50%] left-3 size-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search with anything!"
|
||||
placeholder="Search by token name or symbol"
|
||||
className="h-10 rounded-lg bg-card py-2 pl-9 lg:min-w-[300px]"
|
||||
defaultValue={searchParams?.get("query") || ""}
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
|
||||
+15
-15
@@ -52,20 +52,20 @@ export async function RouteListRow({
|
||||
]);
|
||||
|
||||
return (
|
||||
<TableRow linkBox className="hover:bg-accent/50">
|
||||
<TableRow linkBox className="group transition-colors hover:bg-accent/50">
|
||||
<TableCell>
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex flex-row items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
{resolvedOriginTokenIconUri ? (
|
||||
// For now we're using a normal img tag because the domain for these images is unknown
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={resolvedOriginTokenIconUri}
|
||||
alt={originTokenAddress}
|
||||
className="size-6 rounded-full border border-muted-foreground"
|
||||
className="size-7 rounded-full border border-border/50 shadow-sm transition-transform group-hover:scale-105"
|
||||
/>
|
||||
) : (
|
||||
<div className="size-6 rounded-full bg-muted-foreground" />
|
||||
<div className="size-7 rounded-full bg-muted-foreground/20" />
|
||||
)}
|
||||
{originTokenSymbol && (
|
||||
<CopyTextButton
|
||||
@@ -76,7 +76,7 @@ export async function RouteListRow({
|
||||
: originTokenSymbol
|
||||
}
|
||||
tooltip="Copy Token Address"
|
||||
className="relative z-10 text-base"
|
||||
className="relative z-10 font-medium text-base"
|
||||
variant="ghost"
|
||||
copyIconPosition="right"
|
||||
/>
|
||||
@@ -85,22 +85,22 @@ export async function RouteListRow({
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-muted-foreground">
|
||||
{originChain.name}
|
||||
<TableCell className="text-muted-foreground/90">
|
||||
<span className="font-medium">{originChain.name}</span>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex flex-row items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
{resolvedDestinationTokenIconUri ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={resolvedDestinationTokenIconUri}
|
||||
alt={destinationTokenAddress}
|
||||
className="size-6 rounded-full border border-muted-foreground"
|
||||
className="size-7 rounded-full border border-border/50 shadow-sm transition-transform group-hover:scale-105"
|
||||
/>
|
||||
) : (
|
||||
<div className="size-6 rounded-full bg-muted-foreground" />
|
||||
<div className="size-7 rounded-full bg-muted-foreground/20" />
|
||||
)}
|
||||
{destinationTokenSymbol && (
|
||||
<CopyTextButton
|
||||
@@ -111,7 +111,7 @@ export async function RouteListRow({
|
||||
: destinationTokenSymbol
|
||||
}
|
||||
tooltip="Copy Token Address"
|
||||
className="relative z-10 text-base"
|
||||
className="relative z-10 font-medium text-base"
|
||||
variant="ghost"
|
||||
copyIconPosition="right"
|
||||
/>
|
||||
@@ -120,8 +120,8 @@ export async function RouteListRow({
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-muted-foreground">
|
||||
{destinationChain.name}
|
||||
<TableCell className="text-muted-foreground/90">
|
||||
<span className="font-medium">{destinationChain.name}</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
+36
-85
@@ -7,7 +7,6 @@ import {
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import type { Address } from "thirdweb";
|
||||
import { checksumAddress } from "thirdweb/utils";
|
||||
import { getRoutes } from "../../../utils";
|
||||
import { ChainlistPagination } from "../client/pagination";
|
||||
import { RouteListCard } from "./routelist-card";
|
||||
@@ -25,18 +24,15 @@ export type SearchParams = Partial<{
|
||||
|
||||
// 120 is divisible by 2, 3, and 4 so card layout looks nice
|
||||
const DEFAULT_PAGE_SIZE = 120;
|
||||
const DEFAULT_PAGE = 1;
|
||||
|
||||
async function getRoutesToRender(params: SearchParams) {
|
||||
const filters: Partial<{
|
||||
limit: number;
|
||||
offset: number;
|
||||
originQuery?: string;
|
||||
destinationQuery?: string;
|
||||
originChainId?: number;
|
||||
originTokenAddress?: Address;
|
||||
destinationChainId?: number;
|
||||
destinationTokenAddress?: Address;
|
||||
originTextQuery?: string;
|
||||
destinationTextQuery?: string;
|
||||
}> = {};
|
||||
|
||||
if (params.type === "origin" || typeof params.type === "undefined") {
|
||||
@@ -45,7 +41,7 @@ async function getRoutesToRender(params: SearchParams) {
|
||||
} else if (Number.isInteger(Number(params.query))) {
|
||||
filters.originChainId = Number(params.query);
|
||||
} else if (params.query) {
|
||||
filters.originTextQuery = params.query;
|
||||
filters.originQuery = params.query;
|
||||
}
|
||||
} else if (params.type === "destination") {
|
||||
if (params.query?.startsWith("0x")) {
|
||||
@@ -53,69 +49,20 @@ async function getRoutesToRender(params: SearchParams) {
|
||||
} else if (Number.isInteger(Number(params.query))) {
|
||||
filters.destinationChainId = Number(params.query);
|
||||
} else if (params.query) {
|
||||
filters.destinationTextQuery = params.query;
|
||||
filters.destinationQuery = params.query;
|
||||
}
|
||||
}
|
||||
// Temporary, will update this after the /routes endpoint
|
||||
let routes = await getRoutes({ limit: 100_000 });
|
||||
|
||||
const totalCount = routes.length;
|
||||
|
||||
if (filters.originChainId) {
|
||||
routes = routes.filter(
|
||||
(route) => route.originToken.chainId === filters.originChainId,
|
||||
);
|
||||
}
|
||||
if (filters.originTokenAddress) {
|
||||
const originTokenAddress = filters.originTokenAddress;
|
||||
routes = routes.filter(
|
||||
(route) =>
|
||||
checksumAddress(route.originToken.address) ===
|
||||
checksumAddress(originTokenAddress),
|
||||
);
|
||||
}
|
||||
if (filters.destinationChainId) {
|
||||
routes = routes.filter(
|
||||
(route) => route.destinationToken.chainId === filters.destinationChainId,
|
||||
);
|
||||
}
|
||||
if (filters.destinationTokenAddress) {
|
||||
const destinationTokenAddress = filters.destinationTokenAddress;
|
||||
routes = routes.filter(
|
||||
(route) =>
|
||||
checksumAddress(route.destinationToken.address) ===
|
||||
checksumAddress(destinationTokenAddress),
|
||||
);
|
||||
}
|
||||
|
||||
if (filters.originTextQuery) {
|
||||
const originTextQuery = filters.originTextQuery.toLowerCase();
|
||||
routes = routes.filter((route) => {
|
||||
return (
|
||||
route.originToken.name.toLowerCase().includes(originTextQuery) ||
|
||||
route.originToken.symbol.toLowerCase().includes(originTextQuery)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (filters.destinationTextQuery) {
|
||||
const destinationTextQuery = filters.destinationTextQuery.toLowerCase();
|
||||
routes = routes.filter((route) => {
|
||||
return (
|
||||
route.destinationToken.name
|
||||
.toLowerCase()
|
||||
.includes(destinationTextQuery) ||
|
||||
route.destinationToken.symbol
|
||||
.toLowerCase()
|
||||
.includes(destinationTextQuery)
|
||||
);
|
||||
});
|
||||
}
|
||||
const routes = await getRoutes({
|
||||
limit: DEFAULT_PAGE_SIZE,
|
||||
offset: DEFAULT_PAGE_SIZE * ((params.page || 1) - 1),
|
||||
originQuery: filters.originQuery,
|
||||
destinationQuery: filters.destinationQuery,
|
||||
});
|
||||
|
||||
return {
|
||||
routesToRender: routes,
|
||||
totalCount,
|
||||
filteredCount: routes.length,
|
||||
routesToRender: routes.data,
|
||||
totalCount: routes.meta.totalCount,
|
||||
filteredCount: routes.meta.filteredCount,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -128,36 +75,37 @@ export async function RoutesData(props: {
|
||||
props.searchParams,
|
||||
);
|
||||
|
||||
// pagination
|
||||
const totalPages = Math.ceil(routesToRender.length / DEFAULT_PAGE_SIZE);
|
||||
|
||||
const activePage = Number(props.searchParams.page || DEFAULT_PAGE);
|
||||
const pageSize = DEFAULT_PAGE_SIZE;
|
||||
const startIndex = (activePage - 1) * pageSize;
|
||||
const endIndex = startIndex + pageSize;
|
||||
const paginatedRoutes = routesToRender.slice(startIndex, endIndex);
|
||||
const totalPages = Math.ceil(filteredCount / DEFAULT_PAGE_SIZE);
|
||||
|
||||
return (
|
||||
<>
|
||||
<main>
|
||||
{/* empty state */}
|
||||
{paginatedRoutes.length === 0 ? (
|
||||
{routesToRender.length === 0 ? (
|
||||
<div className="flex h-[300px] items-center justify-center rounded-lg border p-8 lg:h-[500px]">
|
||||
<p className="text-2xl">No Results found</p>
|
||||
</div>
|
||||
) : props.activeView === "table" ? (
|
||||
<TableContainer>
|
||||
<TableContainer className="overflow-hidden rounded-xl border border-border/50 bg-card/50 shadow-sm transition-all">
|
||||
<Table>
|
||||
<TableHeader className="z-0">
|
||||
<TableRow>
|
||||
<TableHead>Origin Token</TableHead>
|
||||
<TableHead>Origin Chain</TableHead>
|
||||
<TableHead>Destination Token</TableHead>
|
||||
<TableHead>Destination Chain</TableHead>
|
||||
<TableRow className="border-border/50 border-b bg-muted/50">
|
||||
<TableHead className="py-4 font-medium text-muted-foreground/80 text-xs uppercase tracking-wider">
|
||||
Origin Token
|
||||
</TableHead>
|
||||
<TableHead className="py-4 font-medium text-muted-foreground/80 text-xs uppercase tracking-wider">
|
||||
Origin Chain
|
||||
</TableHead>
|
||||
<TableHead className="py-4 font-medium text-muted-foreground/80 text-xs uppercase tracking-wider">
|
||||
Destination Token
|
||||
</TableHead>
|
||||
<TableHead className="py-4 font-medium text-muted-foreground/80 text-xs uppercase tracking-wider">
|
||||
Destination Chain
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{paginatedRoutes.map((route) => (
|
||||
{routesToRender.map((route) => (
|
||||
<RouteListRow
|
||||
key={`${route.originToken.chainId}:${route.originToken.address}-${route.destinationToken.chainId}:${route.destinationToken.address}`}
|
||||
originChainId={route.originToken.chainId}
|
||||
@@ -177,7 +125,7 @@ export async function RoutesData(props: {
|
||||
</TableContainer>
|
||||
) : (
|
||||
<ul className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{paginatedRoutes.map((route) => (
|
||||
{routesToRender.map((route) => (
|
||||
<li
|
||||
key={`${route.originToken.chainId}:${route.originToken.address}-${route.destinationToken.chainId}:${route.destinationToken.address}`}
|
||||
className="h-full"
|
||||
@@ -201,12 +149,15 @@ export async function RoutesData(props: {
|
||||
</main>
|
||||
<div className="h-10" />
|
||||
{totalPages > 1 && (
|
||||
<ChainlistPagination totalPages={totalPages} activePage={activePage} />
|
||||
<ChainlistPagination
|
||||
totalPages={totalPages}
|
||||
activePage={Number(props.searchParams.page) || 1}
|
||||
/>
|
||||
)}
|
||||
<div className="h-4" />
|
||||
<p className="text-balance text-center text-muted-foreground text-sm">
|
||||
Showing{" "}
|
||||
<span className="text-accent-foreground">{paginatedRoutes.length}</span>{" "}
|
||||
<span className="text-accent-foreground">{routesToRender.length}</span>{" "}
|
||||
out of{" "}
|
||||
{filteredCount !== totalCount ? (
|
||||
<>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ArrowUpRightIcon } from "lucide-react";
|
||||
import type { Metadata } from "next";
|
||||
import { headers } from "next/headers";
|
||||
import { getAuthToken } from "../../../api/lib/getAuthToken";
|
||||
@@ -9,7 +10,7 @@ import {
|
||||
type SearchParams,
|
||||
} from "./components/server/routes-table";
|
||||
|
||||
const title = "Routes: Swap, Bridge, and On-Ramp";
|
||||
const title = "Universal Bridge Routes: Swap, Bridge, and Onramp";
|
||||
const description =
|
||||
"A list of token routes for swapping, bridging, and on-ramping between EVM chains with thirdweb.";
|
||||
|
||||
@@ -43,15 +44,15 @@ export default async function RoutesPage(props: {
|
||||
|
||||
return (
|
||||
<section className="container mx-auto flex h-full flex-col px-4 py-10">
|
||||
<header className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div className="flex flex-row items-center justify-between gap-4 lg:flex-col lg:justify-start">
|
||||
<header className="flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="font-semibold text-4xl tracking-tighter lg:text-5xl">
|
||||
Routes
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex flex-row items-end gap-4 lg:flex-col ">
|
||||
<div className="flex w-full flex-row gap-4">
|
||||
<div className="flex flex-row items-end gap-4 lg:flex-col">
|
||||
<div className="flex w-full flex-row items-center gap-4">
|
||||
<SearchInput />
|
||||
<QueryType activeType={activeType} />
|
||||
<RouteListView activeView={activeView} />
|
||||
@@ -60,6 +61,29 @@ export default async function RoutesPage(props: {
|
||||
</div>
|
||||
</header>
|
||||
<div className="h-10" />
|
||||
<div className="relative overflow-hidden rounded-lg border-2 border-green-500/20 bg-gradient-to-br from-card/80 to-card/50 p-4 shadow-[inset_0_1px_2px_0_rgba(0,0,0,0.02)]">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-green-500/5 to-transparent" />
|
||||
<div className="relative flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="font-medium text-lg">
|
||||
Get Started with Universal Bridge
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Simple, instant, and secure payments across any token and chain.
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
href="https://portal.thirdweb.com/pay"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 rounded-md bg-green-600 px-4 py-2 font-medium text-sm text-white transition-all hover:bg-green-600/90 hover:shadow-sm"
|
||||
>
|
||||
Learn More
|
||||
<ArrowUpRightIcon className="size-4" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-10" />
|
||||
<RoutesData
|
||||
searchParams={searchParams}
|
||||
activeView={activeView}
|
||||
|
||||
@@ -2,35 +2,69 @@ import "server-only";
|
||||
|
||||
import { NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST } from "@/constants/public-envs";
|
||||
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
|
||||
import { getAuthToken } from "app/(app)/api/lib/getAuthToken";
|
||||
import type { Address } from "thirdweb";
|
||||
import type { Route } from "./types/route";
|
||||
|
||||
export async function getRoutes({
|
||||
limit,
|
||||
offset,
|
||||
originQuery,
|
||||
destinationQuery,
|
||||
originChainId,
|
||||
destinationChainId,
|
||||
originTokenAddress,
|
||||
destinationTokenAddress,
|
||||
}: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
originQuery?: string;
|
||||
destinationQuery?: string;
|
||||
originChainId?: number;
|
||||
destinationChainId?: number;
|
||||
originTokenAddress?: Address;
|
||||
destinationTokenAddress?: Address;
|
||||
} = {}) {
|
||||
const url = new URL(`${NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST}/v1/routes`);
|
||||
if (limit) {
|
||||
url.searchParams.set("limit", limit.toString());
|
||||
}
|
||||
if (offset) {
|
||||
url.searchParams.set("offset", offset.toString());
|
||||
}
|
||||
if (originQuery) {
|
||||
url.searchParams.set("originQuery", originQuery);
|
||||
}
|
||||
if (destinationQuery) {
|
||||
url.searchParams.set("destinationQuery", destinationQuery);
|
||||
}
|
||||
if (originChainId) {
|
||||
url.searchParams.set("originChainId", originChainId.toString());
|
||||
}
|
||||
if (destinationChainId) {
|
||||
url.searchParams.set("destinationChainId", destinationChainId.toString());
|
||||
}
|
||||
if (originTokenAddress) {
|
||||
url.searchParams.set("originTokenAddress", originTokenAddress);
|
||||
}
|
||||
if (destinationTokenAddress) {
|
||||
url.searchParams.set("destinationTokenAddress", destinationTokenAddress);
|
||||
}
|
||||
url.searchParams.set("sortBy", "popularity");
|
||||
// It's faster to filter client side, doesn't seem to be a big performance boost to paginate/filter server side
|
||||
const token = await getAuthToken();
|
||||
const routesResponse = await fetch(url, {
|
||||
headers: token
|
||||
? {
|
||||
authorization: `Bearer ${token}`,
|
||||
}
|
||||
: {
|
||||
"x-secret-key": DASHBOARD_THIRDWEB_SECRET_KEY,
|
||||
},
|
||||
headers: {
|
||||
"x-secret-key": DASHBOARD_THIRDWEB_SECRET_KEY,
|
||||
},
|
||||
next: { revalidate: 60 * 60 },
|
||||
});
|
||||
|
||||
if (!routesResponse.ok) {
|
||||
throw new Error("Failed to fetch routes");
|
||||
}
|
||||
const routes: { data: Route[] } = await routesResponse.json();
|
||||
const routes: {
|
||||
data: Route[];
|
||||
meta: { totalCount: number; filteredCount: number };
|
||||
} = await routesResponse.json();
|
||||
|
||||
return routes.data;
|
||||
return routes;
|
||||
}
|
||||
|
||||
+21
-3
@@ -32,8 +32,8 @@ function useChainStatswithRPC(_rpcUrl: string) {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
method: "eth_blockNumber",
|
||||
params: [],
|
||||
method: "eth_getBlockByNumber",
|
||||
params: ["latest", false],
|
||||
id: 1,
|
||||
}),
|
||||
});
|
||||
@@ -41,9 +41,12 @@ function useChainStatswithRPC(_rpcUrl: string) {
|
||||
const json = await res.json();
|
||||
const latency = (performance.now() - startTimeStamp).toFixed(0);
|
||||
|
||||
const blockNumber = Number.parseInt(json.result.number, 16);
|
||||
const blockGasLimit = Number.parseInt(json.result.gasLimit, 16);
|
||||
return {
|
||||
latency,
|
||||
blockNumber: Number.parseInt(json.result, 16),
|
||||
blockNumber,
|
||||
blockGasLimit,
|
||||
};
|
||||
},
|
||||
refetchInterval: (query) => {
|
||||
@@ -115,6 +118,21 @@ export function ChainLiveStats(props: { rpc: string }) {
|
||||
</div>
|
||||
)}
|
||||
</PrimaryInfoItem>
|
||||
|
||||
{/* Block Gas Limit */}
|
||||
<PrimaryInfoItem title="Block Gas Limit" titleIcon={<PulseDot />}>
|
||||
{stats.isError ? (
|
||||
<p className="fade-in-0 animate-in text-destructive-text">N/A</p>
|
||||
) : stats.data ? (
|
||||
<p className="fade-in-0 animate-in">
|
||||
{stats.data.blockGasLimit ?? "N/A"}
|
||||
</p>
|
||||
) : (
|
||||
<div className="flex h-[28px] w-[140px] py-1">
|
||||
<Skeleton className="h-full w-full" />
|
||||
</div>
|
||||
)}
|
||||
</PrimaryInfoItem>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
+8
-1
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { ErrorPage, LoadingPage } from "../../_components/page-skeletons";
|
||||
import { RedirectToContractOverview } from "../../_components/redirect-contract-overview.client";
|
||||
import { useContractPageMetadata } from "../../_hooks/useContractPageMetadata";
|
||||
@@ -9,6 +10,7 @@ import { ContractDirectListingsPage } from "./ContractDirectListingsPage";
|
||||
export function ContractDirectListingsPageClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
isLoggedIn: boolean;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
|
||||
@@ -21,7 +23,12 @@ export function ContractDirectListingsPageClient(props: {
|
||||
}
|
||||
|
||||
if (!metadataQuery.data.isDirectListingSupported) {
|
||||
return <RedirectToContractOverview contract={props.contract} />;
|
||||
return (
|
||||
<RedirectToContractOverview
|
||||
contract={props.contract}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
+8
-35
@@ -1,45 +1,18 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getRawAccount } from "../../../../../../account/settings/getAccount";
|
||||
import { getContractPageParamsInfo } from "../../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../../_utils/getContractPageMetadata";
|
||||
import { ContractDirectListingsPage } from "./ContractDirectListingsPage";
|
||||
import { ContractDirectListingsPageClient } from "./ContractDirectListingsPage.client";
|
||||
import type { PublicContractPageParams } from "../../types";
|
||||
import { SharedDirectListingsPage } from "./shared-direct-listings-page";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
}>;
|
||||
params: Promise<PublicContractPageParams>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const account = await getRawAccount();
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
if (info.isLocalhostChain) {
|
||||
return (
|
||||
<ContractDirectListingsPageClient
|
||||
contract={info.clientContract}
|
||||
isLoggedIn={!!account}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { isDirectListingSupported, isInsightSupported } =
|
||||
await getContractPageMetadata(info.serverContract);
|
||||
|
||||
if (!isDirectListingSupported) {
|
||||
redirect(`/${params.chain_id}/${params.contractAddress}`);
|
||||
}
|
||||
const [params, account] = await Promise.all([props.params, getRawAccount()]);
|
||||
|
||||
return (
|
||||
<ContractDirectListingsPage
|
||||
contract={info.clientContract}
|
||||
<SharedDirectListingsPage
|
||||
contractAddress={params.contractAddress}
|
||||
chainIdOrSlug={params.chain_id}
|
||||
projectMeta={undefined}
|
||||
isLoggedIn={!!account}
|
||||
isInsightSupported={isInsightSupported}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { redirectToContractLandingPage } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/utils";
|
||||
import { getContractPageParamsInfo } from "../../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../../_utils/getContractPageMetadata";
|
||||
import { ContractDirectListingsPage } from "./ContractDirectListingsPage";
|
||||
import { ContractDirectListingsPageClient } from "./ContractDirectListingsPage.client";
|
||||
|
||||
export async function SharedDirectListingsPage(props: {
|
||||
contractAddress: string;
|
||||
chainIdOrSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
isLoggedIn: boolean;
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo({
|
||||
contractAddress: props.contractAddress,
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
teamId: props.projectMeta?.teamId,
|
||||
});
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
if (info.isLocalhostChain) {
|
||||
return (
|
||||
<ContractDirectListingsPageClient
|
||||
contract={info.clientContract}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { isDirectListingSupported, isInsightSupported } =
|
||||
await getContractPageMetadata(info.serverContract);
|
||||
|
||||
if (!isDirectListingSupported) {
|
||||
redirectToContractLandingPage({
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
contractAddress: props.contractAddress,
|
||||
projectMeta: props.projectMeta,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<ContractDirectListingsPage
|
||||
contract={info.clientContract}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
isInsightSupported={isInsightSupported}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+8
-1
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { ErrorPage, LoadingPage } from "../../_components/page-skeletons";
|
||||
import { RedirectToContractOverview } from "../../_components/redirect-contract-overview.client";
|
||||
import { useContractPageMetadata } from "../../_hooks/useContractPageMetadata";
|
||||
@@ -9,6 +10,7 @@ import { ContractEnglishAuctionsPage } from "./ContractEnglishAuctionsPage";
|
||||
export function ContractEnglishAuctionsPageClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
isLoggedIn: boolean;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
|
||||
@@ -21,7 +23,12 @@ export function ContractEnglishAuctionsPageClient(props: {
|
||||
}
|
||||
|
||||
if (!metadataQuery.data.isEnglishAuctionSupported) {
|
||||
return <RedirectToContractOverview contract={props.contract} />;
|
||||
return (
|
||||
<RedirectToContractOverview
|
||||
contract={props.contract}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
+9
-37
@@ -1,46 +1,18 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getRawAccount } from "../../../../../../account/settings/getAccount";
|
||||
import { getContractPageParamsInfo } from "../../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../../_utils/getContractPageMetadata";
|
||||
import { ContractEnglishAuctionsPage } from "./ContractEnglishAuctionsPage";
|
||||
import { ContractEnglishAuctionsPageClient } from "./ContractEnglishAuctionsPage.client";
|
||||
import type { PublicContractPageParams } from "../../types";
|
||||
import { SharedEnglishAuctionsPage } from "./shared-english-auctions-page";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
}>;
|
||||
params: Promise<PublicContractPageParams>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const twAccount = await getRawAccount();
|
||||
|
||||
if (info.isLocalhostChain) {
|
||||
return (
|
||||
<ContractEnglishAuctionsPageClient
|
||||
contract={info.clientContract}
|
||||
isLoggedIn={!!twAccount}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { isEnglishAuctionSupported, isInsightSupported } =
|
||||
await getContractPageMetadata(info.serverContract);
|
||||
|
||||
if (!isEnglishAuctionSupported) {
|
||||
redirect(`/${params.chain_id}/${params.contractAddress}`);
|
||||
}
|
||||
const [params, account] = await Promise.all([props.params, getRawAccount()]);
|
||||
|
||||
return (
|
||||
<ContractEnglishAuctionsPage
|
||||
contract={info.clientContract}
|
||||
isLoggedIn={!!twAccount}
|
||||
isInsightSupported={isInsightSupported}
|
||||
<SharedEnglishAuctionsPage
|
||||
contractAddress={params.contractAddress}
|
||||
chainIdOrSlug={params.chain_id}
|
||||
projectMeta={undefined}
|
||||
isLoggedIn={!!account}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { redirectToContractLandingPage } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/utils";
|
||||
import { getContractPageParamsInfo } from "../../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../../_utils/getContractPageMetadata";
|
||||
import { ContractEnglishAuctionsPage } from "./ContractEnglishAuctionsPage";
|
||||
import { ContractEnglishAuctionsPageClient } from "./ContractEnglishAuctionsPage.client";
|
||||
|
||||
export async function SharedEnglishAuctionsPage(props: {
|
||||
contractAddress: string;
|
||||
chainIdOrSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
isLoggedIn: boolean;
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo({
|
||||
contractAddress: props.contractAddress,
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
teamId: props.projectMeta?.teamId,
|
||||
});
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
if (info.isLocalhostChain) {
|
||||
return (
|
||||
<ContractEnglishAuctionsPageClient
|
||||
contract={info.clientContract}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const metadata = await getContractPageMetadata(info.serverContract);
|
||||
|
||||
if (!metadata.isEnglishAuctionSupported) {
|
||||
redirectToContractLandingPage({
|
||||
contractAddress: props.contractAddress,
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
projectMeta: props.projectMeta,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<ContractEnglishAuctionsPage
|
||||
contract={info.clientContract}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
isInsightSupported={metadata.isInsightSupported}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+10
-1
@@ -3,6 +3,8 @@ import { TrackedLinkTW } from "@/components/ui/tracked-link";
|
||||
import { useMemo } from "react";
|
||||
import { type NFT, ZERO_ADDRESS } from "thirdweb";
|
||||
import { NFTMediaWithEmptyState } from "tw-components/nft-media";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { buildContractPagePath } from "../_utils/contract-page-path";
|
||||
|
||||
type NFTWithContract = NFT & { contractAddress: string; chainId: number };
|
||||
|
||||
@@ -29,6 +31,7 @@ interface NFTCardsProps {
|
||||
trackingCategory: string;
|
||||
isPending: boolean;
|
||||
allNfts?: boolean;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}
|
||||
|
||||
export const NFTCards: React.FC<NFTCardsProps> = ({
|
||||
@@ -36,6 +39,7 @@ export const NFTCards: React.FC<NFTCardsProps> = ({
|
||||
trackingCategory,
|
||||
isPending,
|
||||
allNfts,
|
||||
projectMeta,
|
||||
}) => {
|
||||
const dummyData = useMemo(() => {
|
||||
return Array.from({
|
||||
@@ -87,7 +91,12 @@ export const NFTCards: React.FC<NFTCardsProps> = ({
|
||||
<TrackedLinkTW
|
||||
category={trackingCategory}
|
||||
label="view_nft"
|
||||
href={`/${token.chainId}/${token.contractAddress}/nfts/${tokenId}`}
|
||||
href={buildContractPagePath({
|
||||
projectMeta,
|
||||
chainIdOrSlug: token.chainId.toString(),
|
||||
contractAddress: token.contractAddress,
|
||||
subpath: `/nfts/${tokenId}`,
|
||||
})}
|
||||
className="before:absolute before:inset-0"
|
||||
>
|
||||
{v.name}
|
||||
|
||||
+2
@@ -126,8 +126,10 @@ const getClaimConditionTypeFromPhase = (
|
||||
if (!phase.snapshot) {
|
||||
return "public";
|
||||
}
|
||||
|
||||
if (phase.snapshot) {
|
||||
if (
|
||||
phase.price === "0" &&
|
||||
typeof phase.snapshot !== "string" &&
|
||||
phase.snapshot.length === 1 &&
|
||||
phase.snapshot.some(
|
||||
|
||||
+11
-2
@@ -3,10 +3,13 @@
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { useEffect, useRef } from "react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { buildContractPagePath } from "../_utils/contract-page-path";
|
||||
import { LoadingPage } from "./page-skeletons";
|
||||
|
||||
export function RedirectToContractOverview(props: {
|
||||
contract: ThirdwebContract;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const router = useDashboardRouter();
|
||||
const redirected = useRef(false);
|
||||
@@ -17,8 +20,14 @@ export function RedirectToContractOverview(props: {
|
||||
return;
|
||||
}
|
||||
redirected.current = true;
|
||||
router.replace(`/${props.contract.chain.id}/${props.contract.address}`);
|
||||
}, [router, props.contract]);
|
||||
const landingPage = buildContractPagePath({
|
||||
projectMeta: props.projectMeta,
|
||||
chainIdOrSlug: props.contract.chain.id.toString(),
|
||||
contractAddress: props.contract.address,
|
||||
});
|
||||
|
||||
router.replace(landingPage);
|
||||
}, [router, props]);
|
||||
|
||||
return <LoadingPage />;
|
||||
}
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ export function ConfigureCustomChain(props: {
|
||||
chainSlug: string;
|
||||
}) {
|
||||
const { chainSlug } = props;
|
||||
const isSlugNumber = Number.isInteger(Number.parseInt(chainSlug));
|
||||
const isSlugNumber = Number.isInteger(Number(chainSlug));
|
||||
const [isConfigured, setIsConfigured] = useState(false);
|
||||
|
||||
if (isConfigured) {
|
||||
|
||||
+4
-2
@@ -2,8 +2,9 @@
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { MinimalTeamsAndProjects } from "components/contract-components/contract-deploy-form/add-to-project-card";
|
||||
import type { ThirdwebClient, ThirdwebContract } from "thirdweb";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
|
||||
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
|
||||
import { getContractPageSidebarLinks } from "../_utils/getContractPageSidebarLinks";
|
||||
@@ -15,7 +16,7 @@ export function ContractPageLayoutClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
children: React.ReactNode;
|
||||
teamsAndProjects: MinimalTeamsAndProjects | undefined;
|
||||
client: ThirdwebClient;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
const headerMetadataQuery = useQuery({
|
||||
@@ -39,6 +40,7 @@ export function ContractPageLayoutClient(props: {
|
||||
chainSlug: props.chainMetadata.slug,
|
||||
contractAddress: props.contract.address,
|
||||
metadata: metadataQuery.data,
|
||||
projectMeta: props.projectMeta,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
+6
-4
@@ -4,8 +4,9 @@ import { SidebarLayout } from "@/components/blocks/SidebarLayout";
|
||||
import type { DashboardContractMetadata } from "@3rdweb-sdk/react/hooks/useDashboardContractMetadata";
|
||||
import type { MinimalTeamsAndProjects } from "components/contract-components/contract-deploy-form/add-to-project-card";
|
||||
import { DeprecatedAlert } from "components/shared/DeprecatedAlert";
|
||||
import type { ThirdwebClient, ThirdwebContract } from "thirdweb";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { ContractMetadata } from "./contract-metadata";
|
||||
import { PrimaryDashboardButton } from "./primary-dashboard-button";
|
||||
|
||||
@@ -15,8 +16,8 @@ export function ContractPageLayout(props: {
|
||||
children: React.ReactNode;
|
||||
sidebarLinks: SidebarLink[];
|
||||
dashboardContractMetadata: DashboardContractMetadata | undefined;
|
||||
client: ThirdwebClient;
|
||||
teamsAndProjects: MinimalTeamsAndProjects | undefined;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
externalLinks:
|
||||
| {
|
||||
name: string;
|
||||
@@ -31,7 +32,7 @@ export function ContractPageLayout(props: {
|
||||
dashboardContractMetadata,
|
||||
externalLinks,
|
||||
teamsAndProjects,
|
||||
client,
|
||||
projectMeta,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
@@ -46,6 +47,7 @@ export function ContractPageLayout(props: {
|
||||
externalLinks={externalLinks}
|
||||
/>
|
||||
<PrimaryDashboardButton
|
||||
projectMeta={projectMeta}
|
||||
contractAddress={contract.address}
|
||||
chain={contract.chain}
|
||||
contractInfo={{
|
||||
@@ -54,7 +56,7 @@ export function ContractPageLayout(props: {
|
||||
contractAddress: contract.address,
|
||||
}}
|
||||
teamsAndProjects={teamsAndProjects}
|
||||
client={client}
|
||||
client={contract.client}
|
||||
/>
|
||||
</div>
|
||||
<DeprecatedAlert chain={chainMetadata} />
|
||||
|
||||
+20
-4
@@ -25,7 +25,9 @@ import { usePathname } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import type { Chain, ThirdwebClient } from "thirdweb";
|
||||
import { useAddContractToProject } from "../../../../../team/[team_slug]/[project_slug]/hooks/project-contracts";
|
||||
import { useAddContractToProject } from "../../../../../team/[team_slug]/[project_slug]/(sidebar)/hooks/project-contracts";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { buildContractPagePath } from "../_utils/contract-page-path";
|
||||
|
||||
const TRACKING_CATEGORY = "add_to_dashboard_upsell";
|
||||
|
||||
@@ -36,6 +38,7 @@ type AddToDashboardCardProps = {
|
||||
hideCodePageLink?: boolean;
|
||||
teamsAndProjects: MinimalTeamsAndProjects | undefined;
|
||||
client: ThirdwebClient;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
};
|
||||
|
||||
export const PrimaryDashboardButton: React.FC<AddToDashboardCardProps> = ({
|
||||
@@ -45,9 +48,17 @@ export const PrimaryDashboardButton: React.FC<AddToDashboardCardProps> = ({
|
||||
hideCodePageLink,
|
||||
teamsAndProjects,
|
||||
client,
|
||||
projectMeta,
|
||||
}) => {
|
||||
const pathname = usePathname();
|
||||
|
||||
const codePagePath = buildContractPagePath({
|
||||
projectMeta,
|
||||
chainIdOrSlug: contractInfo.chainSlug,
|
||||
contractAddress: contractAddress,
|
||||
subpath: "/code",
|
||||
});
|
||||
|
||||
// if user is not logged in
|
||||
if (!teamsAndProjects) {
|
||||
if (hideCodePageLink) {
|
||||
@@ -57,9 +68,7 @@ export const PrimaryDashboardButton: React.FC<AddToDashboardCardProps> = ({
|
||||
if (!pathname?.endsWith("/code")) {
|
||||
return (
|
||||
<Button variant="outline" asChild className="gap-2">
|
||||
<Link
|
||||
href={`/${contractInfo.chainSlug}/${contractInfo.contractAddress}/code`}
|
||||
>
|
||||
<Link href={codePagePath}>
|
||||
<CodeIcon className="size-4" />
|
||||
Code Snippets
|
||||
</Link>
|
||||
@@ -70,6 +79,11 @@ export const PrimaryDashboardButton: React.FC<AddToDashboardCardProps> = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
// if user is on a project page
|
||||
if (projectMeta) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AddToProjectButton
|
||||
contractAddress={contractAddress}
|
||||
@@ -147,6 +161,8 @@ function AddToProjectModalContent(props: {
|
||||
teamId: params.teamId,
|
||||
projectId: params.projectId,
|
||||
chainId: props.chainId,
|
||||
deploymentType: undefined,
|
||||
contractType: undefined,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
|
||||
export function buildContractPagePath(params: {
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
chainIdOrSlug: string;
|
||||
contractAddress: string;
|
||||
subpath?: `/${string}`;
|
||||
}) {
|
||||
const { projectMeta, chainIdOrSlug, contractAddress, subpath } = params;
|
||||
if (projectMeta) {
|
||||
return `/team/${projectMeta.teamSlug}/${projectMeta.projectSlug}/contract/${chainIdOrSlug}/${contractAddress}${subpath || ""}`;
|
||||
}
|
||||
|
||||
return `/${chainIdOrSlug}/${contractAddress}${subpath || ""}`;
|
||||
}
|
||||
+6
-3
@@ -7,10 +7,11 @@ import { getUserThirdwebClient } from "../../../../../api/lib/getAuthToken";
|
||||
|
||||
export async function getContractPageParamsInfo(params: {
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
chainIdOrSlug: string;
|
||||
teamId: string | undefined;
|
||||
}) {
|
||||
const contractAddress = getCheckSummedAddress(params.contractAddress);
|
||||
const chainSlugOrId = params.chain_id;
|
||||
const chainSlugOrId = params.chainIdOrSlug;
|
||||
const chainMetadata = await fetchChainWithLocalOverrides(chainSlugOrId);
|
||||
|
||||
if (!chainMetadata) {
|
||||
@@ -31,7 +32,9 @@ export async function getContractPageParamsInfo(params: {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
chain: mapV4ChainToV5Chain(chainMetadata),
|
||||
// if we have the auth token pass it into the client
|
||||
client: await getUserThirdwebClient(),
|
||||
client: await getUserThirdwebClient({
|
||||
teamId: params.teamId,
|
||||
}),
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
+8
-1
@@ -1,4 +1,6 @@
|
||||
import type { SidebarBaseLink, SidebarLink } from "@/components/blocks/Sidebar";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { buildContractPagePath } from "./contract-page-path";
|
||||
import type { ContractPageMetadata } from "./getContractPageMetadata";
|
||||
|
||||
type SidebarBaseLinkWithHide = SidebarBaseLink & {
|
||||
@@ -9,8 +11,13 @@ export function getContractPageSidebarLinks(data: {
|
||||
metadata: ContractPageMetadata;
|
||||
contractAddress: string;
|
||||
chainSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const layoutPrefix = `/${data.chainSlug}/${data.contractAddress}`;
|
||||
const layoutPrefix = buildContractPagePath({
|
||||
projectMeta: data.projectMeta,
|
||||
chainIdOrSlug: data.chainSlug,
|
||||
contractAddress: data.contractAddress,
|
||||
});
|
||||
|
||||
const generalLinks: SidebarBaseLinkWithHide[] = [
|
||||
{
|
||||
|
||||
+8
-1
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
|
||||
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
|
||||
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
|
||||
@@ -8,6 +9,7 @@ import { AccountSigners } from "./components/account-signers";
|
||||
|
||||
export function AccountSignersClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
|
||||
@@ -20,7 +22,12 @@ export function AccountSignersClient(props: {
|
||||
}
|
||||
|
||||
if (!metadataQuery.data.isAccountPermissionsSupported) {
|
||||
return <RedirectToContractOverview contract={props.contract} />;
|
||||
return (
|
||||
<RedirectToContractOverview
|
||||
contract={props.contract}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <AccountSigners contract={props.contract} />;
|
||||
|
||||
+10
-29
@@ -1,34 +1,15 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { AccountSignersClient } from "./AccountSigners.client";
|
||||
import { AccountSigners } from "./components/account-signers";
|
||||
import type { PublicContractPageParams } from "../types";
|
||||
import { SharedContractAccountPermissionsPage } from "./shared-account-permissions-page";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
}>;
|
||||
params: Promise<PublicContractPageParams>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { clientContract, serverContract, isLocalhostChain } = info;
|
||||
|
||||
if (isLocalhostChain) {
|
||||
return <AccountSignersClient contract={clientContract} />;
|
||||
}
|
||||
|
||||
const { isAccountPermissionsSupported } =
|
||||
await getContractPageMetadata(serverContract);
|
||||
|
||||
if (!isAccountPermissionsSupported) {
|
||||
redirect(`/${params.chain_id}/${params.contractAddress}`);
|
||||
}
|
||||
|
||||
return <AccountSigners contract={clientContract} />;
|
||||
return (
|
||||
<SharedContractAccountPermissionsPage
|
||||
contractAddress={params.contractAddress}
|
||||
chainIdOrSlug={params.chain_id}
|
||||
projectMeta={undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { redirectToContractLandingPage } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/utils";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { AccountSignersClient } from "./AccountSigners.client";
|
||||
import { AccountSigners } from "./components/account-signers";
|
||||
|
||||
export async function SharedContractAccountPermissionsPage(props: {
|
||||
contractAddress: string;
|
||||
chainIdOrSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo({
|
||||
contractAddress: props.contractAddress,
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
teamId: props.projectMeta?.teamId,
|
||||
});
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { clientContract, serverContract, isLocalhostChain } = info;
|
||||
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<AccountSignersClient
|
||||
contract={clientContract}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { isAccountPermissionsSupported } =
|
||||
await getContractPageMetadata(serverContract);
|
||||
|
||||
if (!isAccountPermissionsSupported) {
|
||||
redirectToContractLandingPage({
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
contractAddress: props.contractAddress,
|
||||
projectMeta: props.projectMeta,
|
||||
});
|
||||
}
|
||||
|
||||
return <AccountSigners contract={clientContract} />;
|
||||
}
|
||||
+9
-1
@@ -2,6 +2,7 @@
|
||||
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
|
||||
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
|
||||
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
|
||||
@@ -11,6 +12,7 @@ export function AccountPageClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
chainMetadata: ChainMetadata;
|
||||
isLoggedIn: boolean;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
|
||||
@@ -23,7 +25,12 @@ export function AccountPageClient(props: {
|
||||
}
|
||||
|
||||
if (!metadataQuery.data.isAccount) {
|
||||
return <RedirectToContractOverview contract={props.contract} />;
|
||||
return (
|
||||
<RedirectToContractOverview
|
||||
contract={props.contract}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -32,6 +39,7 @@ export function AccountPageClient(props: {
|
||||
chainMetadata={props.chainMetadata}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
isInsightSupported={metadataQuery.data.isInsightSupported}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+8
-1
@@ -3,6 +3,7 @@
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { Heading } from "tw-components";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { AccountBalance } from "./components/account-balance";
|
||||
import { DepositNative } from "./components/deposit-native";
|
||||
import { NftsOwned } from "./components/nfts-owned";
|
||||
@@ -12,6 +13,7 @@ interface AccountPageProps {
|
||||
chainMetadata: ChainMetadata;
|
||||
isLoggedIn: boolean;
|
||||
isInsightSupported: boolean;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}
|
||||
|
||||
export const AccountPage: React.FC<AccountPageProps> = ({
|
||||
@@ -19,6 +21,7 @@ export const AccountPage: React.FC<AccountPageProps> = ({
|
||||
chainMetadata,
|
||||
isLoggedIn,
|
||||
isInsightSupported,
|
||||
projectMeta,
|
||||
}) => {
|
||||
const symbol = chainMetadata.nativeCurrency.symbol || "Native Token";
|
||||
|
||||
@@ -44,7 +47,11 @@ export const AccountPage: React.FC<AccountPageProps> = ({
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<Heading size="title.sm">NFTs owned</Heading>
|
||||
</div>
|
||||
<NftsOwned contract={contract} isInsightSupported={isInsightSupported} />
|
||||
<NftsOwned
|
||||
contract={contract}
|
||||
isInsightSupported={isInsightSupported}
|
||||
projectMeta={projectMeta}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+4
@@ -2,16 +2,19 @@
|
||||
|
||||
import { useWalletNFTs } from "@3rdweb-sdk/react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { NFTCards } from "../../_components/NFTCards";
|
||||
|
||||
interface NftsOwnedProps {
|
||||
contract: ThirdwebContract;
|
||||
isInsightSupported: boolean;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}
|
||||
|
||||
export const NftsOwned: React.FC<NftsOwnedProps> = ({
|
||||
contract,
|
||||
isInsightSupported,
|
||||
projectMeta,
|
||||
}) => {
|
||||
const { data: walletNFTs, isPending: isWalletNFTsLoading } = useWalletNFTs({
|
||||
chainId: contract.chain.id,
|
||||
@@ -24,6 +27,7 @@ export const NftsOwned: React.FC<NftsOwnedProps> = ({
|
||||
|
||||
return nfts.length !== 0 ? (
|
||||
<NFTCards
|
||||
projectMeta={projectMeta}
|
||||
nfts={nfts.map((nft) => ({
|
||||
id: BigInt(nft.id),
|
||||
supply: BigInt(nft.supply),
|
||||
|
||||
+8
-42
@@ -1,51 +1,17 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getRawAccount } from "../../../../../account/settings/getAccount";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { AccountPage } from "./AccountPage";
|
||||
import { AccountPageClient } from "./AccountPage.client";
|
||||
import type { PublicContractPageParams } from "../types";
|
||||
import { SharedContractAccountPage } from "./shared-account-page";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
}>;
|
||||
params: Promise<PublicContractPageParams>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { clientContract, serverContract, chainMetadata, isLocalhostChain } =
|
||||
info;
|
||||
|
||||
const account = await getRawAccount();
|
||||
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<AccountPageClient
|
||||
contract={clientContract}
|
||||
chainMetadata={chainMetadata}
|
||||
isLoggedIn={!!account}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { isAccount, isInsightSupported } =
|
||||
await getContractPageMetadata(serverContract);
|
||||
|
||||
if (!isAccount) {
|
||||
redirect(`/${params.chain_id}/${params.contractAddress}`);
|
||||
}
|
||||
|
||||
const [params, account] = await Promise.all([props.params, getRawAccount()]);
|
||||
return (
|
||||
<AccountPage
|
||||
contract={clientContract}
|
||||
chainMetadata={chainMetadata}
|
||||
<SharedContractAccountPage
|
||||
contractAddress={params.contractAddress}
|
||||
chainIdOrSlug={params.chain_id}
|
||||
projectMeta={undefined}
|
||||
isLoggedIn={!!account}
|
||||
isInsightSupported={isInsightSupported}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { redirectToContractLandingPage } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/utils";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { AccountPage } from "./AccountPage";
|
||||
import { AccountPageClient } from "./AccountPage.client";
|
||||
|
||||
export async function SharedContractAccountPage(props: {
|
||||
contractAddress: string;
|
||||
chainIdOrSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
isLoggedIn: boolean;
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo({
|
||||
contractAddress: props.contractAddress,
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
teamId: props.projectMeta?.teamId,
|
||||
});
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
if (info.isLocalhostChain) {
|
||||
return (
|
||||
<AccountPageClient
|
||||
contract={info.clientContract}
|
||||
chainMetadata={info.chainMetadata}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { isAccount, isInsightSupported } = await getContractPageMetadata(
|
||||
info.serverContract,
|
||||
);
|
||||
|
||||
if (!isAccount) {
|
||||
redirectToContractLandingPage({
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
contractAddress: props.contractAddress,
|
||||
projectMeta: props.projectMeta,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<AccountPage
|
||||
contract={info.clientContract}
|
||||
chainMetadata={info.chainMetadata}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
isInsightSupported={isInsightSupported}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+13
-2
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
|
||||
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
|
||||
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
|
||||
@@ -9,6 +10,7 @@ import { AccountsPage } from "./AccountsPage";
|
||||
export function AccountsPageClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
isLoggedIn: boolean;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
|
||||
@@ -21,10 +23,19 @@ export function AccountsPageClient(props: {
|
||||
}
|
||||
|
||||
if (!metadataQuery.data.isAccountFactory) {
|
||||
return <RedirectToContractOverview contract={props.contract} />;
|
||||
return (
|
||||
<RedirectToContractOverview
|
||||
contract={props.contract}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AccountsPage contract={props.contract} isLoggedIn={props.isLoggedIn} />
|
||||
<AccountsPage
|
||||
contract={props.contract}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+4
-1
@@ -3,6 +3,7 @@
|
||||
import { ButtonGroup, Flex } from "@chakra-ui/react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { Heading, TrackedLinkButton } from "tw-components";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { AccountsCount } from "./components/accounts-count";
|
||||
import { AccountsTable } from "./components/accounts-table";
|
||||
import { CreateAccountButton } from "./components/create-account-button";
|
||||
@@ -10,11 +11,13 @@ import { CreateAccountButton } from "./components/create-account-button";
|
||||
interface AccountsPageProps {
|
||||
contract: ThirdwebContract;
|
||||
isLoggedIn: boolean;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}
|
||||
|
||||
export const AccountsPage: React.FC<AccountsPageProps> = ({
|
||||
contract,
|
||||
isLoggedIn,
|
||||
projectMeta,
|
||||
}) => {
|
||||
return (
|
||||
<Flex direction="column" gap={6}>
|
||||
@@ -43,7 +46,7 @@ export const AccountsPage: React.FC<AccountsPageProps> = ({
|
||||
</ButtonGroup>
|
||||
</Flex>
|
||||
<AccountsCount contract={contract} />
|
||||
<AccountsTable contract={contract} />
|
||||
<AccountsTable contract={contract} projectMeta={projectMeta} />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
+8
-16
@@ -1,16 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
SimpleGrid,
|
||||
Skeleton,
|
||||
Stat,
|
||||
StatLabel,
|
||||
StatNumber,
|
||||
} from "@chakra-ui/react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { totalAccounts } from "thirdweb/extensions/erc4337";
|
||||
import { useReadContract } from "thirdweb/react";
|
||||
import { Card } from "tw-components";
|
||||
import { StatCard } from "../../overview/components/stat-card";
|
||||
|
||||
type AccountsCountProps = {
|
||||
contract: ThirdwebContract;
|
||||
@@ -19,13 +12,12 @@ type AccountsCountProps = {
|
||||
export const AccountsCount: React.FC<AccountsCountProps> = ({ contract }) => {
|
||||
const totalAccountsQuery = useReadContract(totalAccounts, { contract });
|
||||
return (
|
||||
<SimpleGrid columns={{ base: 1, md: 3 }} spacing={{ base: 3, md: 6 }}>
|
||||
<Card as={Stat}>
|
||||
<StatLabel mb={{ base: 1, md: 0 }}>Total Accounts</StatLabel>
|
||||
<Skeleton isLoaded={totalAccountsQuery.isSuccess}>
|
||||
<StatNumber>{totalAccountsQuery.data?.toString()}</StatNumber>
|
||||
</Skeleton>
|
||||
</Card>
|
||||
</SimpleGrid>
|
||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-3 md:gap-6">
|
||||
<StatCard
|
||||
label="Total Accounts"
|
||||
value={totalAccountsQuery.data?.toString() || "0"}
|
||||
isPending={totalAccountsQuery.isPending}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+14
-2
@@ -16,6 +16,8 @@ import type { ThirdwebContract } from "thirdweb";
|
||||
import { getAccounts, totalAccounts } from "thirdweb/extensions/erc4337";
|
||||
import { useReadContract } from "thirdweb/react";
|
||||
import { Text, TrackedCopyButton } from "tw-components";
|
||||
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { buildContractPagePath } from "../../_utils/contract-page-path";
|
||||
|
||||
const columnHelper = createColumnHelper<{ account: string }>();
|
||||
|
||||
@@ -38,9 +40,13 @@ const columns = [
|
||||
|
||||
type AccountsTableProps = {
|
||||
contract: ThirdwebContract;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
};
|
||||
|
||||
export const AccountsTable: React.FC<AccountsTableProps> = ({ contract }) => {
|
||||
export const AccountsTable: React.FC<AccountsTableProps> = ({
|
||||
contract,
|
||||
projectMeta,
|
||||
}) => {
|
||||
const router = useDashboardRouter();
|
||||
const chainSlug = useChainSlug(contract.chain.id);
|
||||
|
||||
@@ -92,7 +98,13 @@ export const AccountsTable: React.FC<AccountsTableProps> = ({ contract }) => {
|
||||
isPending={accountsQuery.isPending}
|
||||
isFetched={accountsQuery.isFetched}
|
||||
onRowClick={(row) => {
|
||||
router.push(`/${chainSlug}/${row.account}`);
|
||||
const accountContractPagePath = buildContractPagePath({
|
||||
projectMeta,
|
||||
chainIdOrSlug: chainSlug.toString(),
|
||||
contractAddress: row.account,
|
||||
});
|
||||
|
||||
router.push(accountContractPagePath);
|
||||
}}
|
||||
/>
|
||||
{/* pagination */}
|
||||
|
||||
+12
-36
@@ -1,41 +1,17 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getRawAccount } from "../../../../../account/settings/getAccount";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { AccountsPage } from "./AccountsPage";
|
||||
import { AccountsPageClient } from "./AccountsPage.client";
|
||||
import type { PublicContractPageParams } from "../types";
|
||||
import { SharedAccountsPage } from "./shared-accounts-page";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
}>;
|
||||
params: Promise<PublicContractPageParams>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { clientContract, serverContract, isLocalhostChain } = info;
|
||||
|
||||
const [account, { isAccountFactory }] = await Promise.all([
|
||||
getRawAccount(),
|
||||
isLocalhostChain
|
||||
? { isAccountFactory: undefined }
|
||||
: getContractPageMetadata(serverContract),
|
||||
]);
|
||||
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<AccountsPageClient contract={clientContract} isLoggedIn={!!account} />
|
||||
);
|
||||
}
|
||||
|
||||
if (!isAccountFactory) {
|
||||
redirect(`/${params.chain_id}/${params.contractAddress}`);
|
||||
}
|
||||
|
||||
return <AccountsPage contract={clientContract} isLoggedIn={!!account} />;
|
||||
const [params, account] = await Promise.all([props.params, getRawAccount()]);
|
||||
return (
|
||||
<SharedAccountsPage
|
||||
contractAddress={params.contractAddress}
|
||||
chainIdOrSlug={params.chain_id}
|
||||
projectMeta={undefined}
|
||||
isLoggedIn={!!account}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { redirectToContractLandingPage } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/utils";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { AccountsPage } from "./AccountsPage";
|
||||
import { AccountsPageClient } from "./AccountsPage.client";
|
||||
|
||||
export async function SharedAccountsPage(props: {
|
||||
contractAddress: string;
|
||||
chainIdOrSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
isLoggedIn: boolean;
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo({
|
||||
contractAddress: props.contractAddress,
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
teamId: props.projectMeta?.teamId,
|
||||
});
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { clientContract, serverContract, isLocalhostChain } = info;
|
||||
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<AccountsPageClient
|
||||
contract={clientContract}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { isAccountFactory } = await getContractPageMetadata(serverContract);
|
||||
|
||||
if (!isAccountFactory) {
|
||||
redirectToContractLandingPage({
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
contractAddress: props.contractAddress,
|
||||
projectMeta: props.projectMeta,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<AccountsPage
|
||||
contract={clientContract}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+7
-63
@@ -1,71 +1,15 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { type ThirdwebContract, resolveContractAbi } from "thirdweb/contract";
|
||||
import { type Abi, toEventSelector, toFunctionSelector } from "thirdweb/utils";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { ContractAnalyticsPage } from "./ContractAnalyticsPage";
|
||||
import type { PublicContractPageParams } from "../types";
|
||||
import { SharedAnalyticsPage } from "./shared-analytics-page";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
}>;
|
||||
params: Promise<PublicContractPageParams>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const [
|
||||
{ eventSelectorToName, writeFnSelectorToName },
|
||||
{ isInsightSupported },
|
||||
] = await Promise.all([
|
||||
getSelectors(info.serverContract),
|
||||
getContractPageMetadata(info.serverContract),
|
||||
]);
|
||||
|
||||
if (!isInsightSupported) {
|
||||
redirect(`/${params.chain_id}/${params.contractAddress}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<ContractAnalyticsPage
|
||||
contract={info.clientContract}
|
||||
writeFnSelectorToNameRecord={writeFnSelectorToName}
|
||||
eventSelectorToNameRecord={eventSelectorToName}
|
||||
<SharedAnalyticsPage
|
||||
contractAddress={params.contractAddress}
|
||||
chainIdOrSlug={params.chain_id}
|
||||
projectMeta={undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
async function getSelectors(contract: ThirdwebContract) {
|
||||
try {
|
||||
const abi = await resolveContractAbi<Abi>(contract);
|
||||
const writeFnSelectorToName: Record<string, string> = {};
|
||||
const eventSelectorToName: Record<string, string> = {};
|
||||
|
||||
for (const item of abi) {
|
||||
if (item.type === "event") {
|
||||
eventSelectorToName[toEventSelector(item)] = item.name;
|
||||
} else if (
|
||||
// if write function
|
||||
item.type === "function" &&
|
||||
item.stateMutability !== "view" &&
|
||||
item.stateMutability !== "pure"
|
||||
) {
|
||||
writeFnSelectorToName[toFunctionSelector(item)] = item.name;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
writeFnSelectorToName,
|
||||
eventSelectorToName,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
writeFnSelectorToName: {},
|
||||
eventSelectorToName: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { type ThirdwebContract, resolveContractAbi } from "thirdweb/contract";
|
||||
import { type Abi, toEventSelector, toFunctionSelector } from "thirdweb/utils";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { redirectToContractLandingPage } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/utils";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { ContractAnalyticsPage } from "./ContractAnalyticsPage";
|
||||
|
||||
export async function SharedAnalyticsPage(props: {
|
||||
contractAddress: string;
|
||||
chainIdOrSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo({
|
||||
contractAddress: props.contractAddress,
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
teamId: props.projectMeta?.teamId,
|
||||
});
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const [
|
||||
{ eventSelectorToName, writeFnSelectorToName },
|
||||
{ isInsightSupported },
|
||||
] = await Promise.all([
|
||||
getSelectors(info.serverContract),
|
||||
getContractPageMetadata(info.serverContract),
|
||||
]);
|
||||
|
||||
if (!isInsightSupported) {
|
||||
redirectToContractLandingPage({
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
contractAddress: props.contractAddress,
|
||||
projectMeta: props.projectMeta,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<ContractAnalyticsPage
|
||||
contract={info.clientContract}
|
||||
writeFnSelectorToNameRecord={writeFnSelectorToName}
|
||||
eventSelectorToNameRecord={eventSelectorToName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
async function getSelectors(contract: ThirdwebContract) {
|
||||
try {
|
||||
const abi = await resolveContractAbi<Abi>(contract);
|
||||
const writeFnSelectorToName: Record<string, string> = {};
|
||||
const eventSelectorToName: Record<string, string> = {};
|
||||
|
||||
for (const item of abi) {
|
||||
if (item.type === "event") {
|
||||
eventSelectorToName[toEventSelector(item)] = item.name;
|
||||
} else if (
|
||||
// if write function
|
||||
item.type === "function" &&
|
||||
item.stateMutability !== "view" &&
|
||||
item.stateMutability !== "pure"
|
||||
) {
|
||||
writeFnSelectorToName[toFunctionSelector(item)] = item.name;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
writeFnSelectorToName,
|
||||
eventSelectorToName,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
writeFnSelectorToName: {},
|
||||
eventSelectorToName: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
+8
-1
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { ClaimConditions } from "../_components/claim-conditions/claim-conditions";
|
||||
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
|
||||
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
|
||||
@@ -9,6 +10,7 @@ import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
|
||||
export function ClaimConditionsClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
isLoggedIn: boolean;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
|
||||
@@ -27,7 +29,12 @@ export function ClaimConditionsClient(props: {
|
||||
} = metadataQuery.data;
|
||||
|
||||
if (!isERC20ClaimConditionsSupported && !isERC721ClaimConditionsSupported) {
|
||||
return <RedirectToContractOverview contract={props.contract} />;
|
||||
return (
|
||||
<RedirectToContractOverview
|
||||
contract={props.contract}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
+8
-49
@@ -1,58 +1,17 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getRawAccount } from "../../../../../account/settings/getAccount";
|
||||
import { ClaimConditions } from "../_components/claim-conditions/claim-conditions";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { ClaimConditionsClient } from "./ClaimConditions.client";
|
||||
import type { PublicContractPageParams } from "../types";
|
||||
import { SharedClaimConditionsPage } from "./shared-claim-conditions-page";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
}>;
|
||||
params: Promise<PublicContractPageParams>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
const { clientContract, serverContract, isLocalhostChain } = info;
|
||||
|
||||
const [
|
||||
account,
|
||||
{
|
||||
isERC20ClaimConditionsSupported,
|
||||
isERC721ClaimConditionsSupported,
|
||||
supportedERCs,
|
||||
},
|
||||
] = await Promise.all([
|
||||
getRawAccount(),
|
||||
isLocalhostChain
|
||||
? {
|
||||
isERC20ClaimConditionsSupported: undefined,
|
||||
isERC721ClaimConditionsSupported: undefined,
|
||||
supportedERCs: undefined,
|
||||
}
|
||||
: getContractPageMetadata(serverContract),
|
||||
]);
|
||||
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<ClaimConditionsClient contract={clientContract} isLoggedIn={!!account} />
|
||||
);
|
||||
}
|
||||
|
||||
if (!isERC20ClaimConditionsSupported && !isERC721ClaimConditionsSupported) {
|
||||
redirect(`/${params.chain_id}/${params.contractAddress}`);
|
||||
}
|
||||
|
||||
const [params, account] = await Promise.all([props.params, getRawAccount()]);
|
||||
return (
|
||||
<ClaimConditions
|
||||
contract={clientContract}
|
||||
isERC20={supportedERCs.isERC20}
|
||||
<SharedClaimConditionsPage
|
||||
contractAddress={params.contractAddress}
|
||||
chainIdOrSlug={params.chain_id}
|
||||
projectMeta={undefined}
|
||||
isLoggedIn={!!account}
|
||||
isMultiphase={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { redirectToContractLandingPage } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/utils";
|
||||
import { ClaimConditions } from "../_components/claim-conditions/claim-conditions";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { ClaimConditionsClient } from "./ClaimConditions.client";
|
||||
|
||||
export async function SharedClaimConditionsPage(props: {
|
||||
contractAddress: string;
|
||||
chainIdOrSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
isLoggedIn: boolean;
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo({
|
||||
contractAddress: props.contractAddress,
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
teamId: props.projectMeta?.teamId,
|
||||
});
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { clientContract, serverContract, isLocalhostChain } = info;
|
||||
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<ClaimConditionsClient
|
||||
contract={clientContract}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
isERC20ClaimConditionsSupported,
|
||||
isERC721ClaimConditionsSupported,
|
||||
supportedERCs,
|
||||
} = await getContractPageMetadata(serverContract);
|
||||
|
||||
if (!isERC20ClaimConditionsSupported && !isERC721ClaimConditionsSupported) {
|
||||
redirectToContractLandingPage({
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
contractAddress: props.contractAddress,
|
||||
projectMeta: props.projectMeta,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<ClaimConditions
|
||||
contract={clientContract}
|
||||
isERC20={supportedERCs.isERC20}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
isMultiphase={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+7
-33
@@ -1,41 +1,15 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { resolveContractAbi } from "thirdweb/contract";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { ContractCodePage } from "./contract-code-page";
|
||||
import { ContractCodePageClient } from "./contract-code-page.client";
|
||||
import type { PublicContractPageParams } from "../types";
|
||||
import { SharedCodePage } from "./shared-code-page";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
}>;
|
||||
params: Promise<PublicContractPageParams>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { clientContract, serverContract, chainMetadata, isLocalhostChain } =
|
||||
info;
|
||||
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<ContractCodePageClient
|
||||
contract={clientContract}
|
||||
chainMetadata={chainMetadata}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const abi = await resolveContractAbi(serverContract).catch(() => undefined);
|
||||
|
||||
return (
|
||||
<ContractCodePage
|
||||
abi={abi}
|
||||
contract={clientContract}
|
||||
chainMetadata={chainMetadata}
|
||||
<SharedCodePage
|
||||
contractAddress={params.contractAddress}
|
||||
chainIdOrSlug={params.chain_id}
|
||||
projectMeta={undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { resolveContractAbi } from "thirdweb/contract";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { ContractCodePage } from "./contract-code-page";
|
||||
import { ContractCodePageClient } from "./contract-code-page.client";
|
||||
|
||||
export async function SharedCodePage(props: {
|
||||
contractAddress: string;
|
||||
chainIdOrSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo({
|
||||
contractAddress: props.contractAddress,
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
teamId: props.projectMeta?.teamId,
|
||||
});
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { clientContract, serverContract, chainMetadata, isLocalhostChain } =
|
||||
info;
|
||||
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<ContractCodePageClient
|
||||
contract={clientContract}
|
||||
chainMetadata={chainMetadata}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const abi = await resolveContractAbi(serverContract).catch(() => undefined);
|
||||
|
||||
return (
|
||||
<ContractCodePage
|
||||
abi={abi}
|
||||
contract={clientContract}
|
||||
chainMetadata={chainMetadata}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+22
-4
@@ -58,6 +58,8 @@ import {
|
||||
import { useActiveAccount, useSwitchActiveWalletChain } from "thirdweb/react";
|
||||
import { concatHex, padHex } from "thirdweb/utils";
|
||||
import { z } from "zod";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { buildContractPagePath } from "../_utils/contract-page-path";
|
||||
import { SingleNetworkSelector } from "./single-network-selector";
|
||||
|
||||
type CrossChain = {
|
||||
@@ -91,6 +93,7 @@ export function DataTable({
|
||||
inputSalt,
|
||||
initCode,
|
||||
isDirectDeploy,
|
||||
projectMeta,
|
||||
}: {
|
||||
data: CrossChain[];
|
||||
coreMetadata: FetchDeployMetadataResult;
|
||||
@@ -100,6 +103,7 @@ export function DataTable({
|
||||
inputSalt?: `0x${string}`;
|
||||
initCode?: `0x${string}`;
|
||||
isDirectDeploy: boolean;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const activeAccount = useActiveAccount();
|
||||
const switchChain = useSwitchActiveWalletChain();
|
||||
@@ -235,11 +239,17 @@ export function DataTable({
|
||||
header: "Network",
|
||||
cell: ({ row }) => {
|
||||
if (row.getValue("status") === "DEPLOYED") {
|
||||
const href = buildContractPagePath({
|
||||
projectMeta,
|
||||
chainIdOrSlug: `${row.getValue("chainId")}`,
|
||||
contractAddress: coreContract.address,
|
||||
});
|
||||
|
||||
return (
|
||||
<Link
|
||||
target="_blank"
|
||||
className="text-blue-500 underline"
|
||||
href={`/${row.getValue("chainId")}/${coreContract.address}`}
|
||||
href={href}
|
||||
>
|
||||
{row.getValue("network")}
|
||||
</Link>
|
||||
@@ -405,9 +415,17 @@ export function DataTable({
|
||||
});
|
||||
}
|
||||
deployStatusModal.nextStep();
|
||||
deployStatusModal.setViewContractLink(
|
||||
`/${chain.id}/${crosschainContractAddress}`,
|
||||
);
|
||||
|
||||
if (crosschainContractAddress) {
|
||||
const contractLink = buildContractPagePath({
|
||||
projectMeta,
|
||||
chainIdOrSlug: `${chain.id}`,
|
||||
contractAddress: crosschainContractAddress,
|
||||
});
|
||||
|
||||
deployStatusModal.setViewContractLink(contractLink);
|
||||
}
|
||||
|
||||
deployStatusModal.close();
|
||||
|
||||
setCustomChainData((prevData) =>
|
||||
|
||||
+8
-280
@@ -1,287 +1,15 @@
|
||||
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 || []
|
||||
);
|
||||
}
|
||||
import type { PublicContractPageParams } from "../types";
|
||||
import { SharedCrossChainPage } from "./shared-cross-chain-page";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
}>;
|
||||
params: Promise<PublicContractPageParams>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
const ProxyDeployedEvent = prepareEvent({
|
||||
signature:
|
||||
"event ProxyDeployedV2(address indexed implementation, address indexed proxy, address indexed deployer, bytes32 inputSalt, bytes data, bytes extraData)",
|
||||
filters: {
|
||||
proxy: params.contractAddress.toLowerCase(),
|
||||
},
|
||||
});
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { clientContract, serverContract } = info;
|
||||
|
||||
const isModularCore = (await getContractPageMetadata(serverContract))
|
||||
.isModularCore;
|
||||
|
||||
let twCloneFactoryContract: Readonly<
|
||||
ContractOptions<[], `0x${string}`>
|
||||
> | null = null;
|
||||
try {
|
||||
twCloneFactoryContract = await getDeployedCloneFactoryContract({
|
||||
chain: serverContract.chain,
|
||||
client: serverContract.client,
|
||||
});
|
||||
} catch {}
|
||||
|
||||
const originalCode = await eth_getCode(
|
||||
getRpcClient({
|
||||
client: serverContract.client,
|
||||
chain: serverContract.chain,
|
||||
}),
|
||||
{
|
||||
address: serverContract.address,
|
||||
},
|
||||
);
|
||||
|
||||
let initCode: `0x${string}` = "0x";
|
||||
let creationTxReceipt: TransactionReceipt | undefined;
|
||||
let isDirectDeploy = false;
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://contract.thirdweb-dev.com/creation/${serverContract.chain.id}/${serverContract.address}`,
|
||||
);
|
||||
const creationData = await res.json();
|
||||
|
||||
if (creationData.status === "1" && creationData.result[0]?.txHash) {
|
||||
const rpcClient = getRpcClient({
|
||||
client: serverContract.client,
|
||||
chain: serverContract.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],
|
||||
});
|
||||
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: serverContract.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.chainId !== serverContract.chain.id);
|
||||
|
||||
let coreMetadata: FetchDeployMetadataResult | undefined;
|
||||
try {
|
||||
coreMetadata = (
|
||||
await fetchPublishedContractsFromDeploy({
|
||||
contract: serverContract,
|
||||
client: serverContract.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: serverContract,
|
||||
events: [moduleEvent],
|
||||
});
|
||||
|
||||
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: serverContract,
|
||||
data: initializeData,
|
||||
});
|
||||
|
||||
const abi = await resolveContractAbi(serverContract).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: serverContract.chain,
|
||||
client: serverContract.client,
|
||||
address: m,
|
||||
}),
|
||||
client: serverContract.client,
|
||||
})
|
||||
).at(-1),
|
||||
),
|
||||
)) as FetchDeployMetadataResult[])
|
||||
: undefined;
|
||||
}
|
||||
|
||||
if (!isDirectDeploy && !initializeData) {
|
||||
return NoCrossChainPrompt();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<h2 className="mb-1 font-bold text-2xl tracking-tight">
|
||||
Cross-chain contracts
|
||||
</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Deterministically deploy and interact with your contracts on multiple
|
||||
networks.
|
||||
</p>
|
||||
</div>
|
||||
<div className="h-10" />
|
||||
<DataTable
|
||||
coreMetadata={coreMetadata}
|
||||
modulesMetadata={modulesMetadata}
|
||||
initializeData={initializeData}
|
||||
inputSalt={inputSalt}
|
||||
data={chainsDeployedOn}
|
||||
coreContract={clientContract}
|
||||
initCode={initCode}
|
||||
isDirectDeploy={isDirectDeploy}
|
||||
/>
|
||||
</>
|
||||
<SharedCrossChainPage
|
||||
contractAddress={params.contractAddress}
|
||||
chainIdOrSlug={params.chain_id}
|
||||
projectMeta={undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+282
@@ -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 type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { DataTable } from "./data-table";
|
||||
import { NoCrossChainPrompt } from "./no-crosschain-prompt";
|
||||
|
||||
export async function SharedCrossChainPage(props: {
|
||||
contractAddress: string;
|
||||
chainIdOrSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo({
|
||||
contractAddress: props.contractAddress,
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
teamId: props.projectMeta?.teamId,
|
||||
});
|
||||
|
||||
const ProxyDeployedEvent = prepareEvent({
|
||||
signature:
|
||||
"event ProxyDeployedV2(address indexed implementation, address indexed proxy, address indexed deployer, bytes32 inputSalt, bytes data, bytes extraData)",
|
||||
filters: {
|
||||
proxy: props.contractAddress.toLowerCase(),
|
||||
},
|
||||
});
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { clientContract, serverContract } = info;
|
||||
|
||||
const isModularCore = (await getContractPageMetadata(serverContract))
|
||||
.isModularCore;
|
||||
|
||||
let twCloneFactoryContract: Readonly<
|
||||
ContractOptions<[], `0x${string}`>
|
||||
> | null = null;
|
||||
try {
|
||||
twCloneFactoryContract = await getDeployedCloneFactoryContract({
|
||||
chain: serverContract.chain,
|
||||
client: serverContract.client,
|
||||
});
|
||||
} catch {}
|
||||
|
||||
const originalCode = await eth_getCode(
|
||||
getRpcClient({
|
||||
client: serverContract.client,
|
||||
chain: serverContract.chain,
|
||||
}),
|
||||
{
|
||||
address: serverContract.address,
|
||||
},
|
||||
);
|
||||
|
||||
let initCode: `0x${string}` = "0x";
|
||||
let creationTxReceipt: TransactionReceipt | undefined;
|
||||
let isDirectDeploy = false;
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://contract.thirdweb-dev.com/creation/${serverContract.chain.id}/${serverContract.address}`,
|
||||
);
|
||||
const creationData = await res.json();
|
||||
|
||||
if (creationData.status === "1" && creationData.result[0]?.txHash) {
|
||||
const rpcClient = getRpcClient({
|
||||
client: serverContract.client,
|
||||
chain: serverContract.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],
|
||||
});
|
||||
const event = events.find(
|
||||
(e) => e.args.proxy.toLowerCase() === props.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: serverContract.client,
|
||||
chain,
|
||||
});
|
||||
const code = await eth_getCode(rpcRequest, {
|
||||
address: props.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.chainId !== serverContract.chain.id);
|
||||
|
||||
let coreMetadata: FetchDeployMetadataResult | undefined;
|
||||
try {
|
||||
coreMetadata = (
|
||||
await fetchPublishedContractsFromDeploy({
|
||||
contract: serverContract,
|
||||
client: serverContract.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: serverContract,
|
||||
events: [moduleEvent],
|
||||
});
|
||||
|
||||
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: serverContract,
|
||||
data: initializeData,
|
||||
});
|
||||
|
||||
const abi = await resolveContractAbi(serverContract).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: serverContract.chain,
|
||||
client: serverContract.client,
|
||||
address: m,
|
||||
}),
|
||||
client: serverContract.client,
|
||||
})
|
||||
).at(-1),
|
||||
),
|
||||
)) as FetchDeployMetadataResult[])
|
||||
: undefined;
|
||||
}
|
||||
|
||||
if (!isDirectDeploy && !initializeData) {
|
||||
return NoCrossChainPrompt();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<h2 className="mb-1 font-bold text-2xl tracking-tight">
|
||||
Cross-chain contracts
|
||||
</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Deterministically deploy and interact with your contracts on multiple
|
||||
networks.
|
||||
</p>
|
||||
</div>
|
||||
<div className="h-10" />
|
||||
<DataTable
|
||||
coreMetadata={coreMetadata}
|
||||
modulesMetadata={modulesMetadata}
|
||||
initializeData={initializeData}
|
||||
inputSalt={inputSalt}
|
||||
data={chainsDeployedOn}
|
||||
coreContract={clientContract}
|
||||
initCode={initCode}
|
||||
isDirectDeploy={isDirectDeploy}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -29,7 +29,7 @@ export function SingleNetworkSelector(props: {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Number.isInteger(Number.parseInt(searchValue))) {
|
||||
if (Number.isInteger(Number(searchValue))) {
|
||||
return String(chain.chainId).startsWith(searchValue);
|
||||
}
|
||||
return chain.name.toLowerCase().includes(searchValue.toLowerCase());
|
||||
|
||||
+29
-9
@@ -33,12 +33,18 @@ import { toast } from "sonner";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { stringify } from "thirdweb/utils";
|
||||
import { Button, Card, FormLabel, Heading, Text } from "tw-components";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { buildContractPagePath } from "../_utils/contract-page-path";
|
||||
|
||||
interface EventsFeedProps {
|
||||
contract: ThirdwebContract;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}
|
||||
|
||||
export const EventsFeed: React.FC<EventsFeedProps> = ({ contract }) => {
|
||||
export const EventsFeed: React.FC<EventsFeedProps> = ({
|
||||
contract,
|
||||
projectMeta,
|
||||
}) => {
|
||||
const [autoUpdate, setAutoUpdate] = useState(true);
|
||||
const allEvents = useActivity(contract, autoUpdate);
|
||||
const searchParams = useSearchParams();
|
||||
@@ -81,11 +87,18 @@ export const EventsFeed: React.FC<EventsFeedProps> = ({ contract }) => {
|
||||
value={selectedEvent}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
|
||||
if (eventTypes.includes(val)) {
|
||||
const path =
|
||||
e.target.value === "all"
|
||||
? `/${chainSlug}/${contract.address}/events`
|
||||
: `/${chainSlug}/${contract.address}/events?event=${val}`;
|
||||
const path = buildContractPagePath({
|
||||
projectMeta,
|
||||
chainIdOrSlug: chainSlug.toString(),
|
||||
contractAddress: contract.address,
|
||||
subpath:
|
||||
e.target.value === "all"
|
||||
? "/events"
|
||||
: `/events?event=${val}`,
|
||||
});
|
||||
|
||||
router.push(path);
|
||||
setSelectedEvent(val);
|
||||
}
|
||||
@@ -154,6 +167,7 @@ export const EventsFeed: React.FC<EventsFeedProps> = ({ contract }) => {
|
||||
setSelectedEvent={setSelectedEvent}
|
||||
contractAddress={contract.address}
|
||||
chainSlug={chainSlug}
|
||||
projectMeta={projectMeta}
|
||||
/>
|
||||
))}
|
||||
</Accordion>
|
||||
@@ -168,6 +182,7 @@ interface EventsFeedItemProps {
|
||||
setSelectedEvent: React.Dispatch<React.SetStateAction<string>>;
|
||||
contractAddress: string;
|
||||
chainSlug: string | number;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}
|
||||
|
||||
const EventsFeedItem: React.FC<EventsFeedItemProps> = ({
|
||||
@@ -175,9 +190,9 @@ const EventsFeedItem: React.FC<EventsFeedItemProps> = ({
|
||||
setSelectedEvent,
|
||||
contractAddress,
|
||||
chainSlug,
|
||||
projectMeta,
|
||||
}) => {
|
||||
const { onCopy } = useClipboard(transaction.transactionHash);
|
||||
|
||||
const router = useDashboardRouter();
|
||||
|
||||
return (
|
||||
@@ -246,9 +261,14 @@ const EventsFeedItem: React.FC<EventsFeedItemProps> = ({
|
||||
key={idx}
|
||||
onClick={(ev) => {
|
||||
ev.stopPropagation();
|
||||
router.push(
|
||||
`/${chainSlug}/${contractAddress}/events?event=${e.eventName}`,
|
||||
);
|
||||
const path = buildContractPagePath({
|
||||
projectMeta,
|
||||
chainIdOrSlug: chainSlug.toString(),
|
||||
contractAddress: contractAddress,
|
||||
subpath: `/events?event=${e.eventName}`,
|
||||
});
|
||||
|
||||
router.push(path);
|
||||
setSelectedEvent(e.eventName);
|
||||
}}
|
||||
>
|
||||
|
||||
+10
-14
@@ -1,19 +1,15 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { EventsFeed } from "./events-feed";
|
||||
import type { PublicContractPageParams } from "../types";
|
||||
import { SharedEventsPage } from "./shared-events-page";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
}>;
|
||||
params: Promise<PublicContractPageParams>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return <EventsFeed contract={info.clientContract} />;
|
||||
return (
|
||||
<SharedEventsPage
|
||||
contractAddress={params.contractAddress}
|
||||
chainIdOrSlug={params.chain_id}
|
||||
projectMeta={undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { EventsFeed } from "./events-feed";
|
||||
|
||||
export async function SharedEventsPage(props: {
|
||||
contractAddress: string;
|
||||
chainIdOrSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo({
|
||||
contractAddress: props.contractAddress,
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
teamId: props.projectMeta?.teamId,
|
||||
});
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<EventsFeed
|
||||
contract={info.clientContract}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+7
-35
@@ -1,45 +1,17 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { resolveContractAbi } from "thirdweb/contract";
|
||||
import { getRawAccount } from "../../../../../account/settings/getAccount";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { ContractExplorerPage } from "./ContractExplorerPage";
|
||||
import { ContractExplorerPageClient } from "./ContractExplorerPage.client";
|
||||
import type { PublicContractPageParams } from "../types";
|
||||
import { SharedExplorerPage } from "./shared-explorer-page";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
}>;
|
||||
params: Promise<PublicContractPageParams>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { clientContract, serverContract, chainMetadata, isLocalhostChain } =
|
||||
info;
|
||||
|
||||
const account = await getRawAccount();
|
||||
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<ContractExplorerPageClient
|
||||
contract={clientContract}
|
||||
chainMetadata={chainMetadata}
|
||||
isLoggedIn={!!account}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const abi = await resolveContractAbi(serverContract).catch(() => undefined);
|
||||
|
||||
return (
|
||||
<ContractExplorerPage
|
||||
contract={clientContract}
|
||||
abi={abi}
|
||||
chainMetadata={chainMetadata}
|
||||
<SharedExplorerPage
|
||||
contractAddress={params.contractAddress}
|
||||
chainIdOrSlug={params.chain_id}
|
||||
projectMeta={undefined}
|
||||
isLoggedIn={!!account}
|
||||
/>
|
||||
);
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { resolveContractAbi } from "thirdweb/contract";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { ContractExplorerPage } from "./ContractExplorerPage";
|
||||
import { ContractExplorerPageClient } from "./ContractExplorerPage.client";
|
||||
|
||||
export async function SharedExplorerPage(props: {
|
||||
contractAddress: string;
|
||||
chainIdOrSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
isLoggedIn: boolean;
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo({
|
||||
contractAddress: props.contractAddress,
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
teamId: props.projectMeta?.teamId,
|
||||
});
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { clientContract, serverContract, chainMetadata, isLocalhostChain } =
|
||||
info;
|
||||
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<ContractExplorerPageClient
|
||||
contract={clientContract}
|
||||
chainMetadata={chainMetadata}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const abi = await resolveContractAbi(serverContract).catch(() => undefined);
|
||||
|
||||
return (
|
||||
<ContractExplorerPage
|
||||
contract={clientContract}
|
||||
abi={abi}
|
||||
chainMetadata={chainMetadata}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+12
-199
@@ -1,25 +1,8 @@
|
||||
import { getProjects } from "@/api/projects";
|
||||
import { getTeams } from "@/api/team";
|
||||
import type { MinimalTeamsAndProjects } from "components/contract-components/contract-deploy-form/add-to-project-card";
|
||||
import { resolveFunctionSelectors } from "lib/selectors";
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getContractMetadata } from "thirdweb/extensions/common";
|
||||
import { isAddress, isContractDeployed } from "thirdweb/utils";
|
||||
import { shortenIfAddress } from "utils/usedapp-external";
|
||||
import { NebulaChatButton } from "../../../../../nebula-app/(app)/components/FloatingChat/FloatingChat";
|
||||
import {
|
||||
getAuthTokenWalletAddress,
|
||||
getUserThirdwebClient,
|
||||
} from "../../../../api/lib/getAuthToken";
|
||||
import { ConfigureCustomChain } from "./_layout/ConfigureCustomChain";
|
||||
import { getContractMetadataHeaderData } from "./_layout/contract-metadata";
|
||||
import { ContractPageLayout } from "./_layout/contract-page-layout";
|
||||
import { ContractPageLayoutClient } from "./_layout/contract-page-layout.client";
|
||||
import { supportedERCs } from "./_utils/detectedFeatures/supportedERCs";
|
||||
import { getContractPageParamsInfo } from "./_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "./_utils/getContractPageMetadata";
|
||||
import { getContractPageSidebarLinks } from "./_utils/getContractPageSidebarLinks";
|
||||
SharedContractLayout,
|
||||
generateContractLayoutMetadata,
|
||||
} from "./shared-layout";
|
||||
|
||||
export default async function Layout(props: {
|
||||
params: Promise<{
|
||||
@@ -29,141 +12,17 @@ export default async function Layout(props: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
if (!isAddress(params.contractAddress)) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
if (!info) {
|
||||
return <ConfigureCustomChain chainSlug={params.chain_id} />;
|
||||
}
|
||||
|
||||
const { clientContract, serverContract, chainMetadata, isLocalhostChain } =
|
||||
info;
|
||||
|
||||
if (chainMetadata.status === "deprecated") {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const accountAddress = await getAuthTokenWalletAddress();
|
||||
|
||||
const client = await getUserThirdwebClient();
|
||||
const teamsAndProjects = await getTeamsAndProjectsIfLoggedIn();
|
||||
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<ContractPageLayoutClient
|
||||
chainMetadata={chainMetadata}
|
||||
contract={clientContract}
|
||||
teamsAndProjects={teamsAndProjects}
|
||||
client={client}
|
||||
>
|
||||
{props.children}
|
||||
</ContractPageLayoutClient>
|
||||
);
|
||||
}
|
||||
|
||||
// check if the contract exists
|
||||
const isValidContract = await isContractDeployed(serverContract).catch(
|
||||
() => false,
|
||||
);
|
||||
if (!isValidContract) {
|
||||
// TODO - replace 404 with a better page to upsell deploy or other thirdweb products
|
||||
notFound();
|
||||
}
|
||||
const contractPageMetadata = await getContractPageMetadata(serverContract);
|
||||
|
||||
const sidebarLinks = getContractPageSidebarLinks({
|
||||
chainSlug: chainMetadata.slug,
|
||||
contractAddress: serverContract.address,
|
||||
metadata: contractPageMetadata,
|
||||
});
|
||||
|
||||
const { contractMetadata, externalLinks } =
|
||||
await getContractMetadataHeaderData(serverContract);
|
||||
|
||||
const contractAddress = serverContract.address;
|
||||
const chainName = chainMetadata.name;
|
||||
const chainId = chainMetadata.chainId;
|
||||
|
||||
const contractPromptPrefix = `A user is viewing the contract address ${contractAddress} on ${chainName} (Chain ID: ${chainId}). Provide a concise summary of this contract's functionalities, such as token minting, staking, or governance mechanisms. Focus on what the contract enables users to do, avoiding transaction execution details unless requested.
|
||||
Users may be interested in how to interact with the contract. Outline common interaction patterns, such as claiming rewards, participating in governance, or transferring assets. Emphasize the contract's capabilities without guiding through transaction processes unless asked.
|
||||
Provide insights into how the contract is being used. Share information on user engagement, transaction volumes, or integration with other dApps, focusing on the contract's role within the broader ecosystem.
|
||||
Users may be considering integrating the contract into their applications. Discuss how this contract's functionalities can be leveraged within different types of dApps, highlighting potential use cases and benefits.
|
||||
|
||||
The following is the user's message:`;
|
||||
|
||||
const examplePrompts: string[] = [
|
||||
"What does this contract do?",
|
||||
"What permissions or roles exist in this contract?",
|
||||
"Which functions are used the most?",
|
||||
"Has this contract been used recently?",
|
||||
"Who are the largest holders/users of this?",
|
||||
];
|
||||
|
||||
return (
|
||||
<ContractPageLayout
|
||||
chainMetadata={chainMetadata}
|
||||
contract={clientContract}
|
||||
sidebarLinks={sidebarLinks}
|
||||
dashboardContractMetadata={contractMetadata}
|
||||
externalLinks={externalLinks}
|
||||
teamsAndProjects={teamsAndProjects}
|
||||
client={client}
|
||||
<SharedContractLayout
|
||||
contractAddress={params.contractAddress}
|
||||
chainIdOrSlug={params.chain_id}
|
||||
projectMeta={undefined}
|
||||
>
|
||||
<NebulaChatButton
|
||||
isLoggedIn={!!accountAddress}
|
||||
networks={info.chainMetadata.testnet ? "testnet" : "mainnet"}
|
||||
isFloating={true}
|
||||
pageType="contract"
|
||||
label="Ask AI about this contract"
|
||||
client={client}
|
||||
nebulaParams={{
|
||||
messagePrefix: contractPromptPrefix,
|
||||
chainIds: [chainId],
|
||||
wallet: accountAddress ?? undefined,
|
||||
}}
|
||||
examplePrompts={examplePrompts.map((prompt) => ({
|
||||
title: prompt,
|
||||
message: prompt,
|
||||
}))}
|
||||
/>
|
||||
{props.children}
|
||||
</ContractPageLayout>
|
||||
</SharedContractLayout>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
image: x.image,
|
||||
})),
|
||||
})),
|
||||
);
|
||||
|
||||
return teamsAndProjects;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateMetadata(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
@@ -171,54 +30,8 @@ export async function generateMetadata(props: {
|
||||
}>;
|
||||
}): Promise<Metadata> {
|
||||
const params = await props.params;
|
||||
try {
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
if (!info) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
const [functionSelectors, contractMetadata] = await Promise.all([
|
||||
resolveFunctionSelectors(info.serverContract),
|
||||
getContractMetadata({
|
||||
contract: info.serverContract,
|
||||
}),
|
||||
]);
|
||||
|
||||
if (contractMetadata.name === null) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
const { isERC1155, isERC20, isERC721 } = supportedERCs(functionSelectors);
|
||||
|
||||
const contractDisplayName = `${contractMetadata.name}${
|
||||
contractMetadata.symbol ? ` (${contractMetadata.symbol})` : ""
|
||||
}`;
|
||||
|
||||
const cleanedChainName = info?.chainMetadata?.name
|
||||
.replace("Mainnet", "")
|
||||
.replace("Testnet", "")
|
||||
.trim();
|
||||
|
||||
const title = `${contractDisplayName} | ${cleanedChainName} Smart Contract`;
|
||||
let description = "";
|
||||
|
||||
if (isERC721 || isERC1155) {
|
||||
description = `View tokens, source code, transactions, balances, and analytics for the ${contractDisplayName} smart contract on ${cleanedChainName}.`;
|
||||
} else if (isERC20) {
|
||||
description = `View ERC20 tokens, transactions, balances, source code, and analytics for the ${contractDisplayName} smart contract on ${cleanedChainName}`;
|
||||
} else {
|
||||
description = `View tokens, transactions, balances, source code, and analytics for the ${contractDisplayName} smart contract on ${cleanedChainName}`;
|
||||
}
|
||||
|
||||
return {
|
||||
title: title,
|
||||
description: description,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
title: `${shortenIfAddress(params.contractAddress)} | ${params.chain_id}`,
|
||||
description: `View tokens, transactions, balances, source code, and analytics for the smart contract on Chain ID ${params.chain_id}`,
|
||||
};
|
||||
}
|
||||
return generateContractLayoutMetadata({
|
||||
contractAddress: params.contractAddress,
|
||||
chainIdOrSlug: params.chain_id,
|
||||
});
|
||||
}
|
||||
|
||||
+8
-1
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
|
||||
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
|
||||
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
|
||||
@@ -9,6 +10,7 @@ import { ContractEditModulesPage } from "./ContractEditModulesPage";
|
||||
export function ContractEditModulesPageClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
isLoggedIn: boolean;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
|
||||
@@ -21,7 +23,12 @@ export function ContractEditModulesPageClient(props: {
|
||||
}
|
||||
|
||||
if (!metadataQuery.data.isModularCore) {
|
||||
return <RedirectToContractOverview contract={props.contract} />;
|
||||
return (
|
||||
<RedirectToContractOverview
|
||||
contract={props.contract}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
+10
-36
@@ -1,43 +1,17 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getRawAccount } from "../../../../../account/settings/getAccount";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { ContractEditModulesPage } from "./ContractEditModulesPage";
|
||||
import { ContractEditModulesPageClient } from "./ContractEditModulesPage.client";
|
||||
import type { PublicContractPageParams } from "../types";
|
||||
import { SharedModulesPage } from "./shared-modules-page";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
}>;
|
||||
params: Promise<PublicContractPageParams>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { clientContract, serverContract, isLocalhostChain } = info;
|
||||
|
||||
const account = await getRawAccount();
|
||||
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<ContractEditModulesPageClient
|
||||
contract={clientContract}
|
||||
isLoggedIn={!!account}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { isModularCore } = await getContractPageMetadata(serverContract);
|
||||
|
||||
if (!isModularCore) {
|
||||
redirect(`/${params.chain_id}/${params.contractAddress}`);
|
||||
}
|
||||
|
||||
const [params, account] = await Promise.all([props.params, getRawAccount()]);
|
||||
return (
|
||||
<ContractEditModulesPage contract={clientContract} isLoggedIn={!!account} />
|
||||
<SharedModulesPage
|
||||
contractAddress={params.contractAddress}
|
||||
chainIdOrSlug={params.chain_id}
|
||||
projectMeta={undefined}
|
||||
isLoggedIn={!!account}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { redirectToContractLandingPage } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/utils";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { ContractEditModulesPage } from "./ContractEditModulesPage";
|
||||
import { ContractEditModulesPageClient } from "./ContractEditModulesPage.client";
|
||||
|
||||
export async function SharedModulesPage(props: {
|
||||
contractAddress: string;
|
||||
chainIdOrSlug: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
isLoggedIn: boolean;
|
||||
}) {
|
||||
const info = await getContractPageParamsInfo({
|
||||
contractAddress: props.contractAddress,
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
teamId: props.projectMeta?.teamId,
|
||||
});
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { clientContract, serverContract, isLocalhostChain } = info;
|
||||
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<ContractEditModulesPageClient
|
||||
contract={clientContract}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { isModularCore } = await getContractPageMetadata(serverContract);
|
||||
|
||||
if (!isModularCore) {
|
||||
redirectToContractLandingPage({
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
contractAddress: props.contractAddress,
|
||||
projectMeta: props.projectMeta,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<ContractEditModulesPage
|
||||
contract={clientContract}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+9
-1
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
|
||||
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
|
||||
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
|
||||
@@ -9,6 +10,7 @@ import { ContractNFTPage } from "./ContractNFTPage";
|
||||
export function ContractNFTPageClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
isLoggedIn: boolean;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
|
||||
@@ -23,7 +25,12 @@ export function ContractNFTPageClient(props: {
|
||||
const { supportedERCs, functionSelectors } = metadataQuery.data;
|
||||
|
||||
if (!supportedERCs.isERC721 && !supportedERCs.isERC1155) {
|
||||
return <RedirectToContractOverview contract={props.contract} />;
|
||||
return (
|
||||
<RedirectToContractOverview
|
||||
contract={props.contract}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -32,6 +39,7 @@ export function ContractNFTPageClient(props: {
|
||||
isErc721={supportedERCs.isERC721}
|
||||
functionSelectors={functionSelectors}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+4
@@ -1,6 +1,7 @@
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import * as ERC721Ext from "thirdweb/extensions/erc721";
|
||||
import * as ERC1155Ext from "thirdweb/extensions/erc1155";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { BatchLazyMintButton } from "./components/batch-lazy-mint-button";
|
||||
import { NFTClaimButton } from "./components/claim-button";
|
||||
import { NFTLazyMintButton } from "./components/lazy-mint-button";
|
||||
@@ -16,6 +17,7 @@ interface NftOverviewPageProps {
|
||||
tokenId?: string;
|
||||
functionSelectors: string[];
|
||||
isLoggedIn: boolean;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}
|
||||
|
||||
export const ContractNFTPage: React.FC<NftOverviewPageProps> = ({
|
||||
@@ -23,6 +25,7 @@ export const ContractNFTPage: React.FC<NftOverviewPageProps> = ({
|
||||
isErc721,
|
||||
functionSelectors,
|
||||
isLoggedIn,
|
||||
projectMeta,
|
||||
}) => {
|
||||
const isERC721ClaimToSupported =
|
||||
ERC721Ext.isClaimToSupported(functionSelectors);
|
||||
@@ -107,6 +110,7 @@ export const ContractNFTPage: React.FC<NftOverviewPageProps> = ({
|
||||
contract={contract}
|
||||
isErc721={isErc721}
|
||||
tokenByIndex={isTokenByIndexSupported}
|
||||
projectMeta={projectMeta}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+9
-1
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { ErrorPage, LoadingPage } from "../../_components/page-skeletons";
|
||||
import { RedirectToContractOverview } from "../../_components/redirect-contract-overview.client";
|
||||
import { useContractPageMetadata } from "../../_hooks/useContractPageMetadata";
|
||||
@@ -11,6 +12,7 @@ export function TokenIdPageClient(props: {
|
||||
tokenId: string;
|
||||
isLoggedIn: boolean;
|
||||
accountAddress: string | undefined;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}) {
|
||||
const { contract } = props;
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
@@ -26,7 +28,12 @@ export function TokenIdPageClient(props: {
|
||||
const { supportedERCs } = metadataQuery.data;
|
||||
|
||||
if (!supportedERCs.isERC721 && !supportedERCs.isERC1155) {
|
||||
return <RedirectToContractOverview contract={props.contract} />;
|
||||
return (
|
||||
<RedirectToContractOverview
|
||||
contract={props.contract}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -36,6 +43,7 @@ export function TokenIdPageClient(props: {
|
||||
tokenId={props.tokenId}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
accountAddress={props.accountAddress}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+12
-52
@@ -1,63 +1,23 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getRawAccount } from "../../../../../../account/settings/getAccount";
|
||||
import { getAuthTokenWalletAddress } from "../../../../../../api/lib/getAuthToken";
|
||||
import { getContractPageParamsInfo } from "../../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../../_utils/getContractPageMetadata";
|
||||
import { TokenIdPageClient } from "./TokenIdPage.client";
|
||||
import { TokenIdPage } from "./token-id";
|
||||
import type { PublicContractPageParams } from "../../types";
|
||||
import { SharedNFTTokenPage } from "./shared-nfts-token-page";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
contractAddress: string;
|
||||
chain_id: string;
|
||||
tokenId: string;
|
||||
}>;
|
||||
params: Promise<
|
||||
PublicContractPageParams & {
|
||||
tokenId: string;
|
||||
}
|
||||
>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const info = await getContractPageParamsInfo(params);
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
if (!isOnlyNumbers(params.tokenId)) {
|
||||
// redirect to nfts index page
|
||||
redirect(`/${params.chain_id}/${params.contractAddress}/nfts`);
|
||||
}
|
||||
|
||||
const [accountAddress, account] = await Promise.all([
|
||||
getAuthTokenWalletAddress(),
|
||||
getRawAccount(),
|
||||
]);
|
||||
|
||||
const { clientContract, serverContract, isLocalhostChain } = info;
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<TokenIdPageClient
|
||||
contract={clientContract}
|
||||
tokenId={params.tokenId}
|
||||
isLoggedIn={!!account}
|
||||
accountAddress={accountAddress || undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { supportedERCs } = await getContractPageMetadata(serverContract);
|
||||
|
||||
if (!supportedERCs.isERC721 && !supportedERCs.isERC1155) {
|
||||
redirect(`/${params.chain_id}/${params.contractAddress}`);
|
||||
}
|
||||
const [params, account] = await Promise.all([props.params, getRawAccount()]);
|
||||
|
||||
return (
|
||||
<TokenIdPage
|
||||
contract={clientContract}
|
||||
isErc721={supportedERCs.isERC721}
|
||||
<SharedNFTTokenPage
|
||||
contractAddress={params.contractAddress}
|
||||
chainIdOrSlug={params.chain_id}
|
||||
tokenId={params.tokenId}
|
||||
projectMeta={undefined}
|
||||
isLoggedIn={!!account}
|
||||
accountAddress={accountAddress || undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function isOnlyNumbers(str: string) {
|
||||
return /^\d+$/.test(str);
|
||||
}
|
||||
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getAuthTokenWalletAddress } from "../../../../../../api/lib/getAuthToken";
|
||||
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { redirectToContractLandingPage } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/utils";
|
||||
import { buildContractPagePath } from "../../_utils/contract-page-path";
|
||||
import { getContractPageParamsInfo } from "../../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../../_utils/getContractPageMetadata";
|
||||
import { TokenIdPageClient } from "./TokenIdPage.client";
|
||||
import { TokenIdPage } from "./token-id";
|
||||
|
||||
export async function SharedNFTTokenPage(props: {
|
||||
contractAddress: string;
|
||||
chainIdOrSlug: string;
|
||||
tokenId: string;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
isLoggedIn: boolean;
|
||||
}) {
|
||||
const [info, accountAddress] = await Promise.all([
|
||||
getContractPageParamsInfo({
|
||||
contractAddress: props.contractAddress,
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
teamId: props.projectMeta?.teamId,
|
||||
}),
|
||||
getAuthTokenWalletAddress(),
|
||||
]);
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
// redirect to nfts index page
|
||||
if (!isOnlyNumbers(props.tokenId)) {
|
||||
redirect(
|
||||
buildContractPagePath({
|
||||
projectMeta: props.projectMeta,
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
contractAddress: props.contractAddress,
|
||||
subpath: "/nfts",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const { clientContract, serverContract, isLocalhostChain } = info;
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<TokenIdPageClient
|
||||
contract={clientContract}
|
||||
tokenId={props.tokenId}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
accountAddress={accountAddress || undefined}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { supportedERCs } = await getContractPageMetadata(serverContract);
|
||||
|
||||
if (!supportedERCs.isERC721 && !supportedERCs.isERC1155) {
|
||||
redirectToContractLandingPage({
|
||||
chainIdOrSlug: props.chainIdOrSlug,
|
||||
contractAddress: props.contractAddress,
|
||||
projectMeta: props.projectMeta,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<TokenIdPage
|
||||
contract={clientContract}
|
||||
isErc721={supportedERCs.isERC721}
|
||||
tokenId={props.tokenId}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
accountAddress={accountAddress || undefined}
|
||||
projectMeta={props.projectMeta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function isOnlyNumbers(str: string) {
|
||||
return /^\d+$/.test(str);
|
||||
}
|
||||
+12
-11
@@ -17,7 +17,6 @@ import { CodeClient } from "@/components/ui/code/code.client";
|
||||
import { TabButtons } from "@/components/ui/tabs";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { useThirdwebClient } from "@/constants/thirdweb.client";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler";
|
||||
import { useChainSlug } from "hooks/chains/chainSlug";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
@@ -28,6 +27,8 @@ import { getNFT as getErc721NFT } from "thirdweb/extensions/erc721";
|
||||
import { getNFT as getErc1155NFT } from "thirdweb/extensions/erc1155";
|
||||
import { useReadContract } from "thirdweb/react";
|
||||
import { NFTMediaWithEmptyState } from "tw-components/nft-media";
|
||||
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { buildContractPagePath } from "../../_utils/contract-page-path";
|
||||
import { NftProperty } from "../components/nft-property";
|
||||
import { useNFTDrawerTabs } from "./useNftDrawerTabs";
|
||||
|
||||
@@ -53,6 +54,7 @@ interface TokenIdPageProps {
|
||||
isErc721: boolean;
|
||||
isLoggedIn: boolean;
|
||||
accountAddress: string | undefined;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}
|
||||
|
||||
// TODO: verify the entire nft object with zod schema and display an error message
|
||||
@@ -63,9 +65,9 @@ export const TokenIdPage: React.FC<TokenIdPageProps> = ({
|
||||
isErc721,
|
||||
isLoggedIn,
|
||||
accountAddress,
|
||||
projectMeta,
|
||||
}) => {
|
||||
const [tab, setTab] = useState("Details");
|
||||
const router = useDashboardRouter();
|
||||
const chainId = contract.chain.id;
|
||||
const chainSlug = useChainSlug(chainId || 1);
|
||||
|
||||
@@ -113,6 +115,13 @@ export const TokenIdPage: React.FC<TokenIdPageProps> = ({
|
||||
: undefined,
|
||||
};
|
||||
|
||||
const nftsPagePath = buildContractPagePath({
|
||||
projectMeta,
|
||||
chainIdOrSlug: chainSlug.toString(),
|
||||
contractAddress: contract.address,
|
||||
subpath: "/nfts",
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col gap-6 lg:flex-row">
|
||||
@@ -129,15 +138,7 @@ export const TokenIdPage: React.FC<TokenIdPageProps> = ({
|
||||
<Breadcrumb className="mb-3">
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink
|
||||
href={`/${chainSlug}/${contract.address}/nfts`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
router.push(`/${chainSlug}/${contract.address}/nfts`);
|
||||
}}
|
||||
>
|
||||
NFTs
|
||||
</BreadcrumbLink>
|
||||
<BreadcrumbLink href={nftsPagePath}>NFTs</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
|
||||
+15
-6
@@ -41,16 +41,20 @@ import type { NFT, ThirdwebContract } from "thirdweb";
|
||||
import * as ERC721Ext from "thirdweb/extensions/erc721";
|
||||
import * as ERC1155Ext from "thirdweb/extensions/erc1155";
|
||||
import { useReadContract } from "thirdweb/react";
|
||||
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { buildContractPagePath } from "../../_utils/contract-page-path";
|
||||
|
||||
interface ContractOverviewNFTGetAllProps {
|
||||
contract: ThirdwebContract;
|
||||
isErc721: boolean;
|
||||
tokenByIndex: boolean;
|
||||
projectMeta: ProjectMeta | undefined;
|
||||
}
|
||||
export const NFTGetAllTable: React.FC<ContractOverviewNFTGetAllProps> = ({
|
||||
contract,
|
||||
isErc721,
|
||||
tokenByIndex,
|
||||
projectMeta,
|
||||
}) => {
|
||||
// if it's not erc721, it's erc1155
|
||||
const isErc1155 = !isErc721;
|
||||
@@ -297,12 +301,17 @@ export const NFTGetAllTable: React.FC<ContractOverviewNFTGetAllProps> = ({
|
||||
if (!tokenId && tokenId !== 0n) {
|
||||
return;
|
||||
}
|
||||
router.push(
|
||||
`/${chainSlug}/${contract.address}/nfts/${tokenId.toString()}`,
|
||||
{
|
||||
scroll: true,
|
||||
},
|
||||
);
|
||||
|
||||
const path = buildContractPagePath({
|
||||
projectMeta,
|
||||
chainIdOrSlug: chainSlug.toString(),
|
||||
contractAddress: contract.address,
|
||||
subpath: `/nfts/${tokenId.toString()}`,
|
||||
});
|
||||
|
||||
router.push(path, {
|
||||
scroll: true,
|
||||
});
|
||||
}}
|
||||
borderBottomWidth={1}
|
||||
_last={{ borderBottomWidth: 0 }}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user