Compare commits
156
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
6a1e54ec69 | ||
|
|
7ddedfbed1 | ||
|
|
98eda9dd3d | ||
|
|
5a13ad4521 | ||
|
|
557a29a960 | ||
|
|
ff565cb0f8 | ||
|
|
816aa6137b | ||
|
|
7de273c59d | ||
|
|
95cc441edd | ||
|
|
6f1d4b1fcf | ||
|
|
7ede361870 | ||
|
|
8b50779229 | ||
|
|
282d9f2c39 | ||
|
|
e739de158d | ||
|
|
451594c80c | ||
|
|
9634e49113 | ||
|
|
a4e12c5516 | ||
|
|
2297915633 | ||
|
|
585ba2f575 | ||
|
|
a68ec74d25 | ||
|
|
7ae3a81a11 | ||
|
|
ccb9cd5d0a | ||
|
|
3dc11c087a | ||
|
|
49479d40b0 | ||
|
|
3d7c2d4f67 | ||
|
|
5b40f76b4d | ||
|
|
ef1550cff5 | ||
|
|
b1719fd4ff | ||
|
|
598d4006d9 | ||
|
|
bc75ae6826 | ||
|
|
8fb8a7530c | ||
|
|
106ca3cd01 | ||
|
|
76a718c8f5 | ||
|
|
1e924e037f | ||
|
|
99d2497b51 | ||
|
|
a5a1d17f3b | ||
|
|
62c179d775 | ||
|
|
e96ebba2cb |
@@ -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@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16
|
||||
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@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16
|
||||
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@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16
|
||||
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@06245a4e0a36c064a573d4150030f5ec548e4fcc #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.
|
||||
@@ -123,6 +123,7 @@ const baseNextConfig: NextConfig = {
|
||||
webpackBuildWorker: true,
|
||||
webpackMemoryOptimizations: true,
|
||||
serverSourceMaps: false,
|
||||
taint: true,
|
||||
},
|
||||
serverExternalPackages: ["pino-pretty"],
|
||||
async headers() {
|
||||
|
||||
+18
-18
@@ -71,7 +71,7 @@
|
||||
"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",
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
|
||||
import { API_SERVER_URL } from "../constants/env";
|
||||
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "../constants/public-envs";
|
||||
|
||||
export async function acceptInvite(options: {
|
||||
teamId: string;
|
||||
@@ -17,7 +17,7 @@ export async function acceptInvite(options: {
|
||||
}
|
||||
|
||||
const res = await fetch(
|
||||
`${API_SERVER_URL}/v1/teams/${options.teamId}/invites/${options.inviteId}/accept`,
|
||||
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${options.teamId}/invites/${options.inviteId}/accept`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
|
||||
import { API_SERVER_URL } from "../constants/env";
|
||||
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "../constants/public-envs";
|
||||
|
||||
export async function reSubscribePlan(options: {
|
||||
teamId: string;
|
||||
@@ -14,7 +14,7 @@ export async function reSubscribePlan(options: {
|
||||
}
|
||||
|
||||
const res = await fetch(
|
||||
`${API_SERVER_URL}/v1/teams/${options.teamId}/checkout/resubscribe-plan`,
|
||||
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${options.teamId}/checkout/resubscribe-plan`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
|
||||
import { API_SERVER_URL } from "../constants/env";
|
||||
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "../constants/public-envs";
|
||||
|
||||
export async function confirmEmailWithOTP(otp: string) {
|
||||
const token = await getAuthToken();
|
||||
@@ -12,16 +12,19 @@ export async function confirmEmailWithOTP(otp: string) {
|
||||
};
|
||||
}
|
||||
|
||||
const res = await fetch(`${API_SERVER_URL}/v1/account/confirmEmail`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
const res = await fetch(
|
||||
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/account/confirmEmail`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
confirmationToken: otp,
|
||||
}),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
confirmationToken: otp,
|
||||
}),
|
||||
});
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
const json = await res.json();
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
import { defineDashboardChain } from "lib/defineDashboardChain";
|
||||
import { ZERO_ADDRESS, isAddress, toTokens } from "thirdweb";
|
||||
import { getWalletBalance } from "thirdweb/wallets";
|
||||
import { getUserThirdwebClient } from "../../app/(app)/api/lib/getAuthToken";
|
||||
import { MORALIS_API_KEY } from "../constants/server-envs";
|
||||
import { serverThirdwebClient } from "../constants/thirdweb-client.server";
|
||||
|
||||
type BalanceQueryResponse = Array<{
|
||||
balance: string;
|
||||
@@ -23,7 +24,6 @@ export async function getTokenBalancesFromMoralis(params: {
|
||||
error: string;
|
||||
}
|
||||
> {
|
||||
const client = await getUserThirdwebClient();
|
||||
const { contractAddress, chainId } = params;
|
||||
|
||||
if (!isAddress(contractAddress)) {
|
||||
@@ -39,7 +39,7 @@ export async function getTokenBalancesFromMoralis(params: {
|
||||
const balance = await getWalletBalance({
|
||||
address: contractAddress,
|
||||
chain,
|
||||
client,
|
||||
client: serverThirdwebClient,
|
||||
});
|
||||
return [
|
||||
{
|
||||
@@ -61,7 +61,7 @@ export async function getTokenBalancesFromMoralis(params: {
|
||||
const resp = await fetch(tokenBalanceEndpoint, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"x-api-key": process.env.MORALIS_API_KEY || "",
|
||||
"x-api-key": MORALIS_API_KEY,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@ import type { WalletNFT } from "lib/wallet/nfts/types";
|
||||
import { getVercelEnv } from "../../lib/vercel-utils";
|
||||
import { isAlchemySupported } from "../../lib/wallet/nfts/isAlchemySupported";
|
||||
import { isMoralisSupported } from "../../lib/wallet/nfts/isMoralisSupported";
|
||||
import { DASHBOARD_THIRDWEB_CLIENT_ID } from "../constants/env";
|
||||
import { NET_PUBLIC_DASHBOARD_THIRDWEB_CLIENT_ID } from "../constants/public-envs";
|
||||
import { MORALIS_API_KEY } from "../constants/server-envs";
|
||||
|
||||
type WalletNFTApiReturn =
|
||||
| { result: WalletNFT[]; error?: undefined }
|
||||
@@ -59,13 +60,13 @@ export async function getWalletNFTs(params: {
|
||||
}
|
||||
}
|
||||
|
||||
if (isMoralisSupported(chainId) && process.env.MORALIS_API_KEY) {
|
||||
if (isMoralisSupported(chainId) && MORALIS_API_KEY) {
|
||||
const url = generateMoralisUrl({ chainId, owner });
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-API-Key": process.env.MORALIS_API_KEY,
|
||||
"X-API-Key": MORALIS_API_KEY,
|
||||
},
|
||||
next: {
|
||||
revalidate: 10, // cache for 10 seconds
|
||||
@@ -148,7 +149,7 @@ async function getWalletNFTsFromInsight(params: {
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
"x-client-id": DASHBOARD_THIRDWEB_CLIENT_ID,
|
||||
"x-client-id": NET_PUBLIC_DASHBOARD_THIRDWEB_CLIENT_ID,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
"use server";
|
||||
|
||||
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
|
||||
import { API_SERVER_URL, THIRDWEB_ENGINE_CLOUD_URL } from "../constants/env";
|
||||
import {
|
||||
NEXT_PUBLIC_ENGINE_CLOUD_URL,
|
||||
NEXT_PUBLIC_THIRDWEB_API_HOST,
|
||||
} from "../constants/public-envs";
|
||||
import { ANALYTICS_SERVICE_URL } from "../constants/server-envs";
|
||||
|
||||
type ProxyActionParams = {
|
||||
pathname: string;
|
||||
@@ -76,22 +80,13 @@ async function proxy<T>(
|
||||
}
|
||||
|
||||
export async function apiServerProxy<T>(params: ProxyActionParams) {
|
||||
return proxy<T>(API_SERVER_URL, params);
|
||||
return proxy<T>(NEXT_PUBLIC_THIRDWEB_API_HOST, params);
|
||||
}
|
||||
|
||||
export async function engineCloudProxy<T>(params: ProxyActionParams) {
|
||||
return proxy<T>(THIRDWEB_ENGINE_CLOUD_URL, params);
|
||||
}
|
||||
|
||||
export async function payServerProxy<T>(params: ProxyActionParams) {
|
||||
return proxy<T>(
|
||||
process.env.NEXT_PUBLIC_PAY_URL
|
||||
? `https://${process.env.NEXT_PUBLIC_PAY_URL}`
|
||||
: "https://pay.thirdweb-dev.com",
|
||||
params,
|
||||
);
|
||||
return proxy<T>(NEXT_PUBLIC_ENGINE_CLOUD_URL, params);
|
||||
}
|
||||
|
||||
export async function analyticsServerProxy<T>(params: ProxyActionParams) {
|
||||
return proxy<T>(process.env.ANALYTICS_SERVICE_URL || "", params);
|
||||
return proxy<T>(ANALYTICS_SERVICE_URL, params);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
|
||||
export async function revalidatePathAction(
|
||||
path: string,
|
||||
type: "page" | "layout",
|
||||
) {
|
||||
revalidatePath(path, type);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
|
||||
import { API_SERVER_URL } from "../constants/env";
|
||||
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "../constants/public-envs";
|
||||
|
||||
export async function sendTeamInvites(options: {
|
||||
teamId: string;
|
||||
@@ -40,17 +40,20 @@ async function sendInvite(
|
||||
invite: { email: string; role: "OWNER" | "MEMBER" },
|
||||
token: string,
|
||||
) {
|
||||
const res = await fetch(`${API_SERVER_URL}/v1/teams/${teamId}/invites`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
const res = await fetch(
|
||||
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${teamId}/invites`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
inviteEmail: invite.email,
|
||||
inviteRole: invite.role,
|
||||
}),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
inviteEmail: invite.email,
|
||||
inviteRole: invite.role,
|
||||
}),
|
||||
});
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
const errorMessage = await res.text();
|
||||
|
||||
@@ -2,13 +2,12 @@ import "server-only";
|
||||
|
||||
import Stripe from "stripe";
|
||||
import type { Team } from "../api/team";
|
||||
import { STRIPE_SECRET_KEY } from "../constants/server-envs";
|
||||
|
||||
let existingStripe: Stripe | undefined;
|
||||
|
||||
function getStripe() {
|
||||
if (!existingStripe) {
|
||||
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY;
|
||||
|
||||
if (!STRIPE_SECRET_KEY) {
|
||||
throw new Error("STRIPE_SECRET_KEY is not set");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use server";
|
||||
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
|
||||
import { API_SERVER_URL } from "../constants/env";
|
||||
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "../constants/public-envs";
|
||||
|
||||
export async function updateAccount(values: {
|
||||
name?: string;
|
||||
@@ -13,7 +13,7 @@ export async function updateAccount(values: {
|
||||
throw new Error("No Auth token");
|
||||
}
|
||||
|
||||
const res = await fetch(`${API_SERVER_URL}/v1/account`, {
|
||||
const res = await fetch(`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/account`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
||||
@@ -6,11 +6,14 @@ import type {
|
||||
InAppWalletStats,
|
||||
RpcMethodStats,
|
||||
TransactionStats,
|
||||
UniversalBridgeStats,
|
||||
UniversalBridgeWalletStats,
|
||||
UserOpStats,
|
||||
WalletStats,
|
||||
WalletUserStats,
|
||||
} from "types/analytics";
|
||||
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
|
||||
import { ANALYTICS_SERVICE_URL } from "../constants/server-envs";
|
||||
import { getChains } from "./chain";
|
||||
|
||||
async function fetchAnalytics(
|
||||
@@ -29,34 +32,38 @@ async function fetchAnalytics(
|
||||
}
|
||||
|
||||
// create a new URL object for the analytics server
|
||||
const ANALYTICS_SERVICE_URL = new URL(
|
||||
process.env.ANALYTICS_SERVICE_URL || "https://analytics.thirdweb.com",
|
||||
const analyticsServiceUrl = new URL(
|
||||
ANALYTICS_SERVICE_URL || "https://analytics.thirdweb.com",
|
||||
);
|
||||
ANALYTICS_SERVICE_URL.pathname = pathname;
|
||||
|
||||
analyticsServiceUrl.pathname = pathname;
|
||||
for (const param of searchParams?.split("&") || []) {
|
||||
const [key, value] = param.split("=");
|
||||
if (!key || !value) {
|
||||
throw new Error("Invalid input, no key or value provided");
|
||||
}
|
||||
ANALYTICS_SERVICE_URL.searchParams.append(
|
||||
analyticsServiceUrl.searchParams.append(
|
||||
decodeURIComponent(key),
|
||||
decodeURIComponent(value),
|
||||
);
|
||||
}
|
||||
// client id DEBUG OVERRIDE
|
||||
// ANALYTICS_SERVICE_URL.searchParams.delete("clientId");
|
||||
// ANALYTICS_SERVICE_URL.searchParams.delete("accountId");
|
||||
// ANALYTICS_SERVICE_URL.searchParams.append(
|
||||
// "clientId",
|
||||
// "...",
|
||||
// analyticsServiceUrl.searchParams.delete("projectId");
|
||||
// analyticsServiceUrl.searchParams.delete("teamId");
|
||||
// analyticsServiceUrl.searchParams.append(
|
||||
// "teamId",
|
||||
// "team_cm0lde33r02344w129k5hm2xz",
|
||||
// );
|
||||
// analyticsServiceUrl.searchParams.append(
|
||||
// "projectId",
|
||||
// "prj_cm4rqwx9b002qrnsnr37wqpo6",
|
||||
// );
|
||||
|
||||
return fetch(ANALYTICS_SERVICE_URL, {
|
||||
return fetch(analyticsServiceUrl, {
|
||||
...init,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
...init?.headers,
|
||||
authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -367,3 +374,60 @@ export async function getEcosystemWalletUsage(args: {
|
||||
|
||||
return json.data as EcosystemWalletStats[];
|
||||
}
|
||||
|
||||
export async function getUniversalBridgeUsage(args: {
|
||||
teamId: string;
|
||||
projectId?: string;
|
||||
from?: Date;
|
||||
to?: Date;
|
||||
period?: "day" | "week" | "month" | "year" | "all";
|
||||
}) {
|
||||
const searchParams = buildSearchParams(args);
|
||||
const res = await fetchAnalytics(`v2/universal?${searchParams.toString()}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (res?.status !== 200) {
|
||||
const reason = await res?.text();
|
||||
console.error(
|
||||
`Failed to fetch universal bridge stats: ${res?.status} - ${res.statusText} - ${reason}`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return json.data as UniversalBridgeStats[];
|
||||
}
|
||||
|
||||
export async function getUniversalBridgeWalletUsage(args: {
|
||||
teamId: string;
|
||||
projectId: string;
|
||||
from?: Date;
|
||||
to?: Date;
|
||||
period?: "day" | "week" | "month" | "year" | "all";
|
||||
}) {
|
||||
const searchParams = buildSearchParams(args);
|
||||
const res = await fetchAnalytics(
|
||||
`v2/universal/wallets?${searchParams.toString()}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (res?.status !== 200) {
|
||||
const reason = await res?.text();
|
||||
console.error(
|
||||
`Failed to fetch universal bridge wallet stats: ${res?.status} - ${res.statusText} - ${reason}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return json.data as UniversalBridgeWalletStats[];
|
||||
}
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import "server-only";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import type { ChainService } from "../../app/(app)/(dashboard)/(chain)/types/chain";
|
||||
import { API_SERVER_URL, THIRDWEB_API_SECRET } from "../constants/env";
|
||||
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "../constants/public-envs";
|
||||
import { API_SERVER_SECRET } from "../constants/server-envs";
|
||||
|
||||
export async function getGasSponsoredChains() {
|
||||
if (!THIRDWEB_API_SECRET) {
|
||||
if (!API_SERVER_SECRET) {
|
||||
throw new Error("API_SERVER_SECRET is not set");
|
||||
}
|
||||
const res = await fetch(`${API_SERVER_URL}/v1/chains/gas-sponsored`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-service-api-key": THIRDWEB_API_SECRET,
|
||||
const res = await fetch(
|
||||
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/chains/gas-sponsored`,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-service-api-key": API_SERVER_SECRET,
|
||||
},
|
||||
next: {
|
||||
revalidate: 15 * 60, //15 minutes
|
||||
},
|
||||
},
|
||||
next: {
|
||||
revalidate: 15 * 60, //15 minutes
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
console.error(
|
||||
@@ -37,7 +41,7 @@ export async function getGasSponsoredChains() {
|
||||
|
||||
export function getChains() {
|
||||
return fetch(
|
||||
`${API_SERVER_URL}/v1/chains`,
|
||||
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/chains`,
|
||||
// revalidate every 60 minutes
|
||||
{ next: { revalidate: 60 * 60 } },
|
||||
).then((res) => res.json()) as Promise<{ data: ChainMetadata[] }>;
|
||||
@@ -45,7 +49,7 @@ export function getChains() {
|
||||
|
||||
export function getChainServices() {
|
||||
return fetch(
|
||||
`${API_SERVER_URL}/v1/chains/services`,
|
||||
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/chains/services`,
|
||||
// revalidate every 60 minutes
|
||||
{ next: { revalidate: 60 * 60 } },
|
||||
).then((res) => res.json()) as Promise<{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
|
||||
import { API_SERVER_URL } from "../constants/env";
|
||||
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "../constants/public-envs";
|
||||
|
||||
export type LinkedWallet = {
|
||||
createdAt: string;
|
||||
@@ -14,11 +14,14 @@ export async function getLinkedWallets() {
|
||||
return null;
|
||||
}
|
||||
|
||||
const res = await fetch(`${API_SERVER_URL}/v1/account/wallets`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
const res = await fetch(
|
||||
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/account/wallets`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
if (res.ok) {
|
||||
const json = (await res.json()) as {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import "server-only";
|
||||
import { API_SERVER_URL } from "@/constants/env";
|
||||
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
|
||||
import type { ProjectResponse } from "@thirdweb-dev/service-utils";
|
||||
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
|
||||
|
||||
@@ -13,7 +13,7 @@ export async function getProjects(teamSlug: string) {
|
||||
}
|
||||
|
||||
const teamsRes = await fetch(
|
||||
`${API_SERVER_URL}/v1/teams/${teamSlug}/projects`,
|
||||
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${teamSlug}/projects`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
@@ -34,7 +34,7 @@ export async function getProject(teamSlug: string, projectSlug: string) {
|
||||
}
|
||||
|
||||
const teamsRes = await fetch(
|
||||
`${API_SERVER_URL}/v1/teams/${teamSlug}/projects/${projectSlug}`,
|
||||
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${teamSlug}/projects/${projectSlug}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { API_SERVER_URL, THIRDWEB_API_SECRET } from "../constants/env";
|
||||
import "server-only";
|
||||
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "../constants/public-envs";
|
||||
import { API_SERVER_SECRET } from "../constants/server-envs";
|
||||
|
||||
export type SMSCountryTiers = {
|
||||
tier1: string[];
|
||||
@@ -9,18 +11,21 @@ export type SMSCountryTiers = {
|
||||
};
|
||||
|
||||
export async function getSMSCountryTiers() {
|
||||
if (!THIRDWEB_API_SECRET) {
|
||||
if (!API_SERVER_SECRET) {
|
||||
throw new Error("API_SERVER_SECRET is not set");
|
||||
}
|
||||
const res = await fetch(`${API_SERVER_URL}/v1/sms/list-country-tiers`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-service-api-key": THIRDWEB_API_SECRET,
|
||||
const res = await fetch(
|
||||
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/sms/list-country-tiers`,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-service-api-key": API_SERVER_SECRET,
|
||||
},
|
||||
next: {
|
||||
revalidate: 15 * 60, //15 minutes
|
||||
},
|
||||
},
|
||||
next: {
|
||||
revalidate: 15 * 60, //15 minutes
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
console.error(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
|
||||
import { API_SERVER_URL } from "../constants/env";
|
||||
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "../constants/public-envs";
|
||||
|
||||
export type TeamInvite = {
|
||||
id: string;
|
||||
@@ -25,7 +25,7 @@ export async function getTeamInvites(
|
||||
}
|
||||
|
||||
const res = await fetch(
|
||||
`${API_SERVER_URL}/v1/teams/${teamId}/invites?skip=${options.start}&take=${options.count}`,
|
||||
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${teamId}/invites?skip=${options.start}&take=${options.count}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import "server-only";
|
||||
import { API_SERVER_URL } from "@/constants/env";
|
||||
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
|
||||
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
|
||||
|
||||
const TeamAccountRole = {
|
||||
@@ -33,7 +33,7 @@ export async function getMembers(teamSlug: string) {
|
||||
}
|
||||
|
||||
const teamsRes = await fetch(
|
||||
`${API_SERVER_URL}/v1/teams/${teamSlug}/members`,
|
||||
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${teamSlug}/members`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
@@ -56,7 +56,7 @@ export async function getMemberById(teamSlug: string, memberId: string) {
|
||||
}
|
||||
|
||||
const res = await fetch(
|
||||
`${API_SERVER_URL}/v1/teams/${teamSlug}/members/${memberId}`,
|
||||
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${teamSlug}/members/${memberId}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
|
||||
import { API_SERVER_URL } from "../constants/env";
|
||||
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "../constants/public-envs";
|
||||
import type { ProductSKU } from "../lib/billing";
|
||||
|
||||
type InvoiceLine = {
|
||||
@@ -47,7 +47,7 @@ export async function getTeamSubscriptions(slug: string) {
|
||||
}
|
||||
|
||||
const teamRes = await fetch(
|
||||
`${API_SERVER_URL}/v1/teams/${slug}/subscriptions`,
|
||||
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${slug}/subscriptions`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import "server-only";
|
||||
import { API_SERVER_URL, THIRDWEB_API_SECRET } from "@/constants/env";
|
||||
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
|
||||
import { API_SERVER_SECRET } from "@/constants/server-envs";
|
||||
import type { TeamResponse } from "@thirdweb-dev/service-utils";
|
||||
import { cookies } from "next/headers";
|
||||
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
|
||||
@@ -14,11 +15,14 @@ export async function getTeamBySlug(slug: string) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const teamRes = await fetch(`${API_SERVER_URL}/v1/teams/${slug}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
const teamRes = await fetch(
|
||||
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${slug}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
if (teamRes.ok) {
|
||||
return (await teamRes.json())?.result as Team;
|
||||
}
|
||||
@@ -26,11 +30,14 @@ export async function getTeamBySlug(slug: string) {
|
||||
}
|
||||
|
||||
export async function service_getTeamBySlug(slug: string) {
|
||||
const teamRes = await fetch(`${API_SERVER_URL}/v1/teams/${slug}`, {
|
||||
headers: {
|
||||
"x-service-api-key": THIRDWEB_API_SECRET,
|
||||
const teamRes = await fetch(
|
||||
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${slug}`,
|
||||
{
|
||||
headers: {
|
||||
"x-service-api-key": API_SERVER_SECRET,
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
if (teamRes.ok) {
|
||||
return (await teamRes.json())?.result as Team;
|
||||
@@ -49,7 +56,7 @@ export async function getTeams() {
|
||||
return null;
|
||||
}
|
||||
|
||||
const teamsRes = await fetch(`${API_SERVER_URL}/v1/teams`, {
|
||||
const teamsRes = await fetch(`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
@@ -66,7 +73,7 @@ export async function getDefaultTeam() {
|
||||
return null;
|
||||
}
|
||||
|
||||
const res = await fetch(`${API_SERVER_URL}/v1/teams/~`, {
|
||||
const res = await fetch(`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/~`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
@@ -77,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;
|
||||
@@ -93,5 +100,5 @@ export async function getLastVisitedTeamOrDefaultTeam() {
|
||||
}
|
||||
}
|
||||
|
||||
return getDefaultTeam();
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const UB_BASE_URL = process.env.NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST;
|
||||
@@ -1,7 +1,8 @@
|
||||
"use server";
|
||||
import { getAuthToken } from "app/(app)/api/lib/getAuthToken";
|
||||
import { NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST } from "../../constants/public-envs";
|
||||
|
||||
const UB_BASE_URL = process.env.NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST;
|
||||
const UB_BASE_URL = NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST;
|
||||
|
||||
type Webhook = {
|
||||
url: string;
|
||||
@@ -15,13 +16,15 @@ type Webhook = {
|
||||
|
||||
export async function getWebhooks(props: {
|
||||
clientId: string;
|
||||
teamId: string;
|
||||
}) {
|
||||
const authToken = await getAuthToken();
|
||||
const res = await fetch(`${UB_BASE_URL}/v1/developer/webhooks`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id-override": props.clientId,
|
||||
"x-client-id": props.clientId,
|
||||
"x-team-id": props.teamId,
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
@@ -37,12 +40,20 @@ export async function getWebhooks(props: {
|
||||
|
||||
export async function createWebhook(props: {
|
||||
clientId: string;
|
||||
teamId: string;
|
||||
version?: number;
|
||||
url: string;
|
||||
label: string;
|
||||
secret?: string;
|
||||
}) {
|
||||
const authToken = await getAuthToken();
|
||||
console.log(
|
||||
"UB_BASE_URL",
|
||||
UB_BASE_URL,
|
||||
props.clientId,
|
||||
props.teamId,
|
||||
authToken,
|
||||
);
|
||||
const res = await fetch(`${UB_BASE_URL}/v1/developer/webhooks`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
@@ -53,7 +64,8 @@ export async function createWebhook(props: {
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id-override": props.clientId,
|
||||
"x-client-id": props.clientId,
|
||||
"x-team-id": props.teamId,
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
@@ -68,6 +80,7 @@ export async function createWebhook(props: {
|
||||
|
||||
export async function deleteWebhook(props: {
|
||||
clientId: string;
|
||||
teamId: string;
|
||||
webhookId: string;
|
||||
}) {
|
||||
const authToken = await getAuthToken();
|
||||
@@ -77,7 +90,8 @@ export async function deleteWebhook(props: {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id-override": props.clientId,
|
||||
"x-client-id": props.clientId,
|
||||
"x-team-id": props.teamId,
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
},
|
||||
@@ -108,7 +122,7 @@ export async function getFees(props: {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-team-id": props.teamId,
|
||||
"x-client-id-override": props.clientId,
|
||||
"x-client-id": props.clientId,
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
@@ -133,8 +147,8 @@ export async function updateFee(props: {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id-override": props.clientId,
|
||||
"x-team-id": props.teamId,
|
||||
"x-client-id": props.clientId,
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
@@ -150,3 +164,87 @@ export async function updateFee(props: {
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
export type PaymentsResponse = {
|
||||
data: Payment[];
|
||||
meta: {
|
||||
totalCount: number;
|
||||
};
|
||||
};
|
||||
export type Payment = {
|
||||
id: string;
|
||||
blockNumber?: bigint;
|
||||
transactionId: string;
|
||||
clientId: string;
|
||||
sender: string;
|
||||
receiver: string;
|
||||
developerFeeRecipient: string;
|
||||
developerFeeBps: number;
|
||||
transactions: Array<{
|
||||
chainId: number;
|
||||
transactionHash: string;
|
||||
}>;
|
||||
status: "PENDING" | "COMPLETED" | "FAILED" | "NOT_FOUND";
|
||||
type: "buy" | "sell" | "transfer";
|
||||
originAmount: bigint;
|
||||
destinationAmount: bigint;
|
||||
purchaseData: unknown;
|
||||
originToken: {
|
||||
address: string;
|
||||
symbol: string;
|
||||
decimals: number;
|
||||
chainId: number;
|
||||
};
|
||||
destinationToken: {
|
||||
address: string;
|
||||
symbol: string;
|
||||
decimals: number;
|
||||
chainId: number;
|
||||
};
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
export async function getPayments(props: {
|
||||
clientId: string;
|
||||
teamId: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}) {
|
||||
const authToken = await getAuthToken();
|
||||
|
||||
// Build URL with query parameters if provided
|
||||
let url = `${UB_BASE_URL}/v1/developer/payments`;
|
||||
const queryParams = new URLSearchParams();
|
||||
|
||||
if (props.limit) {
|
||||
queryParams.append("limit", props.limit.toString());
|
||||
}
|
||||
|
||||
if (props.offset) {
|
||||
queryParams.append("offset", props.offset.toString());
|
||||
}
|
||||
|
||||
// Append query params to URL if any exist
|
||||
const queryString = queryParams.toString();
|
||||
if (queryString) {
|
||||
url = `${url}?${queryString}`;
|
||||
}
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": props.clientId,
|
||||
"x-team-id": props.teamId,
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(text);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return json as PaymentsResponse;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
"use server";
|
||||
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
|
||||
import { UB_BASE_URL } from "./constants";
|
||||
|
||||
export type TokenMetadata = {
|
||||
name: string;
|
||||
symbol: string;
|
||||
address: string;
|
||||
decimals: number;
|
||||
chainId: number;
|
||||
iconUri?: string;
|
||||
};
|
||||
|
||||
export async function getUniversalBridgeTokens(props: {
|
||||
chainId?: number;
|
||||
}) {
|
||||
const url = new URL(`${UB_BASE_URL}/v1/tokens`);
|
||||
|
||||
if (props.chainId) {
|
||||
url.searchParams.append("chainId", String(props.chainId));
|
||||
}
|
||||
url.searchParams.append("limit", "1000");
|
||||
|
||||
const res = await fetch(url.toString(), {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-secret-key": DASHBOARD_THIRDWEB_SECRET_KEY,
|
||||
} as Record<string, string>,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(text);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return json.data as Array<TokenMetadata>;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import "server-only";
|
||||
import { unstable_cache } from "next/cache";
|
||||
import { ANALYTICS_SERVICE_URL } from "../../constants/server-envs";
|
||||
|
||||
export type RPCUsageDataItem = {
|
||||
date: string;
|
||||
@@ -16,7 +17,7 @@ export const fetchRPCUsage = unstable_cache(
|
||||
to: string;
|
||||
period: "day" | "week" | "month" | "year" | "all";
|
||||
}) => {
|
||||
const analyticsEndpoint = process.env.ANALYTICS_SERVICE_URL as string;
|
||||
const analyticsEndpoint = ANALYTICS_SERVICE_URL;
|
||||
const url = new URL(`${analyticsEndpoint}/v2/rpc/usage-types`);
|
||||
url.searchParams.set("teamId", params.teamId);
|
||||
if (params.projectId) {
|
||||
@@ -79,7 +80,7 @@ export const getLast24HoursRPCUsage = unstable_cache(
|
||||
projectId?: string;
|
||||
authToken: string;
|
||||
}) => {
|
||||
const analyticsEndpoint = process.env.ANALYTICS_SERVICE_URL as string;
|
||||
const analyticsEndpoint = ANALYTICS_SERVICE_URL;
|
||||
const url = new URL(`${analyticsEndpoint}/v2/rpc/24-hours`);
|
||||
url.searchParams.set("teamId", params.teamId);
|
||||
if (params.projectId) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import "server-only";
|
||||
|
||||
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
|
||||
import { API_SERVER_URL } from "../constants/env";
|
||||
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "../constants/public-envs";
|
||||
|
||||
export type VerifiedDomainResponse =
|
||||
| {
|
||||
@@ -27,7 +27,7 @@ export async function checkDomainVerification(
|
||||
}
|
||||
|
||||
const res = await fetch(
|
||||
`${API_SERVER_URL}/v1/teams/${teamIdOrSlug}/verified-domain`,
|
||||
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${teamIdOrSlug}/verified-domain`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
@@ -54,7 +54,7 @@ export async function createDomainVerification(
|
||||
}
|
||||
|
||||
const res = await fetch(
|
||||
`${API_SERVER_URL}/v1/teams/${teamIdOrSlug}/verified-domain`,
|
||||
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${teamIdOrSlug}/verified-domain`,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({ domain }),
|
||||
|
||||
@@ -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());
|
||||
@@ -144,18 +144,26 @@ export function SingleNetworkSelector(props: {
|
||||
side?: "left" | "right" | "top" | "bottom";
|
||||
disableChainId?: boolean;
|
||||
align?: "center" | "start" | "end";
|
||||
disableTestnets?: boolean;
|
||||
placeholder?: string;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
const { allChains, idToChain } = useAllChainsData();
|
||||
|
||||
const chainsToShow = useMemo(() => {
|
||||
if (!props.chainIds) {
|
||||
return allChains;
|
||||
let chains = allChains;
|
||||
|
||||
if (props.disableTestnets) {
|
||||
chains = chains.filter((chain) => !chain.testnet);
|
||||
}
|
||||
const chainIdSet = new Set(props.chainIds);
|
||||
return allChains.filter((chain) => chainIdSet.has(chain.chainId));
|
||||
}, [allChains, props.chainIds]);
|
||||
|
||||
if (props.chainIds) {
|
||||
const chainIdSet = new Set(props.chainIds);
|
||||
chains = chains.filter((chain) => chainIdSet.has(chain.chainId));
|
||||
}
|
||||
|
||||
return chains;
|
||||
}, [allChains, props.chainIds, props.disableTestnets]);
|
||||
|
||||
const options = useMemo(() => {
|
||||
return chainsToShow.map((chain) => {
|
||||
@@ -173,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());
|
||||
@@ -218,6 +226,7 @@ export function SingleNetworkSelector(props: {
|
||||
<SelectWithSearch
|
||||
searchPlaceholder="Search by Name or Chain ID"
|
||||
value={String(props.chainId)}
|
||||
showCheck={false}
|
||||
options={options}
|
||||
onValueChange={(chainId) => {
|
||||
props.onChange(Number(chainId));
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
BadgeContainer,
|
||||
storybookThirdwebClient,
|
||||
} from "../../../stories/utils";
|
||||
import { TokenSelector } from "./TokenSelector";
|
||||
|
||||
const meta = {
|
||||
title: "blocks/Cards/TokenSelector",
|
||||
component: Story,
|
||||
parameters: {
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof Story>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="container flex max-w-6xl flex-col gap-8 py-10">
|
||||
<Variant label="No Chains selected by default" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Variant(props: {
|
||||
label: string;
|
||||
selectedChainId?: number;
|
||||
}) {
|
||||
const [token, setToken] = useState<
|
||||
| {
|
||||
address: string;
|
||||
chainId: number;
|
||||
}
|
||||
| undefined
|
||||
>(undefined);
|
||||
|
||||
return (
|
||||
<BadgeContainer label={props.label}>
|
||||
<TokenSelector
|
||||
addNativeTokenIfMissing={false}
|
||||
showCheck={false}
|
||||
selectedToken={token}
|
||||
chainId={props.selectedChainId}
|
||||
client={storybookThirdwebClient}
|
||||
onChange={(v) => {
|
||||
setToken({
|
||||
address: v.address,
|
||||
chainId: v.chainId,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
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";
|
||||
import { SelectWithSearch } from "./select-with-search";
|
||||
|
||||
type Option = { label: string; value: string };
|
||||
|
||||
const checksummedNativeTokenAddress = getAddress(NATIVE_TOKEN_ADDRESS);
|
||||
|
||||
export function TokenSelector(props: {
|
||||
selectedToken: { chainId: number; address: string } | undefined;
|
||||
onChange: (token: TokenMetadata) => void;
|
||||
className?: string;
|
||||
popoverContentClassName?: string;
|
||||
chainId?: number;
|
||||
side?: "left" | "right" | "top" | "bottom";
|
||||
disableAddress?: boolean;
|
||||
align?: "center" | "start" | "end";
|
||||
placeholder?: string;
|
||||
client: ThirdwebClient;
|
||||
disabled?: boolean;
|
||||
enabled?: boolean;
|
||||
showCheck: boolean;
|
||||
addNativeTokenIfMissing: boolean;
|
||||
}) {
|
||||
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.map((token) => {
|
||||
return {
|
||||
label: token.symbol,
|
||||
value: `${token.chainId}:${token.address}`,
|
||||
};
|
||||
}) || []
|
||||
);
|
||||
}, [tokens]);
|
||||
|
||||
const searchFn = useCallback(
|
||||
(option: Option, searchValue: string) => {
|
||||
const token = addressChainToToken.get(option.value);
|
||||
if (!token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Number.isInteger(Number(searchValue))) {
|
||||
return String(token.chainId).startsWith(searchValue);
|
||||
}
|
||||
return (
|
||||
token.name.toLowerCase().includes(searchValue.toLowerCase()) ||
|
||||
token.symbol.toLowerCase().includes(searchValue.toLowerCase()) ||
|
||||
token.address.toLowerCase().includes(searchValue.toLowerCase())
|
||||
);
|
||||
},
|
||||
[addressChainToToken],
|
||||
);
|
||||
|
||||
const renderOption = useCallback(
|
||||
(option: Option) => {
|
||||
const token = addressChainToToken.get(option.value);
|
||||
if (!token) {
|
||||
return option.label;
|
||||
}
|
||||
const resolvedSrc = token.iconUri
|
||||
? replaceIpfsUrl(token.iconUri, props.client)
|
||||
: fallbackChainIcon;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<span className="flex grow gap-2 truncate text-left">
|
||||
<Img
|
||||
// render different image element if src changes to avoid showing old image while loading new one
|
||||
key={resolvedSrc}
|
||||
className={cn("size-5 rounded-full object-contain")}
|
||||
src={resolvedSrc}
|
||||
loading={"lazy"}
|
||||
alt=""
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
fallback={<img src={fallbackChainIcon} alt="" />}
|
||||
skeleton={
|
||||
<div className="animate-pulse rounded-full bg-border" />
|
||||
}
|
||||
/>
|
||||
{token.symbol}
|
||||
</span>
|
||||
|
||||
{!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>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[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={selectedValue}
|
||||
options={options}
|
||||
onValueChange={(tokenAddress) => {
|
||||
const token = addressChainToToken.get(tokenAddress);
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
props.onChange(token);
|
||||
}}
|
||||
closeOnSelect={true}
|
||||
showCheck={props.showCheck}
|
||||
placeholder={
|
||||
tokensQuery.isPending
|
||||
? "Loading Tokens..."
|
||||
: props.placeholder || "Select Token"
|
||||
}
|
||||
overrideSearchFn={searchFn}
|
||||
renderOption={renderOption}
|
||||
className={props.className}
|
||||
popoverContentClassName={props.popoverContentClassName}
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -34,6 +34,7 @@ interface SelectWithSearchProps
|
||||
side?: "left" | "right" | "top" | "bottom";
|
||||
align?: "center" | "start" | "end";
|
||||
closeOnSelect?: boolean;
|
||||
showCheck?: boolean;
|
||||
}
|
||||
|
||||
export const SelectWithSearch = React.forwardRef<
|
||||
@@ -52,6 +53,7 @@ export const SelectWithSearch = React.forwardRef<
|
||||
popoverContentClassName,
|
||||
searchPlaceholder,
|
||||
closeOnSelect,
|
||||
showCheck = true,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
@@ -126,7 +128,7 @@ export const SelectWithSearch = React.forwardRef<
|
||||
<span
|
||||
className={cn(
|
||||
"truncate text-muted-foreground text-sm",
|
||||
selectedOption && "text-foreground",
|
||||
selectedOption && "w-full text-foreground",
|
||||
)}
|
||||
>
|
||||
{renderOption && selectedOption
|
||||
@@ -193,9 +195,11 @@ export const SelectWithSearch = React.forwardRef<
|
||||
i === optionsToShow.length - 1 ? lastItemRef : undefined
|
||||
}
|
||||
>
|
||||
<div className="flex size-4 items-center justify-center">
|
||||
{isSelected && <CheckIcon className="size-4" />}
|
||||
</div>
|
||||
{showCheck && (
|
||||
<div className="flex size-4 items-center justify-center">
|
||||
{isSelected && <CheckIcon className="size-4" />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="min-w-0 grow">
|
||||
{renderOption ? renderOption(option) : option.label}
|
||||
|
||||
@@ -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> }
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type React from "react";
|
||||
import { useRef } from "react";
|
||||
|
||||
interface ImageUploadProps {
|
||||
value: File | undefined;
|
||||
onChange?: (files: File[]) => void;
|
||||
children?: React.ReactNode;
|
||||
variant?: React.ComponentProps<typeof Button>["variant"];
|
||||
className?: string;
|
||||
multiple?: boolean;
|
||||
accept: string;
|
||||
}
|
||||
|
||||
export function ImageUploadButton(props: ImageUploadProps) {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(e.target.files || []);
|
||||
props.onChange?.(files);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
variant={props.variant}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className={props.className}
|
||||
>
|
||||
{props.children}
|
||||
</Button>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple={props.multiple}
|
||||
accept={props.accept}
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
aria-label="Upload image"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export const isProd =
|
||||
(process.env.VERCEL_ENV || process.env.NEXT_PUBLIC_VERCEL_ENV) ===
|
||||
"production";
|
||||
|
||||
export const PROD_OR_DEV_URL = isProd ? "thirdweb.com" : "thirdweb-dev.com";
|
||||
|
||||
export const BASE_URL = isProd
|
||||
? "https://thirdweb.com"
|
||||
: (process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL
|
||||
? `https://${process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL}`
|
||||
: "http://localhost:3000") || "https://thirdweb-dev.com";
|
||||
@@ -1,62 +0,0 @@
|
||||
export const DASHBOARD_THIRDWEB_CLIENT_ID =
|
||||
process.env.NEXT_PUBLIC_DASHBOARD_CLIENT_ID || "";
|
||||
|
||||
export const DASHBOARD_THIRDWEB_SECRET_KEY =
|
||||
process.env.DASHBOARD_SECRET_KEY || "";
|
||||
|
||||
export const NEXT_PUBLIC_NEBULA_APP_CLIENT_ID =
|
||||
process.env.NEXT_PUBLIC_NEBULA_APP_CLIENT_ID || "";
|
||||
|
||||
export const NEBULA_APP_SECRET_KEY = process.env.NEBULA_APP_SECRET_KEY || "";
|
||||
|
||||
export const THIRDWEB_VAULT_URL =
|
||||
process.env.NEXT_PUBLIC_THIRDWEB_VAULT_URL || "";
|
||||
|
||||
export const THIRDWEB_ENGINE_CLOUD_URL =
|
||||
process.env.NEXT_PUBLIC_ENGINE_CLOUD_URL || "";
|
||||
|
||||
export const THIRDWEB_API_SECRET = process.env.API_SERVER_SECRET || "";
|
||||
|
||||
export const IPFS_GATEWAY_URL =
|
||||
(process.env.NEXT_PUBLIC_IPFS_GATEWAY_URL as string) ||
|
||||
"https://{clientId}.ipfscdn.io/ipfs/{cid}/{path}";
|
||||
|
||||
export const isProd =
|
||||
(process.env.VERCEL_ENV || process.env.NEXT_PUBLIC_VERCEL_ENV) ===
|
||||
"production";
|
||||
|
||||
export const PROD_OR_DEV_URL = isProd ? "thirdweb.com" : "thirdweb-dev.com";
|
||||
|
||||
export const DASHBOARD_STORAGE_URL =
|
||||
process.env.NEXT_PUBLIC_DASHBOARD_UPLOAD_SERVER ||
|
||||
"https://storage.thirdweb.com";
|
||||
|
||||
export const API_SERVER_URL =
|
||||
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com";
|
||||
|
||||
export const BRIDGE_URL =
|
||||
process.env.NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST || "https://bridge.thirdweb.com";
|
||||
|
||||
/**
|
||||
* Faucet stuff
|
||||
*/
|
||||
// Cloudflare Turnstile Site key
|
||||
export const TURNSTILE_SITE_KEY =
|
||||
process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY || "";
|
||||
export const THIRDWEB_ENGINE_FAUCET_WALLET =
|
||||
process.env.NEXT_PUBLIC_THIRDWEB_ENGINE_FAUCET_WALLET || "";
|
||||
export const THIRDWEB_ENGINE_URL = process.env.THIRDWEB_ENGINE_URL;
|
||||
export const THIRDWEB_ACCESS_TOKEN = process.env.THIRDWEB_ACCESS_TOKEN;
|
||||
// Comma-separated list of chain IDs to disable faucet for.
|
||||
export const DISABLE_FAUCET_CHAIN_IDS = process.env.DISABLE_FAUCET_CHAIN_IDS;
|
||||
|
||||
export const BASE_URL = isProd
|
||||
? "https://thirdweb.com"
|
||||
: (process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL
|
||||
? `https://${process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL}`
|
||||
: "http://localhost:3000") || "https://thirdweb-dev.com";
|
||||
|
||||
export const NEXT_PUBLIC_NEBULA_URL = process.env.NEXT_PUBLIC_NEBULA_URL;
|
||||
|
||||
export const INSIGHT_SERVICE_API_KEY =
|
||||
process.env.INSIGHT_SERVICE_API_KEY || "";
|
||||
@@ -0,0 +1,36 @@
|
||||
export const NET_PUBLIC_DASHBOARD_THIRDWEB_CLIENT_ID =
|
||||
process.env.NEXT_PUBLIC_DASHBOARD_CLIENT_ID || "";
|
||||
|
||||
export const NEXT_PUBLIC_NEBULA_APP_CLIENT_ID =
|
||||
process.env.NEXT_PUBLIC_NEBULA_APP_CLIENT_ID || "";
|
||||
|
||||
export const NEXT_PUBLIC_THIRDWEB_VAULT_URL =
|
||||
process.env.NEXT_PUBLIC_THIRDWEB_VAULT_URL || "";
|
||||
|
||||
export const NEXT_PUBLIC_ENGINE_CLOUD_URL =
|
||||
process.env.NEXT_PUBLIC_ENGINE_CLOUD_URL || "";
|
||||
|
||||
export const NEXT_PUBLIC_IPFS_GATEWAY_URL =
|
||||
(process.env.NEXT_PUBLIC_IPFS_GATEWAY_URL as string) ||
|
||||
"https://{clientId}.ipfscdn.io/ipfs/{cid}/{path}";
|
||||
|
||||
export const NEXT_PUBLIC_DASHBOARD_UPLOAD_SERVER =
|
||||
process.env.NEXT_PUBLIC_DASHBOARD_UPLOAD_SERVER ||
|
||||
"https://storage.thirdweb.com";
|
||||
|
||||
export const NEXT_PUBLIC_THIRDWEB_API_HOST =
|
||||
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com";
|
||||
|
||||
export const NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST =
|
||||
process.env.NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST || "https://bridge.thirdweb.com";
|
||||
|
||||
export const NEXT_PUBLIC_TURNSTILE_SITE_KEY =
|
||||
process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY || "";
|
||||
|
||||
export const NEXT_PUBLIC_THIRDWEB_ENGINE_FAUCET_WALLET =
|
||||
process.env.NEXT_PUBLIC_THIRDWEB_ENGINE_FAUCET_WALLET || "";
|
||||
|
||||
export const NEXT_PUBLIC_NEBULA_URL = process.env.NEXT_PUBLIC_NEBULA_URL || "";
|
||||
|
||||
export const NEXT_PUBLIC_DEMO_ENGINE_URL =
|
||||
process.env.NEXT_PUBLIC_DEMO_ENGINE_URL || "";
|
||||
@@ -0,0 +1,109 @@
|
||||
import "server-only";
|
||||
import { experimental_taintUniqueValue } from "react";
|
||||
import { isProd } from "./env-utils";
|
||||
|
||||
// Make sure to taint the server only envs here with experimental_taintUniqueValue ONLY if they contain a UNIQUE sensitive value
|
||||
// if an env has a generic value that may appear naturally in client components - do not taint it
|
||||
|
||||
export const DASHBOARD_THIRDWEB_SECRET_KEY =
|
||||
process.env.DASHBOARD_SECRET_KEY || "";
|
||||
|
||||
experimental_taintUniqueValue(
|
||||
"Do not pass DASHBOARD_THIRDWEB_SECRET_KEY to the client",
|
||||
process,
|
||||
DASHBOARD_THIRDWEB_SECRET_KEY,
|
||||
);
|
||||
|
||||
export const NEBULA_APP_SECRET_KEY = process.env.NEBULA_APP_SECRET_KEY || "";
|
||||
|
||||
if (NEBULA_APP_SECRET_KEY) {
|
||||
experimental_taintUniqueValue(
|
||||
"Do not pass NEBULA_APP_SECRET_KEY to the client",
|
||||
process,
|
||||
NEBULA_APP_SECRET_KEY,
|
||||
);
|
||||
}
|
||||
|
||||
export const API_SERVER_SECRET = process.env.API_SERVER_SECRET || "";
|
||||
|
||||
if (API_SERVER_SECRET) {
|
||||
experimental_taintUniqueValue(
|
||||
"Do not pass API_SERVER_SECRET to the client",
|
||||
process,
|
||||
API_SERVER_SECRET,
|
||||
);
|
||||
}
|
||||
|
||||
export const THIRDWEB_ENGINE_URL = process.env.THIRDWEB_ENGINE_URL || "";
|
||||
|
||||
export const THIRDWEB_ACCESS_TOKEN = process.env.THIRDWEB_ACCESS_TOKEN || "";
|
||||
|
||||
if (THIRDWEB_ACCESS_TOKEN) {
|
||||
experimental_taintUniqueValue(
|
||||
"Do not pass THIRDWEB_ACCESS_TOKEN to the client",
|
||||
process,
|
||||
THIRDWEB_ACCESS_TOKEN,
|
||||
);
|
||||
}
|
||||
|
||||
// Comma-separated list of chain IDs to disable faucet for.
|
||||
// Note: Can not taint this - because the value is too generic
|
||||
export const DISABLE_FAUCET_CHAIN_IDS =
|
||||
process.env.DISABLE_FAUCET_CHAIN_IDS || "";
|
||||
|
||||
export const INSIGHT_SERVICE_API_KEY =
|
||||
process.env.INSIGHT_SERVICE_API_KEY || "";
|
||||
|
||||
// The value of INSIGHT_SERVICE_API_KEY on dev is generic - so we don't taint it
|
||||
// TODO - have better INSIGHT_SERVICE_API_KEY on dev
|
||||
if (isProd && INSIGHT_SERVICE_API_KEY) {
|
||||
experimental_taintUniqueValue(
|
||||
"Do not pass INSIGHT_SERVICE_API_KEY to the client",
|
||||
process,
|
||||
INSIGHT_SERVICE_API_KEY,
|
||||
);
|
||||
}
|
||||
|
||||
export const MORALIS_API_KEY = process.env.MORALIS_API_KEY || "";
|
||||
|
||||
if (MORALIS_API_KEY) {
|
||||
experimental_taintUniqueValue(
|
||||
"Do not pass MORALIS_API_KEY to the client",
|
||||
process,
|
||||
MORALIS_API_KEY,
|
||||
);
|
||||
}
|
||||
|
||||
export const ANALYTICS_SERVICE_URL = process.env.ANALYTICS_SERVICE_URL || "";
|
||||
|
||||
export const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY || "";
|
||||
|
||||
if (STRIPE_SECRET_KEY) {
|
||||
experimental_taintUniqueValue(
|
||||
"Do not pass STRIPE_SECRET_KEY to the client",
|
||||
process,
|
||||
STRIPE_SECRET_KEY,
|
||||
);
|
||||
}
|
||||
|
||||
export const API_ROUTES_CLIENT_ID = process.env.API_ROUTES_CLIENT_ID || "";
|
||||
|
||||
export const TURNSTILE_SECRET_KEY = process.env.TURNSTILE_SECRET_KEY || "";
|
||||
|
||||
if (TURNSTILE_SECRET_KEY) {
|
||||
experimental_taintUniqueValue(
|
||||
"Do not pass TURNSTILE_SECRET_KEY to the client",
|
||||
process,
|
||||
TURNSTILE_SECRET_KEY,
|
||||
);
|
||||
}
|
||||
|
||||
export const REDIS_URL = process.env.REDIS_URL || "";
|
||||
|
||||
if (REDIS_URL) {
|
||||
experimental_taintUniqueValue(
|
||||
"Do not pass REDIS_URL to the client",
|
||||
process,
|
||||
REDIS_URL,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { getConfiguredThirdwebClient } from "./thirdweb.server";
|
||||
|
||||
export function getClientThirdwebClient(params?: {
|
||||
jwt: string | undefined | null;
|
||||
teamId: string | undefined | null;
|
||||
}) {
|
||||
return getConfiguredThirdwebClient({
|
||||
secretKey: params?.jwt ?? undefined,
|
||||
teamId: params?.teamId ?? undefined,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import "server-only";
|
||||
|
||||
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "./server-envs";
|
||||
import { getConfiguredThirdwebClient } from "./thirdweb.server";
|
||||
|
||||
export const serverThirdwebClient = getConfiguredThirdwebClient({
|
||||
teamId: undefined,
|
||||
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
|
||||
});
|
||||
@@ -4,7 +4,7 @@ import { useActiveAccount } from "thirdweb/react";
|
||||
import type { GetAuthTokenResponse } from "../../app/(app)/api/auth/get-auth-token/route";
|
||||
import { LAST_USED_TEAM_ID } from "../../constants/cookies";
|
||||
import { getCookie } from "../../lib/cookie";
|
||||
import { getThirdwebClient } from "./thirdweb.server";
|
||||
import { getClientThirdwebClient } from "./thirdweb-client.client";
|
||||
|
||||
// returns a thirdweb client with optional JWT passed i
|
||||
|
||||
@@ -38,7 +38,7 @@ export function useThirdwebClient(jwt?: string) {
|
||||
return useMemo(
|
||||
// prefer jwt from props over the one from the token query if it exists
|
||||
() =>
|
||||
getThirdwebClient({
|
||||
getClientThirdwebClient({
|
||||
jwt: jwt || query.data,
|
||||
teamId: lastUsedTeamId,
|
||||
}),
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {
|
||||
DASHBOARD_THIRDWEB_CLIENT_ID,
|
||||
DASHBOARD_THIRDWEB_SECRET_KEY,
|
||||
IPFS_GATEWAY_URL,
|
||||
} from "@/constants/env";
|
||||
NET_PUBLIC_DASHBOARD_THIRDWEB_CLIENT_ID,
|
||||
NEXT_PUBLIC_IPFS_GATEWAY_URL,
|
||||
} from "@/constants/public-envs";
|
||||
import {
|
||||
THIRDWEB_BRIDGE_URL,
|
||||
THIRDWEB_BUNDLER_DOMAIN,
|
||||
THIRDWEB_INAPP_WALLET_DOMAIN,
|
||||
THIRDWEB_INSIGHT_API_DOMAIN,
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
THIRDWEB_SOCIAL_API_DOMAIN,
|
||||
THIRDWEB_STORAGE_DOMAIN,
|
||||
} from "constants/urls";
|
||||
import { createThirdwebClient } from "thirdweb";
|
||||
import { type ThirdwebClient, createThirdwebClient } from "thirdweb";
|
||||
import { populateEip712Transaction } from "thirdweb/transaction";
|
||||
import {
|
||||
getTransactionDecorator,
|
||||
@@ -22,15 +22,10 @@ import {
|
||||
import { getZkPaymasterData } from "thirdweb/wallets/smart";
|
||||
import { getVercelEnv } from "../../lib/vercel-utils";
|
||||
|
||||
// returns a thirdweb client with optional JWT passed in
|
||||
export function getThirdwebClient(
|
||||
options:
|
||||
| {
|
||||
jwt: string | null | undefined;
|
||||
teamId: string | undefined;
|
||||
}
|
||||
| undefined,
|
||||
) {
|
||||
export function getConfiguredThirdwebClient(options: {
|
||||
secretKey: string | undefined;
|
||||
teamId: string | undefined;
|
||||
}): ThirdwebClient {
|
||||
if (getVercelEnv() !== "production") {
|
||||
// if not on production: run this when creating a client to set the domains
|
||||
setThirdwebDomains({
|
||||
@@ -41,6 +36,7 @@ export function getThirdwebClient(
|
||||
social: THIRDWEB_SOCIAL_API_DOMAIN,
|
||||
bundler: THIRDWEB_BUNDLER_DOMAIN,
|
||||
insight: THIRDWEB_INSIGHT_API_DOMAIN,
|
||||
bridge: THIRDWEB_BRIDGE_URL,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -78,12 +74,12 @@ export function getThirdwebClient(
|
||||
}
|
||||
|
||||
return createThirdwebClient({
|
||||
teamId: options?.teamId,
|
||||
secretKey: options?.jwt ? options.jwt : DASHBOARD_THIRDWEB_SECRET_KEY,
|
||||
clientId: DASHBOARD_THIRDWEB_CLIENT_ID,
|
||||
teamId: options.teamId,
|
||||
secretKey: options.secretKey,
|
||||
clientId: NET_PUBLIC_DASHBOARD_THIRDWEB_CLIENT_ID,
|
||||
config: {
|
||||
storage: {
|
||||
gatewayUrl: IPFS_GATEWAY_URL,
|
||||
gatewayUrl: NEXT_PUBLIC_IPFS_GATEWAY_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
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import { ArrowUpRightIcon } from "lucide-react";
|
||||
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 title = "Universal Bridge: Swap, Bridge, and Onramp";
|
||||
const description =
|
||||
"Swap, bridge, and on-ramp to any EVM chain with thirdweb's Universal Bridge.";
|
||||
|
||||
@@ -19,7 +20,7 @@ export default async function RoutesPage({
|
||||
searchParams,
|
||||
}: { searchParams: Record<string, string | string[]> }) {
|
||||
const { chainId } = searchParams;
|
||||
const client = getThirdwebClient(undefined);
|
||||
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">
|
||||
@@ -35,6 +36,34 @@ export default async function RoutesPage({
|
||||
src="/assets/login/background.svg"
|
||||
className="-bottom-12 -right-12 pointer-events-none absolute lg:right-0 lg:bottom-0"
|
||||
/>
|
||||
|
||||
<div className="absolute inset-x-0 bottom-24 z-20">
|
||||
<div className="container mx-auto px-4">
|
||||
<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>
|
||||
</div>
|
||||
</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)}
|
||||
|
||||
+3
-5
@@ -1,8 +1,8 @@
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { defineChain } from "thirdweb";
|
||||
import { getChainMetadata } from "thirdweb/chains";
|
||||
import { serverThirdwebClient } from "../../../../../../../@/constants/thirdweb-client.server";
|
||||
|
||||
type RouteListCardProps = {
|
||||
originChainId: number;
|
||||
@@ -15,7 +15,6 @@ type RouteListCardProps = {
|
||||
destinationTokenIconUri?: string | null;
|
||||
destinationTokenSymbol: string;
|
||||
destinationTokenName: string;
|
||||
client: ThirdwebClient;
|
||||
};
|
||||
|
||||
export async function RouteListCard({
|
||||
@@ -27,7 +26,6 @@ export async function RouteListCard({
|
||||
destinationTokenAddress,
|
||||
destinationTokenIconUri,
|
||||
destinationTokenName,
|
||||
client,
|
||||
}: RouteListCardProps) {
|
||||
const [
|
||||
originChain,
|
||||
@@ -42,13 +40,13 @@ export async function RouteListCard({
|
||||
originTokenIconUri
|
||||
? resolveSchemeWithErrorHandler({
|
||||
uri: originTokenIconUri,
|
||||
client,
|
||||
client: serverThirdwebClient,
|
||||
})
|
||||
: undefined,
|
||||
destinationTokenIconUri
|
||||
? resolveSchemeWithErrorHandler({
|
||||
uri: destinationTokenIconUri,
|
||||
client,
|
||||
client: serverThirdwebClient,
|
||||
})
|
||||
: undefined,
|
||||
]);
|
||||
|
||||
+18
-20
@@ -1,7 +1,7 @@
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { TableCell, TableRow } from "@/components/ui/table";
|
||||
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
|
||||
import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { defineChain, getChainMetadata } from "thirdweb/chains";
|
||||
|
||||
type RouteListRowProps = {
|
||||
@@ -15,7 +15,6 @@ type RouteListRowProps = {
|
||||
destinationTokenIconUri?: string | null;
|
||||
destinationTokenSymbol?: string;
|
||||
destinationTokenName?: string;
|
||||
client: ThirdwebClient;
|
||||
};
|
||||
|
||||
export async function RouteListRow({
|
||||
@@ -27,7 +26,6 @@ export async function RouteListRow({
|
||||
destinationTokenAddress,
|
||||
destinationTokenIconUri,
|
||||
destinationTokenSymbol,
|
||||
client,
|
||||
}: RouteListRowProps) {
|
||||
const [
|
||||
originChain,
|
||||
@@ -42,32 +40,32 @@ export async function RouteListRow({
|
||||
originTokenIconUri
|
||||
? resolveSchemeWithErrorHandler({
|
||||
uri: originTokenIconUri,
|
||||
client,
|
||||
client: serverThirdwebClient,
|
||||
})
|
||||
: undefined,
|
||||
destinationTokenIconUri
|
||||
? resolveSchemeWithErrorHandler({
|
||||
uri: destinationTokenIconUri,
|
||||
client,
|
||||
client: serverThirdwebClient,
|
||||
})
|
||||
: undefined,
|
||||
]);
|
||||
|
||||
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
|
||||
@@ -78,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"
|
||||
/>
|
||||
@@ -87,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
|
||||
@@ -113,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"
|
||||
/>
|
||||
@@ -122,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
-89
@@ -6,9 +6,7 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import type { Address } from "thirdweb";
|
||||
import { checksumAddress } from "thirdweb/utils";
|
||||
import { getRoutes } from "../../../utils";
|
||||
import { ChainlistPagination } from "../client/pagination";
|
||||
import { RouteListCard } from "./routelist-card";
|
||||
@@ -26,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") {
|
||||
@@ -46,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")) {
|
||||
@@ -54,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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -129,37 +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 client = getThirdwebClient(undefined);
|
||||
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}
|
||||
@@ -172,7 +118,6 @@ export async function RoutesData(props: {
|
||||
destinationTokenIconUri={route.destinationToken.iconUri}
|
||||
destinationTokenSymbol={route.destinationToken.symbol}
|
||||
destinationTokenName={route.destinationToken.name}
|
||||
client={client}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -180,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"
|
||||
@@ -196,7 +141,6 @@ export async function RoutesData(props: {
|
||||
destinationTokenIconUri={route.destinationToken.iconUri}
|
||||
destinationTokenSymbol={route.destinationToken.symbol}
|
||||
destinationTokenName={route.destinationToken.name}
|
||||
client={client}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
@@ -205,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}
|
||||
|
||||
@@ -1,35 +1,70 @@
|
||||
import "server-only";
|
||||
|
||||
import { BRIDGE_URL, DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/env";
|
||||
import { getAuthToken } from "app/(app)/api/lib/getAuthToken";
|
||||
import { NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST } from "@/constants/public-envs";
|
||||
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
|
||||
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(`${BRIDGE_URL}/v1/routes`);
|
||||
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;
|
||||
}
|
||||
|
||||
+8
-8
@@ -20,9 +20,9 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
THIRDWEB_ENGINE_FAUCET_WALLET,
|
||||
TURNSTILE_SITE_KEY,
|
||||
} from "@/constants/env";
|
||||
NEXT_PUBLIC_THIRDWEB_ENGINE_FAUCET_WALLET,
|
||||
NEXT_PUBLIC_TURNSTILE_SITE_KEY,
|
||||
} from "@/constants/public-envs";
|
||||
import { useThirdwebClient } from "@/constants/thirdweb.client";
|
||||
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@@ -90,7 +90,7 @@ export function FaucetButton({
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const definedChain = mapV4ChainToV5Chain(chain);
|
||||
const faucetWalletBalanceQuery = useWalletBalance({
|
||||
address: THIRDWEB_ENGINE_FAUCET_WALLET,
|
||||
address: NEXT_PUBLIC_THIRDWEB_ENGINE_FAUCET_WALLET,
|
||||
chain: definedChain,
|
||||
client,
|
||||
});
|
||||
@@ -260,7 +260,7 @@ export function FaucetButton({
|
||||
<FormControl>
|
||||
<Turnstile
|
||||
className="mt-4"
|
||||
siteKey={TURNSTILE_SITE_KEY}
|
||||
siteKey={NEXT_PUBLIC_TURNSTILE_SITE_KEY}
|
||||
onSuccess={(token) => field.onChange(token)}
|
||||
/>
|
||||
</FormControl>
|
||||
@@ -342,7 +342,7 @@ function SendFundsToFaucetModalContent(props: {
|
||||
const sendNativeTokenTx = prepareTransaction({
|
||||
chain: props.chain,
|
||||
client: props.client,
|
||||
to: THIRDWEB_ENGINE_FAUCET_WALLET,
|
||||
to: NEXT_PUBLIC_THIRDWEB_ENGINE_FAUCET_WALLET,
|
||||
value: toWei(values.amount.toString()),
|
||||
});
|
||||
|
||||
@@ -370,8 +370,8 @@ function SendFundsToFaucetModalContent(props: {
|
||||
copyIconPosition="right"
|
||||
variant="outline"
|
||||
className="w-full justify-between bg-card py-2 font-mono"
|
||||
textToCopy={THIRDWEB_ENGINE_FAUCET_WALLET}
|
||||
textToShow={THIRDWEB_ENGINE_FAUCET_WALLET}
|
||||
textToCopy={NEXT_PUBLIC_THIRDWEB_ENGINE_FAUCET_WALLET}
|
||||
textToShow={NEXT_PUBLIC_THIRDWEB_ENGINE_FAUCET_WALLET}
|
||||
tooltip={undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
+24
-5
@@ -3,7 +3,8 @@
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { DASHBOARD_THIRDWEB_CLIENT_ID, isProd } from "@/constants/env";
|
||||
import { isProd } from "@/constants/env-utils";
|
||||
import { NET_PUBLIC_DASHBOARD_THIRDWEB_CLIENT_ID } from "@/constants/public-envs";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { CircleCheckIcon, XIcon } from "lucide-react";
|
||||
import { hostnameEndsWith } from "utils/url";
|
||||
@@ -13,7 +14,7 @@ function useChainStatswithRPC(_rpcUrl: string) {
|
||||
let rpcUrl = _rpcUrl.replace(
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
"${THIRDWEB_API_KEY}",
|
||||
DASHBOARD_THIRDWEB_CLIENT_ID,
|
||||
NET_PUBLIC_DASHBOARD_THIRDWEB_CLIENT_ID,
|
||||
);
|
||||
|
||||
// based on the environment hit dev or production
|
||||
@@ -31,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,
|
||||
}),
|
||||
});
|
||||
@@ -40,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) => {
|
||||
@@ -114,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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
-3
@@ -1,6 +1,5 @@
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { ChainIcon } from "../../../../components/server/chain-icon";
|
||||
import { PayModalButton } from "../client/PayModal";
|
||||
@@ -9,7 +8,6 @@ import { SectionTitle } from "./SectionTitle";
|
||||
|
||||
export function BuyFundsSection(props: {
|
||||
chain: ChainMetadata;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
const sanitizedChainName = props.chain.name.replace("Mainnet", "").trim();
|
||||
|
||||
@@ -22,7 +20,6 @@ export function BuyFundsSection(props: {
|
||||
<ChainIcon
|
||||
className="-mr-2 size-12 rounded-full border p-1"
|
||||
iconUrl={props.chain.icon?.url}
|
||||
client={props.client}
|
||||
/>
|
||||
<CreditCardIcon
|
||||
bg="hsl(var(--background))"
|
||||
|
||||
+1
-4
@@ -1,4 +1,3 @@
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { getFaucetClaimAmount } from "../../../../../../api/testnet-faucet/claim/claim-amount";
|
||||
import { ChainIcon } from "../../../../components/server/chain-icon";
|
||||
@@ -9,9 +8,8 @@ import { SectionTitle } from "./SectionTitle";
|
||||
export async function FaucetSection(props: {
|
||||
chain: ChainMetadata;
|
||||
isLoggedIn: boolean;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
const { chain, isLoggedIn, client } = props;
|
||||
const { chain, isLoggedIn } = props;
|
||||
|
||||
// Check eligibility.
|
||||
const sanitizedChainName = chain.name.replace("Mainnet", "").trim();
|
||||
@@ -26,7 +24,6 @@ export async function FaucetSection(props: {
|
||||
<ChainIcon
|
||||
className="-mr-2 size-12 rounded-full border p-1"
|
||||
iconUrl={props.chain.icon?.url}
|
||||
client={client}
|
||||
/>
|
||||
<GiftIcon bg="hsl(var(--background))" className="-ml-2 size-12" />
|
||||
</div>
|
||||
|
||||
-3
@@ -1,7 +1,6 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Link from "next/link";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { mapV4ChainToV5Chain } from "../../../../../../../../contexts/map-chains";
|
||||
import { ChainIcon } from "../../../../components/server/chain-icon";
|
||||
@@ -11,7 +10,6 @@ type ChainHeaderProps = {
|
||||
headerImageUrl?: string;
|
||||
logoUrl?: string;
|
||||
chain: ChainMetadata;
|
||||
client: ThirdwebClient;
|
||||
};
|
||||
|
||||
// TODO: improve the behavior when clicking "Get started with thirdweb", currently just redirects to the dashboard
|
||||
@@ -41,7 +39,6 @@ export function ChainHeader(props: ChainHeaderProps) {
|
||||
{/* chain logo */}
|
||||
|
||||
<ChainIcon
|
||||
client={props.client}
|
||||
iconUrl={props.logoUrl}
|
||||
className={cn(
|
||||
"-translate-y-[50%] absolute top-0 left-0 size-20 overflow-hidden rounded-full border border-border bg-muted p-2 lg:size-36 lg:p-4",
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { ChevronDownIcon, TicketCheckIcon } from "lucide-react";
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
@@ -71,10 +71,7 @@ export default async function ChainPageLayout(props: {
|
||||
}
|
||||
|
||||
const chainMetadata = await getChainMetadata(chain.chainId);
|
||||
const client = getThirdwebClient({
|
||||
jwt: authToken,
|
||||
teamId: undefined,
|
||||
});
|
||||
const client = getClientThirdwebClient(undefined);
|
||||
|
||||
const chainPromptPrefix = `\
|
||||
You are assisting users exploring the chain ${chain.name} (Chain ID: ${chain.chainId}). Provide concise insights into the types of applications and activities prevalent on this chain, such as DeFi protocols, NFT marketplaces, or gaming platforms. Highlight notable projects or trends without delving into technical details like consensus mechanisms or gas fees.
|
||||
@@ -163,7 +160,6 @@ The following is the user's message:
|
||||
headerImageUrl={chainMetadata?.headerImgUrl}
|
||||
logoUrl={chain.icon?.url}
|
||||
chain={chain}
|
||||
client={client}
|
||||
/>
|
||||
|
||||
<div className="h-4 md:h-8" />
|
||||
|
||||
+2
-3
@@ -1,8 +1,7 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import { ImageResponse } from "next/og";
|
||||
import { download } from "thirdweb/storage";
|
||||
import { fetchChain } from "utils/fetchChain";
|
||||
import { serverThirdwebClient } from "../../../../../../@/constants/thirdweb-client.server";
|
||||
|
||||
// Route segment config
|
||||
export const runtime = "edge";
|
||||
@@ -79,7 +78,7 @@ export default async function Image({
|
||||
chain.icon?.url && hasWorkingChainIcon
|
||||
? download({
|
||||
uri: chain.icon.url,
|
||||
client: getThirdwebClient(undefined),
|
||||
client: serverThirdwebClient,
|
||||
}).then((res) => res.arrayBuffer())
|
||||
: undefined,
|
||||
// download the background image (based on chain)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { CircleAlertIcon } from "lucide-react";
|
||||
import { getRawAccount } from "../../../../account/settings/getAccount";
|
||||
import { getUserThirdwebClient } from "../../../../api/lib/getAuthToken";
|
||||
import { getChain, getChainMetadata } from "../../utils";
|
||||
import NextSteps from "./components/client/NextSteps";
|
||||
import { BuyFundsSection } from "./components/server/BuyFundsSection";
|
||||
@@ -19,7 +18,6 @@ export default async function Page(props: {
|
||||
const chainMetadata = await getChainMetadata(chain.chainId);
|
||||
const isDeprecated = chain.status === "deprecated";
|
||||
|
||||
const client = await getUserThirdwebClient();
|
||||
const account = await getRawAccount();
|
||||
|
||||
return (
|
||||
@@ -44,9 +42,9 @@ export default async function Page(props: {
|
||||
|
||||
{/* Faucet / Buy Funds */}
|
||||
{chain.testnet ? (
|
||||
<FaucetSection chain={chain} isLoggedIn={!!account} client={client} />
|
||||
<FaucetSection chain={chain} isLoggedIn={!!account} />
|
||||
) : chain.services.find((c) => c.service === "pay" && c.enabled) ? (
|
||||
<BuyFundsSection chain={chain} client={client} />
|
||||
<BuyFundsSection chain={chain} />
|
||||
) : null}
|
||||
|
||||
{/* Chain Overview */}
|
||||
|
||||
+4
-5
@@ -1,5 +1,4 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { localhost } from "thirdweb/chains";
|
||||
import { getRawAccount } from "../../../../../../account/settings/getAccount";
|
||||
import { getContractPageParamsInfo } from "../../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../../_utils/getContractPageMetadata";
|
||||
@@ -20,17 +19,17 @@ export default async function Page(props: {
|
||||
notFound();
|
||||
}
|
||||
|
||||
if (info.chainMetadata.chainId === localhost.id) {
|
||||
if (info.isLocalhostChain) {
|
||||
return (
|
||||
<ContractDirectListingsPageClient
|
||||
contract={info.contract}
|
||||
contract={info.clientContract}
|
||||
isLoggedIn={!!account}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { isDirectListingSupported, isInsightSupported } =
|
||||
await getContractPageMetadata(info.contract);
|
||||
await getContractPageMetadata(info.serverContract);
|
||||
|
||||
if (!isDirectListingSupported) {
|
||||
redirect(`/${params.chain_id}/${params.contractAddress}`);
|
||||
@@ -38,7 +37,7 @@ export default async function Page(props: {
|
||||
|
||||
return (
|
||||
<ContractDirectListingsPage
|
||||
contract={info.contract}
|
||||
contract={info.clientContract}
|
||||
isLoggedIn={!!account}
|
||||
isInsightSupported={isInsightSupported}
|
||||
/>
|
||||
|
||||
+4
-5
@@ -1,5 +1,4 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { localhost } from "thirdweb/chains";
|
||||
import { getRawAccount } from "../../../../../../account/settings/getAccount";
|
||||
import { getContractPageParamsInfo } from "../../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../../_utils/getContractPageMetadata";
|
||||
@@ -21,17 +20,17 @@ export default async function Page(props: {
|
||||
|
||||
const twAccount = await getRawAccount();
|
||||
|
||||
if (info.chainMetadata.chainId === localhost.id) {
|
||||
if (info.isLocalhostChain) {
|
||||
return (
|
||||
<ContractEnglishAuctionsPageClient
|
||||
contract={info.contract}
|
||||
contract={info.clientContract}
|
||||
isLoggedIn={!!twAccount}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { isEnglishAuctionSupported, isInsightSupported } =
|
||||
await getContractPageMetadata(info.contract);
|
||||
await getContractPageMetadata(info.serverContract);
|
||||
|
||||
if (!isEnglishAuctionSupported) {
|
||||
redirect(`/${params.chain_id}/${params.contractAddress}`);
|
||||
@@ -39,7 +38,7 @@ export default async function Page(props: {
|
||||
|
||||
return (
|
||||
<ContractEnglishAuctionsPage
|
||||
contract={info.contract}
|
||||
contract={info.clientContract}
|
||||
isLoggedIn={!!twAccount}
|
||||
isInsightSupported={isInsightSupported}
|
||||
/>
|
||||
|
||||
+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(
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { getContractPageMetadataSetup } from "../_utils/getContractPageMetadata";
|
||||
import { getContractPageMetadataSetup } from "../_utils/getContractPageMetadataSetup";
|
||||
|
||||
export function useContractPageMetadata(contract: ThirdwebContract) {
|
||||
return useQuery({
|
||||
|
||||
+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) {
|
||||
|
||||
+3
-1
@@ -25,7 +25,7 @@ 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";
|
||||
|
||||
const TRACKING_CATEGORY = "add_to_dashboard_upsell";
|
||||
|
||||
@@ -147,6 +147,8 @@ function AddToProjectModalContent(props: {
|
||||
teamId: params.teamId,
|
||||
projectId: params.projectId,
|
||||
chainId: props.chainId,
|
||||
deploymentType: undefined,
|
||||
contractType: undefined,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
|
||||
+15
-13
@@ -1,10 +1,9 @@
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
|
||||
import { mapV4ChainToV5Chain } from "contexts/map-chains";
|
||||
import { cookies } from "next/headers";
|
||||
import { getAddress, getContract, isAddress } from "thirdweb";
|
||||
import { LAST_USED_TEAM_ID } from "../../../../../../../constants/cookies";
|
||||
import { localhost } from "thirdweb/chains";
|
||||
import { fetchChainWithLocalOverrides } from "../../../../../../../utils/fetchChainWithLocalOverrides";
|
||||
import { getAuthToken } from "../../../../../api/lib/getAuthToken";
|
||||
import { getUserThirdwebClient } from "../../../../../api/lib/getAuthToken";
|
||||
|
||||
export async function getContractPageParamsInfo(params: {
|
||||
contractAddress: string;
|
||||
@@ -19,24 +18,27 @@ export async function getContractPageParamsInfo(params: {
|
||||
}
|
||||
|
||||
// attempt to get the auth token
|
||||
const token = await getAuthToken();
|
||||
const cookiesObj = await cookies();
|
||||
const teamId = cookiesObj.get(LAST_USED_TEAM_ID)?.value;
|
||||
|
||||
const contract = getContract({
|
||||
const serverContract = getContract({
|
||||
address: contractAddress,
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
chain: mapV4ChainToV5Chain(chainMetadata),
|
||||
client: serverThirdwebClient,
|
||||
});
|
||||
|
||||
const clientContract = getContract({
|
||||
address: contractAddress,
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
chain: mapV4ChainToV5Chain(chainMetadata),
|
||||
// if we have the auth token pass it into the client
|
||||
client: getThirdwebClient({
|
||||
jwt: token || undefined,
|
||||
teamId,
|
||||
}),
|
||||
client: await getUserThirdwebClient(),
|
||||
});
|
||||
|
||||
return {
|
||||
chainMetadata,
|
||||
contract,
|
||||
serverContract,
|
||||
clientContract,
|
||||
isLocalhostChain: chainMetadata.chainId === localhost.id,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+3
-73
@@ -1,26 +1,7 @@
|
||||
import { resolveFunctionSelectors } from "lib/selectors";
|
||||
import "server-only";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { contractType as getContractType } from "thirdweb/extensions/thirdweb";
|
||||
import {
|
||||
isERC20ClaimConditionsSupported,
|
||||
isERC721ClaimConditionsSupported,
|
||||
} from "./detectedFeatures/claimConditions";
|
||||
import {
|
||||
isAccountContract,
|
||||
isAccountFactoryContract,
|
||||
isAccountPermissionsSupported,
|
||||
} from "./detectedFeatures/erc4337";
|
||||
import {
|
||||
isDirectListingSupported,
|
||||
isEnglishAuctionSupported,
|
||||
} from "./detectedFeatures/marketplace";
|
||||
import { isModularCoreContract } from "./detectedFeatures/modular";
|
||||
import {
|
||||
isPermissionsEnumerableSupported,
|
||||
isPermissionsSupported,
|
||||
} from "./detectedFeatures/permissions";
|
||||
import { supportedERCs } from "./detectedFeatures/supportedERCs";
|
||||
import { type EmbedTypeToShow, getEmbedTypeToShow } from "./getEmbedTypeToShow";
|
||||
import { getContractPageMetadataSetup } from "./getContractPageMetadataSetup";
|
||||
import type { EmbedTypeToShow } from "./getEmbedTypeToShow";
|
||||
import { isInsightSupportedForChain } from "./isAnalyticsSupportedForChain";
|
||||
|
||||
export type ContractPageMetadata = {
|
||||
@@ -49,54 +30,3 @@ export type ContractPageMetadata = {
|
||||
export async function getContractPageMetadata(contract: ThirdwebContract) {
|
||||
return getContractPageMetadataSetup(contract, isInsightSupportedForChain);
|
||||
}
|
||||
|
||||
export async function getContractPageMetadataSetup(
|
||||
contract: ThirdwebContract,
|
||||
isAnalyticsSupportedFn: (chainId: number) => Promise<boolean>,
|
||||
): Promise<ContractPageMetadata> {
|
||||
const [
|
||||
functionSelectorsResult,
|
||||
isInsightSupportedResult,
|
||||
contractTypeResult,
|
||||
] = await Promise.allSettled([
|
||||
resolveFunctionSelectors(contract),
|
||||
isAnalyticsSupportedFn(contract.chain.id),
|
||||
getContractType({ contract }),
|
||||
]);
|
||||
|
||||
const functionSelectors =
|
||||
functionSelectorsResult.status === "fulfilled"
|
||||
? functionSelectorsResult.value
|
||||
: [];
|
||||
|
||||
const isInsightSupported =
|
||||
isInsightSupportedResult.status === "fulfilled"
|
||||
? isInsightSupportedResult.value
|
||||
: true;
|
||||
|
||||
const contractType =
|
||||
contractTypeResult.status === "fulfilled" ? contractTypeResult.value : null;
|
||||
|
||||
return {
|
||||
supportedERCs: supportedERCs(functionSelectors),
|
||||
isDirectListingSupported: isDirectListingSupported(functionSelectors),
|
||||
isEnglishAuctionSupported: isEnglishAuctionSupported(functionSelectors),
|
||||
isPermissionsSupported: isPermissionsSupported(functionSelectors),
|
||||
isPermissionsEnumerableSupported:
|
||||
isPermissionsEnumerableSupported(functionSelectors),
|
||||
isModularCore: isModularCoreContract(functionSelectors),
|
||||
embedType: getEmbedTypeToShow(functionSelectors),
|
||||
isInsightSupported: isInsightSupported,
|
||||
isSplitSupported: contractType === "Split",
|
||||
isVoteContract: contractType === "VoteERC20",
|
||||
isERC721ClaimConditionsSupported:
|
||||
isERC721ClaimConditionsSupported(functionSelectors),
|
||||
isERC20ClaimConditionsSupported:
|
||||
isERC20ClaimConditionsSupported(functionSelectors),
|
||||
isAccountFactory: isAccountFactoryContract(functionSelectors),
|
||||
isAccount: isAccountContract(functionSelectors),
|
||||
isAccountPermissionsSupported:
|
||||
isAccountPermissionsSupported(functionSelectors),
|
||||
functionSelectors,
|
||||
};
|
||||
}
|
||||
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
import { resolveFunctionSelectors } from "lib/selectors";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { contractType as getContractType } from "thirdweb/extensions/thirdweb";
|
||||
import {
|
||||
isERC20ClaimConditionsSupported,
|
||||
isERC721ClaimConditionsSupported,
|
||||
} from "./detectedFeatures/claimConditions";
|
||||
import {
|
||||
isAccountContract,
|
||||
isAccountFactoryContract,
|
||||
isAccountPermissionsSupported,
|
||||
} from "./detectedFeatures/erc4337";
|
||||
import {
|
||||
isDirectListingSupported,
|
||||
isEnglishAuctionSupported,
|
||||
} from "./detectedFeatures/marketplace";
|
||||
import { isModularCoreContract } from "./detectedFeatures/modular";
|
||||
import {
|
||||
isPermissionsEnumerableSupported,
|
||||
isPermissionsSupported,
|
||||
} from "./detectedFeatures/permissions";
|
||||
import { supportedERCs } from "./detectedFeatures/supportedERCs";
|
||||
import { type EmbedTypeToShow, getEmbedTypeToShow } from "./getEmbedTypeToShow";
|
||||
|
||||
type ContractPageMetadata = {
|
||||
supportedERCs: {
|
||||
isERC721: boolean;
|
||||
isERC1155: boolean;
|
||||
isERC20: boolean;
|
||||
};
|
||||
isDirectListingSupported: boolean;
|
||||
isEnglishAuctionSupported: boolean;
|
||||
isPermissionsSupported: boolean;
|
||||
isVoteContract: boolean;
|
||||
isPermissionsEnumerableSupported: boolean;
|
||||
isModularCore: boolean;
|
||||
embedType: EmbedTypeToShow;
|
||||
isInsightSupported: boolean;
|
||||
isSplitSupported: boolean;
|
||||
isERC721ClaimConditionsSupported: boolean;
|
||||
isERC20ClaimConditionsSupported: boolean;
|
||||
isAccountFactory: boolean;
|
||||
isAccount: boolean;
|
||||
isAccountPermissionsSupported: boolean;
|
||||
functionSelectors: string[];
|
||||
};
|
||||
|
||||
export async function getContractPageMetadataSetup(
|
||||
contract: ThirdwebContract,
|
||||
isAnalyticsSupportedFn: (chainId: number) => Promise<boolean>,
|
||||
): Promise<ContractPageMetadata> {
|
||||
const [
|
||||
functionSelectorsResult,
|
||||
isInsightSupportedResult,
|
||||
contractTypeResult,
|
||||
] = await Promise.allSettled([
|
||||
resolveFunctionSelectors(contract),
|
||||
isAnalyticsSupportedFn(contract.chain.id),
|
||||
getContractType({ contract }),
|
||||
]);
|
||||
|
||||
const functionSelectors =
|
||||
functionSelectorsResult.status === "fulfilled"
|
||||
? functionSelectorsResult.value
|
||||
: [];
|
||||
|
||||
const isInsightSupported =
|
||||
isInsightSupportedResult.status === "fulfilled"
|
||||
? isInsightSupportedResult.value
|
||||
: true;
|
||||
|
||||
const contractType =
|
||||
contractTypeResult.status === "fulfilled" ? contractTypeResult.value : null;
|
||||
|
||||
return {
|
||||
supportedERCs: supportedERCs(functionSelectors),
|
||||
isDirectListingSupported: isDirectListingSupported(functionSelectors),
|
||||
isEnglishAuctionSupported: isEnglishAuctionSupported(functionSelectors),
|
||||
isPermissionsSupported: isPermissionsSupported(functionSelectors),
|
||||
isPermissionsEnumerableSupported:
|
||||
isPermissionsEnumerableSupported(functionSelectors),
|
||||
isModularCore: isModularCoreContract(functionSelectors),
|
||||
embedType: getEmbedTypeToShow(functionSelectors),
|
||||
isInsightSupported: isInsightSupported,
|
||||
isSplitSupported: contractType === "Split",
|
||||
isVoteContract: contractType === "VoteERC20",
|
||||
isERC721ClaimConditionsSupported:
|
||||
isERC721ClaimConditionsSupported(functionSelectors),
|
||||
isERC20ClaimConditionsSupported:
|
||||
isERC20ClaimConditionsSupported(functionSelectors),
|
||||
isAccountFactory: isAccountFactoryContract(functionSelectors),
|
||||
isAccount: isAccountContract(functionSelectors),
|
||||
isAccountPermissionsSupported:
|
||||
isAccountPermissionsSupported(functionSelectors),
|
||||
functionSelectors,
|
||||
};
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { INSIGHT_SERVICE_API_KEY } from "@/constants/env";
|
||||
import { INSIGHT_SERVICE_API_KEY } from "@/constants/server-envs";
|
||||
import { getVercelEnv } from "lib/vercel-utils";
|
||||
|
||||
const thirdwebDomain =
|
||||
|
||||
+5
-6
@@ -1,5 +1,4 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { localhost } from "thirdweb/chains";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { AccountSignersClient } from "./AccountSigners.client";
|
||||
@@ -18,18 +17,18 @@ export default async function Page(props: {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { contract, chainMetadata } = info;
|
||||
const { clientContract, serverContract, isLocalhostChain } = info;
|
||||
|
||||
if (chainMetadata.chainId === localhost.id) {
|
||||
return <AccountSignersClient contract={contract} />;
|
||||
if (isLocalhostChain) {
|
||||
return <AccountSignersClient contract={clientContract} />;
|
||||
}
|
||||
|
||||
const { isAccountPermissionsSupported } =
|
||||
await getContractPageMetadata(contract);
|
||||
await getContractPageMetadata(serverContract);
|
||||
|
||||
if (!isAccountPermissionsSupported) {
|
||||
redirect(`/${params.chain_id}/${params.contractAddress}`);
|
||||
}
|
||||
|
||||
return <AccountSigners contract={contract} />;
|
||||
return <AccountSigners contract={clientContract} />;
|
||||
}
|
||||
|
||||
+6
-6
@@ -1,5 +1,4 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { localhost } from "thirdweb/chains";
|
||||
import { getRawAccount } from "../../../../../account/settings/getAccount";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
@@ -19,14 +18,15 @@ export default async function Page(props: {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { contract, chainMetadata } = info;
|
||||
const { clientContract, serverContract, chainMetadata, isLocalhostChain } =
|
||||
info;
|
||||
|
||||
const account = await getRawAccount();
|
||||
|
||||
if (contract.chain.id === localhost.id) {
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<AccountPageClient
|
||||
contract={contract}
|
||||
contract={clientContract}
|
||||
chainMetadata={chainMetadata}
|
||||
isLoggedIn={!!account}
|
||||
/>
|
||||
@@ -34,7 +34,7 @@ export default async function Page(props: {
|
||||
}
|
||||
|
||||
const { isAccount, isInsightSupported } =
|
||||
await getContractPageMetadata(contract);
|
||||
await getContractPageMetadata(serverContract);
|
||||
|
||||
if (!isAccount) {
|
||||
redirect(`/${params.chain_id}/${params.contractAddress}`);
|
||||
@@ -42,7 +42,7 @@ export default async function Page(props: {
|
||||
|
||||
return (
|
||||
<AccountPage
|
||||
contract={contract}
|
||||
contract={clientContract}
|
||||
chainMetadata={chainMetadata}
|
||||
isLoggedIn={!!account}
|
||||
isInsightSupported={isInsightSupported}
|
||||
|
||||
+12
-8
@@ -1,5 +1,4 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { localhost } from "thirdweb/chains";
|
||||
import { getRawAccount } from "../../../../../account/settings/getAccount";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
@@ -19,19 +18,24 @@ export default async function Page(props: {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { contract, chainMetadata } = info;
|
||||
const { clientContract, serverContract, isLocalhostChain } = info;
|
||||
|
||||
const account = await getRawAccount();
|
||||
const [account, { isAccountFactory }] = await Promise.all([
|
||||
getRawAccount(),
|
||||
isLocalhostChain
|
||||
? { isAccountFactory: undefined }
|
||||
: getContractPageMetadata(serverContract),
|
||||
]);
|
||||
|
||||
if (chainMetadata.chainId === localhost.id) {
|
||||
return <AccountsPageClient contract={contract} isLoggedIn={!!account} />;
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<AccountsPageClient contract={clientContract} isLoggedIn={!!account} />
|
||||
);
|
||||
}
|
||||
|
||||
const { isAccountFactory } = await getContractPageMetadata(contract);
|
||||
|
||||
if (!isAccountFactory) {
|
||||
redirect(`/${params.chain_id}/${params.contractAddress}`);
|
||||
}
|
||||
|
||||
return <AccountsPage contract={contract} isLoggedIn={!!account} />;
|
||||
return <AccountsPage contract={clientContract} isLoggedIn={!!account} />;
|
||||
}
|
||||
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
|
||||
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
|
||||
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
|
||||
import { ContractAnalyticsPage } from "./ContractAnalyticsPage";
|
||||
|
||||
export function ContractAnalyticsPageClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
writeFnSelectorToNameRecord: Record<string, string>;
|
||||
eventSelectorToNameRecord: Record<string, string>;
|
||||
}) {
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
|
||||
if (metadataQuery.isPending) {
|
||||
return <LoadingPage />;
|
||||
}
|
||||
|
||||
if (metadataQuery.isError) {
|
||||
return <ErrorPage />;
|
||||
}
|
||||
|
||||
if (!metadataQuery.data.isInsightSupported) {
|
||||
return <RedirectToContractOverview contract={props.contract} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ContractAnalyticsPage
|
||||
contract={props.contract}
|
||||
writeFnSelectorToNameRecord={props.writeFnSelectorToNameRecord}
|
||||
eventSelectorToNameRecord={props.eventSelectorToNameRecord}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+8
-18
@@ -1,11 +1,9 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { localhost } from "thirdweb/chains";
|
||||
import { type ThirdwebContract, resolveContractAbi } from "thirdweb/contract";
|
||||
import { type Abi, toEventSelector, toFunctionSelector } from "thirdweb/utils";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { ContractAnalyticsPage } from "./ContractAnalyticsPage";
|
||||
import { ContractAnalyticsPageClient } from "./ContractAnalyticsPage.client";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
@@ -20,21 +18,13 @@ export default async function Page(props: {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { eventSelectorToName, writeFnSelectorToName } = await getSelectors(
|
||||
info.contract,
|
||||
);
|
||||
|
||||
if (info.chainMetadata.chainId === localhost.id) {
|
||||
return (
|
||||
<ContractAnalyticsPageClient
|
||||
contract={info.contract}
|
||||
writeFnSelectorToNameRecord={writeFnSelectorToName}
|
||||
eventSelectorToNameRecord={eventSelectorToName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { isInsightSupported } = await getContractPageMetadata(info.contract);
|
||||
const [
|
||||
{ eventSelectorToName, writeFnSelectorToName },
|
||||
{ isInsightSupported },
|
||||
] = await Promise.all([
|
||||
getSelectors(info.serverContract),
|
||||
getContractPageMetadata(info.serverContract),
|
||||
]);
|
||||
|
||||
if (!isInsightSupported) {
|
||||
redirect(`/${params.chain_id}/${params.contractAddress}`);
|
||||
@@ -42,7 +32,7 @@ export default async function Page(props: {
|
||||
|
||||
return (
|
||||
<ContractAnalyticsPage
|
||||
contract={info.contract}
|
||||
contract={info.clientContract}
|
||||
writeFnSelectorToNameRecord={writeFnSelectorToName}
|
||||
eventSelectorToNameRecord={eventSelectorToName}
|
||||
/>
|
||||
|
||||
+23
-12
@@ -1,5 +1,4 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { localhost } from "thirdweb/chains";
|
||||
import { getRawAccount } from "../../../../../account/settings/getAccount";
|
||||
import { ClaimConditions } from "../_components/claim-conditions/claim-conditions";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
@@ -18,27 +17,39 @@ export default async function Page(props: {
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
const { contract, chainMetadata } = info;
|
||||
const { clientContract, serverContract, isLocalhostChain } = info;
|
||||
|
||||
const account = await getRawAccount();
|
||||
const [
|
||||
account,
|
||||
{
|
||||
isERC20ClaimConditionsSupported,
|
||||
isERC721ClaimConditionsSupported,
|
||||
supportedERCs,
|
||||
},
|
||||
] = await Promise.all([
|
||||
getRawAccount(),
|
||||
isLocalhostChain
|
||||
? {
|
||||
isERC20ClaimConditionsSupported: undefined,
|
||||
isERC721ClaimConditionsSupported: undefined,
|
||||
supportedERCs: undefined,
|
||||
}
|
||||
: getContractPageMetadata(serverContract),
|
||||
]);
|
||||
|
||||
if (chainMetadata.chainId === localhost.id) {
|
||||
return <ClaimConditionsClient contract={contract} isLoggedIn={!!account} />;
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<ClaimConditionsClient contract={clientContract} isLoggedIn={!!account} />
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
isERC20ClaimConditionsSupported,
|
||||
isERC721ClaimConditionsSupported,
|
||||
supportedERCs,
|
||||
} = await getContractPageMetadata(contract);
|
||||
|
||||
if (!isERC20ClaimConditionsSupported && !isERC721ClaimConditionsSupported) {
|
||||
redirect(`/${params.chain_id}/${params.contractAddress}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<ClaimConditions
|
||||
contract={contract}
|
||||
contract={clientContract}
|
||||
isERC20={supportedERCs.isERC20}
|
||||
isLoggedIn={!!account}
|
||||
isMultiphase={true}
|
||||
|
||||
+6
-6
@@ -1,5 +1,4 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { localhost } from "thirdweb/chains";
|
||||
import { resolveContractAbi } from "thirdweb/contract";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { ContractCodePage } from "./contract-code-page";
|
||||
@@ -18,23 +17,24 @@ export default async function Page(props: {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { contract, chainMetadata } = info;
|
||||
const { clientContract, serverContract, chainMetadata, isLocalhostChain } =
|
||||
info;
|
||||
|
||||
if (contract.chain.id === localhost.id) {
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<ContractCodePageClient
|
||||
contract={contract}
|
||||
contract={clientContract}
|
||||
chainMetadata={chainMetadata}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const abi = await resolveContractAbi(contract).catch(() => undefined);
|
||||
const abi = await resolveContractAbi(serverContract).catch(() => undefined);
|
||||
|
||||
return (
|
||||
<ContractCodePage
|
||||
abi={abi}
|
||||
contract={contract}
|
||||
contract={clientContract}
|
||||
chainMetadata={chainMetadata}
|
||||
/>
|
||||
);
|
||||
|
||||
+24
-21
@@ -53,27 +53,28 @@ export default async function Page(props: {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { contract } = info;
|
||||
const { clientContract, serverContract } = info;
|
||||
|
||||
const isModularCore = (await getContractPageMetadata(contract)).isModularCore;
|
||||
const isModularCore = (await getContractPageMetadata(serverContract))
|
||||
.isModularCore;
|
||||
|
||||
let twCloneFactoryContract: Readonly<
|
||||
ContractOptions<[], `0x${string}`>
|
||||
> | null = null;
|
||||
try {
|
||||
twCloneFactoryContract = await getDeployedCloneFactoryContract({
|
||||
chain: contract.chain,
|
||||
client: contract.client,
|
||||
chain: serverContract.chain,
|
||||
client: serverContract.client,
|
||||
});
|
||||
} catch {}
|
||||
|
||||
const originalCode = await eth_getCode(
|
||||
getRpcClient({
|
||||
client: contract.client,
|
||||
chain: contract.chain,
|
||||
client: serverContract.client,
|
||||
chain: serverContract.chain,
|
||||
}),
|
||||
{
|
||||
address: contract.address,
|
||||
address: serverContract.address,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -82,14 +83,14 @@ export default async function Page(props: {
|
||||
let isDirectDeploy = false;
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://contract.thirdweb-dev.com/creation/${contract.chain.id}/${contract.address}`,
|
||||
`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: contract.client,
|
||||
chain: contract.chain,
|
||||
client: serverContract.client,
|
||||
chain: serverContract.chain,
|
||||
});
|
||||
creationTxReceipt = await eth_getTransactionReceipt(rpcClient, {
|
||||
hash: creationData.result[0]?.txHash,
|
||||
@@ -141,7 +142,7 @@ export default async function Page(props: {
|
||||
const chainMetadata = await getChainMetadata(chain);
|
||||
|
||||
const rpcRequest = getRpcClient({
|
||||
client: contract.client,
|
||||
client: serverContract.client,
|
||||
chain,
|
||||
});
|
||||
const code = await eth_getCode(rpcRequest, {
|
||||
@@ -167,14 +168,14 @@ export default async function Page(props: {
|
||||
}
|
||||
}),
|
||||
)
|
||||
).filter((c) => c.chainId !== contract.chain.id);
|
||||
).filter((c) => c.chainId !== serverContract.chain.id);
|
||||
|
||||
let coreMetadata: FetchDeployMetadataResult | undefined;
|
||||
try {
|
||||
coreMetadata = (
|
||||
await fetchPublishedContractsFromDeploy({
|
||||
contract,
|
||||
client: contract.client,
|
||||
contract: serverContract,
|
||||
client: serverContract.client,
|
||||
})
|
||||
).at(-1) as FetchDeployMetadataResult;
|
||||
} catch {}
|
||||
@@ -201,7 +202,7 @@ export default async function Page(props: {
|
||||
// fetch events from contract
|
||||
if (!modules && creationBlockNumber) {
|
||||
const events = await getContractEvents({
|
||||
contract: contract,
|
||||
contract: serverContract,
|
||||
events: [moduleEvent],
|
||||
});
|
||||
|
||||
@@ -216,11 +217,13 @@ export default async function Page(props: {
|
||||
if (!modules && initializeData) {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
const decodedData: any = await decodeFunctionData({
|
||||
contract,
|
||||
contract: serverContract,
|
||||
data: initializeData,
|
||||
});
|
||||
|
||||
const abi = await resolveContractAbi(contract).catch(() => undefined);
|
||||
const abi = await resolveContractAbi(serverContract).catch(
|
||||
() => undefined,
|
||||
);
|
||||
|
||||
if (abi) {
|
||||
const initializeFunction = abi.find(
|
||||
@@ -241,11 +244,11 @@ export default async function Page(props: {
|
||||
(
|
||||
await fetchPublishedContractsFromDeploy({
|
||||
contract: getContract({
|
||||
chain: contract.chain,
|
||||
client: contract.client,
|
||||
chain: serverContract.chain,
|
||||
client: serverContract.client,
|
||||
address: m,
|
||||
}),
|
||||
client: contract.client,
|
||||
client: serverContract.client,
|
||||
})
|
||||
).at(-1),
|
||||
),
|
||||
@@ -275,7 +278,7 @@ export default async function Page(props: {
|
||||
initializeData={initializeData}
|
||||
inputSalt={inputSalt}
|
||||
data={chainsDeployedOn}
|
||||
coreContract={contract}
|
||||
coreContract={clientContract}
|
||||
initCode={initCode}
|
||||
isDirectDeploy={isDirectDeploy}
|
||||
/>
|
||||
|
||||
+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());
|
||||
|
||||
+1
-1
@@ -15,5 +15,5 @@ export default async function Page(props: {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return <EventsFeed contract={info.contract} />;
|
||||
return <EventsFeed contract={info.clientContract} />;
|
||||
}
|
||||
|
||||
+6
-6
@@ -1,5 +1,4 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { localhost } from "thirdweb/chains";
|
||||
import { resolveContractAbi } from "thirdweb/contract";
|
||||
import { getRawAccount } from "../../../../../account/settings/getAccount";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
@@ -19,25 +18,26 @@ export default async function Page(props: {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { contract, chainMetadata } = info;
|
||||
const { clientContract, serverContract, chainMetadata, isLocalhostChain } =
|
||||
info;
|
||||
|
||||
const account = await getRawAccount();
|
||||
|
||||
if (contract.chain.id === localhost.id) {
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<ContractExplorerPageClient
|
||||
contract={contract}
|
||||
contract={clientContract}
|
||||
chainMetadata={chainMetadata}
|
||||
isLoggedIn={!!account}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const abi = await resolveContractAbi(contract).catch(() => undefined);
|
||||
const abi = await resolveContractAbi(serverContract).catch(() => undefined);
|
||||
|
||||
return (
|
||||
<ContractExplorerPage
|
||||
contract={contract}
|
||||
contract={clientContract}
|
||||
abi={abi}
|
||||
chainMetadata={chainMetadata}
|
||||
isLoggedIn={!!account}
|
||||
|
||||
+19
-27
@@ -4,11 +4,11 @@ import type { MinimalTeamsAndProjects } from "components/contract-components/con
|
||||
import { resolveFunctionSelectors } from "lib/selectors";
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { localhost } from "thirdweb/chains";
|
||||
import { getContractMetadata } from "thirdweb/extensions/common";
|
||||
import { isAddress, isContractDeployed } from "thirdweb/utils";
|
||||
import { shortenIfAddress } from "utils/usedapp-external";
|
||||
import { NebulaChatButton } from "../../../../../nebula-app/(app)/components/FloatingChat/FloatingChat";
|
||||
import { examplePrompts } from "../../../../../nebula-app/(app)/data/examplePrompts";
|
||||
import {
|
||||
getAuthTokenWalletAddress,
|
||||
getUserThirdwebClient,
|
||||
@@ -40,7 +40,8 @@ export default async function Layout(props: {
|
||||
return <ConfigureCustomChain chainSlug={params.chain_id} />;
|
||||
}
|
||||
|
||||
const { contract, chainMetadata } = info;
|
||||
const { clientContract, serverContract, chainMetadata, isLocalhostChain } =
|
||||
info;
|
||||
|
||||
if (chainMetadata.status === "deprecated") {
|
||||
notFound();
|
||||
@@ -51,11 +52,11 @@ export default async function Layout(props: {
|
||||
const client = await getUserThirdwebClient();
|
||||
const teamsAndProjects = await getTeamsAndProjectsIfLoggedIn();
|
||||
|
||||
if (contract.chain.id === localhost.id) {
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<ContractPageLayoutClient
|
||||
chainMetadata={chainMetadata}
|
||||
contract={contract}
|
||||
contract={clientContract}
|
||||
teamsAndProjects={teamsAndProjects}
|
||||
client={client}
|
||||
>
|
||||
@@ -65,25 +66,27 @@ export default async function Layout(props: {
|
||||
}
|
||||
|
||||
// check if the contract exists
|
||||
const isValidContract = await isContractDeployed(contract).catch(() => false);
|
||||
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(contract);
|
||||
const contractPageMetadata = await getContractPageMetadata(serverContract);
|
||||
|
||||
const sidebarLinks = getContractPageSidebarLinks({
|
||||
chainSlug: chainMetadata.slug,
|
||||
contractAddress: contract.address,
|
||||
contractAddress: serverContract.address,
|
||||
metadata: contractPageMetadata,
|
||||
});
|
||||
|
||||
const { contractMetadata, externalLinks } =
|
||||
await getContractMetadataHeaderData(contract);
|
||||
await getContractMetadataHeaderData(serverContract);
|
||||
|
||||
const contractAddress = info.contract.address;
|
||||
const chainName = info.chainMetadata.name;
|
||||
const chainId = info.contract.chain.id;
|
||||
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.
|
||||
@@ -92,18 +95,10 @@ Users may be considering integrating the contract into their applications. Discu
|
||||
|
||||
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={contract}
|
||||
contract={clientContract}
|
||||
sidebarLinks={sidebarLinks}
|
||||
dashboardContractMetadata={contractMetadata}
|
||||
externalLinks={externalLinks}
|
||||
@@ -119,13 +114,10 @@ The following is the user's message:`;
|
||||
client={client}
|
||||
nebulaParams={{
|
||||
messagePrefix: contractPromptPrefix,
|
||||
chainIds: [info.contract.chain.id],
|
||||
chainIds: [chainId],
|
||||
wallet: accountAddress ?? undefined,
|
||||
}}
|
||||
examplePrompts={examplePrompts.map((prompt) => ({
|
||||
title: prompt,
|
||||
message: prompt,
|
||||
}))}
|
||||
examplePrompts={examplePrompts}
|
||||
/>
|
||||
{props.children}
|
||||
</ContractPageLayout>
|
||||
@@ -177,9 +169,9 @@ export async function generateMetadata(props: {
|
||||
}
|
||||
|
||||
const [functionSelectors, contractMetadata] = await Promise.all([
|
||||
resolveFunctionSelectors(info.contract),
|
||||
resolveFunctionSelectors(info.serverContract),
|
||||
getContractMetadata({
|
||||
contract: info.contract,
|
||||
contract: info.serverContract,
|
||||
}),
|
||||
]);
|
||||
|
||||
|
||||
+7
-6
@@ -1,5 +1,4 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { localhost } from "thirdweb/chains";
|
||||
import { getRawAccount } from "../../../../../account/settings/getAccount";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
@@ -19,24 +18,26 @@ export default async function Page(props: {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { contract } = info;
|
||||
const { clientContract, serverContract, isLocalhostChain } = info;
|
||||
|
||||
const account = await getRawAccount();
|
||||
|
||||
if (contract.chain.id === localhost.id) {
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<ContractEditModulesPageClient
|
||||
contract={contract}
|
||||
contract={clientContract}
|
||||
isLoggedIn={!!account}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { isModularCore } = await getContractPageMetadata(contract);
|
||||
const { isModularCore } = await getContractPageMetadata(serverContract);
|
||||
|
||||
if (!isModularCore) {
|
||||
redirect(`/${params.chain_id}/${params.contractAddress}`);
|
||||
}
|
||||
|
||||
return <ContractEditModulesPage contract={contract} isLoggedIn={!!account} />;
|
||||
return (
|
||||
<ContractEditModulesPage contract={clientContract} isLoggedIn={!!account} />
|
||||
);
|
||||
}
|
||||
|
||||
+5
-6
@@ -1,5 +1,4 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { localhost } from "thirdweb/chains";
|
||||
import { getRawAccount } from "../../../../../../account/settings/getAccount";
|
||||
import { getAuthTokenWalletAddress } from "../../../../../../api/lib/getAuthToken";
|
||||
import { getContractPageParamsInfo } from "../../_utils/getContractFromParams";
|
||||
@@ -30,11 +29,11 @@ export default async function Page(props: {
|
||||
getRawAccount(),
|
||||
]);
|
||||
|
||||
const { contract } = info;
|
||||
if (contract.chain.id === localhost.id) {
|
||||
const { clientContract, serverContract, isLocalhostChain } = info;
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<TokenIdPageClient
|
||||
contract={contract}
|
||||
contract={clientContract}
|
||||
tokenId={params.tokenId}
|
||||
isLoggedIn={!!account}
|
||||
accountAddress={accountAddress || undefined}
|
||||
@@ -42,7 +41,7 @@ export default async function Page(props: {
|
||||
);
|
||||
}
|
||||
|
||||
const { supportedERCs } = await getContractPageMetadata(contract);
|
||||
const { supportedERCs } = await getContractPageMetadata(serverContract);
|
||||
|
||||
if (!supportedERCs.isERC721 && !supportedERCs.isERC1155) {
|
||||
redirect(`/${params.chain_id}/${params.contractAddress}`);
|
||||
@@ -50,7 +49,7 @@ export default async function Page(props: {
|
||||
|
||||
return (
|
||||
<TokenIdPage
|
||||
contract={contract}
|
||||
contract={clientContract}
|
||||
isErc721={supportedERCs.isERC721}
|
||||
tokenId={params.tokenId}
|
||||
isLoggedIn={!!account}
|
||||
|
||||
+7
-6
@@ -1,5 +1,4 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { localhost } from "thirdweb/chains";
|
||||
import { getRawAccount } from "../../../../../account/settings/getAccount";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
@@ -21,13 +20,15 @@ export default async function Page(props: {
|
||||
|
||||
const account = await getRawAccount();
|
||||
|
||||
const { contract } = info;
|
||||
if (contract.chain.id === localhost.id) {
|
||||
return <ContractNFTPageClient contract={contract} isLoggedIn={!!account} />;
|
||||
const { clientContract, serverContract, isLocalhostChain } = info;
|
||||
if (isLocalhostChain) {
|
||||
return (
|
||||
<ContractNFTPageClient contract={clientContract} isLoggedIn={!!account} />
|
||||
);
|
||||
}
|
||||
|
||||
const { supportedERCs, functionSelectors } =
|
||||
await getContractPageMetadata(contract);
|
||||
await getContractPageMetadata(serverContract);
|
||||
|
||||
if (!supportedERCs.isERC721 && !supportedERCs.isERC1155) {
|
||||
redirect(`/${params.chain_id}/${params.contractAddress}`);
|
||||
@@ -35,7 +36,7 @@ export default async function Page(props: {
|
||||
|
||||
return (
|
||||
<ContractNFTPage
|
||||
contract={contract}
|
||||
contract={clientContract}
|
||||
isErc721={supportedERCs.isERC721}
|
||||
functionSelectors={functionSelectors}
|
||||
isLoggedIn={!!account}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user