Compare commits
191
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ac8cba2e8 | ||
|
|
35aaf24e8a | ||
|
|
ad2d1757e2 | ||
|
|
52aba0ffb8 | ||
|
|
9809d5cf66 | ||
|
|
8b10674481 | ||
|
|
a3c76d5f7d | ||
|
|
f5f6848ead | ||
|
|
82ba2b20fc | ||
|
|
b0e69b8794 | ||
|
|
dd983dab8f | ||
|
|
068cb592dc | ||
|
|
359d008d38 | ||
|
|
7b5682ce27 | ||
|
|
51f46d34cf | ||
|
|
63a45e21cd | ||
|
|
c11fe95416 | ||
|
|
851fee1c83 | ||
|
|
9983e6ccfd | ||
|
|
73c924b440 | ||
|
|
2976af3a63 | ||
|
|
419fb31601 | ||
|
|
c76fe10dd0 | ||
|
|
c63ef1c4e9 | ||
|
|
c7ff3b1020 | ||
|
|
b06a9184c1 | ||
|
|
5080b4707f | ||
|
|
d6bc43c34d | ||
|
|
ef577028fd | ||
|
|
304700f318 | ||
|
|
6cd681c9ce | ||
|
|
144bb33ffd | ||
|
|
01116bb650 | ||
|
|
f07b4b56c5 | ||
|
|
9442dc7729 | ||
|
|
6c4e684ee6 | ||
|
|
68978711f8 | ||
|
|
eca4ce2b82 | ||
|
|
80d5595e7f | ||
|
|
d0bbbfd39d | ||
|
|
e26d81ce1d | ||
|
|
be3316ec1b | ||
|
|
872369770f | ||
|
|
3d48826928 | ||
|
|
fa640ad64b | ||
|
|
eb1a9c3c3e | ||
|
|
a54c1aba35 | ||
|
|
bea4f01742 | ||
|
|
e19f7a21ad | ||
|
|
c37af4f191 | ||
|
|
f964b8aaff | ||
|
|
424e6b8f9b | ||
|
|
763d3de5a8 | ||
|
|
f6f138ccbc | ||
|
|
f5d5e73e57 | ||
|
|
673e51b11c | ||
|
|
282edf7ccb | ||
|
|
a87e91b99c | ||
|
|
0e630c4550 | ||
|
|
11c7bf0955 | ||
|
|
c9e430d092 | ||
|
|
1f440d0c97 | ||
|
|
3a0aac4393 | ||
|
|
7b13015053 | ||
|
|
7f081618e1 | ||
|
|
881c7a96f7 | ||
|
|
ad68969559 | ||
|
|
915d955da1 | ||
|
|
7da16cff2f | ||
|
|
f0edc0f253 | ||
|
|
0a2d269782 | ||
|
|
3d996d1d23 | ||
|
|
40781b8170 | ||
|
|
d907017217 | ||
|
|
cf53c2d7f1 | ||
|
|
207cb9dbeb | ||
|
|
b4d365bdc0 | ||
|
|
ea4e2fe376 | ||
|
|
1d54cff7b4 | ||
|
|
fd04eefea9 | ||
|
|
4cc68310dc | ||
|
|
ccdce496e0 | ||
|
|
2725f337f8 | ||
|
|
2c6ea3e6dd | ||
|
|
219fc69d72 | ||
|
|
8d17685683 | ||
|
|
8ef418c543 | ||
|
|
b88f75b494 | ||
|
|
c83f1c6f7a | ||
|
|
bc0295487b | ||
|
|
90b779107d | ||
|
|
afe187de95 | ||
|
|
759b93beff | ||
|
|
8e357b3cb3 | ||
|
|
6c318f83d6 | ||
|
|
f790c9fae9 | ||
|
|
bcbb3f540a | ||
|
|
931c730a08 | ||
|
|
e0c83121c2 | ||
|
|
a82a31e5ad | ||
|
|
3d235e218a | ||
|
|
c08fb1356e | ||
|
|
68cca6f992 | ||
|
|
e0abf2b273 | ||
|
|
abf0bbaf35 | ||
|
|
23cb3bd98c | ||
|
|
dcef3f4fe7 | ||
|
|
bf46520a6b | ||
|
|
7defce7264 | ||
|
|
ae3a122cf1 | ||
|
|
2412a6958c | ||
|
|
bfd3be764c | ||
|
|
46b9d2dd82 | ||
|
|
3e9eb49787 | ||
|
|
630edadcec | ||
|
|
f757e60846 | ||
|
|
d5d593956e | ||
|
|
9a4b1cb147 | ||
|
|
85fe9491cd | ||
|
|
899051f788 | ||
|
|
5828b9bd45 | ||
|
|
2b68a32eaa | ||
|
|
66ae33ee8b | ||
|
|
e9788096d6 | ||
|
|
706d696434 | ||
|
|
c36cfbb5c7 | ||
|
|
93a9ba9121 | ||
|
|
6a7b556d75 | ||
|
|
e0ace736a8 | ||
|
|
b30369f3cc | ||
|
|
f2121e9519 | ||
|
|
29d05a50bf | ||
|
|
f00f559a12 | ||
|
|
ceba6835dd | ||
|
|
b33c1a30ac | ||
|
|
72fbb1550f | ||
|
|
1e7df5ad3d | ||
|
|
ca16ec783b | ||
|
|
b1a780832e | ||
|
|
9bb65ad5b2 | ||
|
|
db291d2d38 | ||
|
|
9dd0b17f15 | ||
|
|
12082fd5fd | ||
|
|
5ff6063961 | ||
|
|
3db3982fab | ||
|
|
bf90104330 | ||
|
|
8003e93f19 | ||
|
|
51c8318df7 | ||
|
|
f043f1eef4 | ||
|
|
d68da2239b | ||
|
|
6a6b0fba96 | ||
|
|
c934102c51 | ||
|
|
8c58c131ac | ||
|
|
2779afc15f | ||
|
|
fe50abd5ab | ||
|
|
e830109274 | ||
|
|
e7f42e6ac5 | ||
|
|
91ff13b8aa | ||
|
|
47354d5a19 | ||
|
|
8280efb991 | ||
|
|
5f11ae0cd4 | ||
|
|
c9a0c23c6c | ||
|
|
43b0680124 | ||
|
|
cccbde9a2e | ||
|
|
85a6afc377 | ||
|
|
e74aca9c90 | ||
|
|
b0cac940bd | ||
|
|
1a19e14845 | ||
|
|
464af6a54c | ||
|
|
f3c55d7139 | ||
|
|
9545b43675 | ||
|
|
869bbcfa68 | ||
|
|
35c7d21e36 | ||
|
|
edf3aad8c4 | ||
|
|
ab4190e23a | ||
|
|
9086cdba20 | ||
|
|
0082e996ca | ||
|
|
28955e0ac5 | ||
|
|
05550d045f | ||
|
|
a0c45cb25d | ||
|
|
6fc4d838ff | ||
|
|
fe940b3c66 | ||
|
|
080d7f2122 | ||
|
|
ae75073a4a | ||
|
|
781a626461 | ||
|
|
f630912b9b | ||
|
|
163ab8afe1 | ||
|
|
62cfbb7441 | ||
|
|
d7104f69f4 | ||
|
|
23545cb999 | ||
|
|
f9283f389b |
@@ -188,9 +188,16 @@ jobs:
|
||||
- name: Build Packages
|
||||
run: pnpm build
|
||||
|
||||
- name: Report bundle size
|
||||
- name: Report bundle size (thirdweb)
|
||||
uses: andresz1/size-limit-action@94bc357df29c36c8f8d50ea497c3e225c3c95d1d # v1.8.0
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
package_manager: pnpm
|
||||
directory: packages/thirdweb
|
||||
directory: packages/thirdweb
|
||||
|
||||
- name: Report bundle size (nexus)
|
||||
uses: andresz1/size-limit-action@94bc357df29c36c8f8d50ea497c3e225c3c95d1d # v1.8.0
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
package_manager: pnpm
|
||||
directory: packages/nexus
|
||||
@@ -18,7 +18,7 @@ Welcome, AI copilots! This guide captures the coding standards, architectural de
|
||||
2. Formatting & Linting
|
||||
|
||||
- Biome governs formatting and linting; its rules live in biome.json.
|
||||
- Run pnpm biome check --apply before committing.
|
||||
- Run `pnpm fix` & `pnpm lint` before committing, make sure there are no linting errors.
|
||||
- Avoid editor‑specific configs; rely on the shared settings.
|
||||
- make sure everything builds after each file change by running `pnpm build`
|
||||
|
||||
@@ -136,7 +136,7 @@ export function reportContractDeployed(properties: {
|
||||
}
|
||||
```
|
||||
|
||||
- **Client-side only**: never import `posthog-js` in server components.
|
||||
- **Client-side only**: never import `posthog-js` in server components.
|
||||
- **Housekeeping**: Inform **#eng-core-services** before renaming or removing an existing event.
|
||||
|
||||
⸻
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import { defineConfig } from "checkly";
|
||||
import { Frequency } from "checkly/constructs";
|
||||
|
||||
export default defineConfig({
|
||||
checks: {
|
||||
activated: true,
|
||||
browserChecks: {
|
||||
frequency: Frequency.EVERY_24H,
|
||||
testMatch: "./tests/**/*.spec.ts",
|
||||
},
|
||||
checkMatch: "./**/*.check.ts",
|
||||
frequency: Frequency.EVERY_24H,
|
||||
ignoreDirectoriesMatch: [],
|
||||
locations: ["us-east-1", "eu-west-1"],
|
||||
muted: false,
|
||||
playwrightConfig: {
|
||||
use: {
|
||||
baseURL: process.env.ENVIRONMENT_URL || "https://thirdweb.com",
|
||||
},
|
||||
},
|
||||
runtimeId: "2023.09",
|
||||
tags: ["website"],
|
||||
},
|
||||
cli: {
|
||||
runLocation: "eu-west-1",
|
||||
},
|
||||
logicalId: "thirdweb-www",
|
||||
projectName: "thirdweb.com",
|
||||
repoUrl: "https://github.com/thirdweb-dev/dashboard",
|
||||
});
|
||||
@@ -10,7 +10,8 @@ module.exports = [
|
||||
"/wallets",
|
||||
"/account-abstraction",
|
||||
"/payments",
|
||||
"/payments/x402",
|
||||
"/x402",
|
||||
"/nexus",
|
||||
"/auth",
|
||||
"/in-app-wallets",
|
||||
"/transactions",
|
||||
@@ -20,6 +21,7 @@ module.exports = [
|
||||
"/rpc",
|
||||
"/insight",
|
||||
"/storage",
|
||||
"/gateway",
|
||||
// -- end scale category
|
||||
|
||||
// -- ai
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@marsidev/react-turnstile": "^1.1.0",
|
||||
"@number-flow/react": "^0.5.10",
|
||||
"@radix-ui/react-accordion": "^1.2.11",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
@@ -47,7 +48,7 @@
|
||||
"next-themes": "^0.4.6",
|
||||
"nextjs-toploader": "^1.6.12",
|
||||
"nuqs": "^2.4.3",
|
||||
"ogl": "^1.0.11",
|
||||
"p-limit": "^7.2.0",
|
||||
"papaparse": "^5.5.3",
|
||||
"pluralize": "^8.0.0",
|
||||
"posthog-js": "1.256.1",
|
||||
@@ -83,7 +84,6 @@
|
||||
"@chromatic-com/storybook": "4.0.1",
|
||||
"@next/bundle-analyzer": "15.3.5",
|
||||
"@next/eslint-plugin-next": "15.3.5",
|
||||
"@playwright/test": "1.53.2",
|
||||
"@storybook/addon-docs": "9.0.15",
|
||||
"@storybook/addon-links": "9.0.15",
|
||||
"@storybook/addon-onboarding": "9.0.15",
|
||||
@@ -100,7 +100,6 @@
|
||||
"@typescript-eslint/eslint-plugin": "7.14.1",
|
||||
"@typescript-eslint/parser": "7.14.1",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"checkly": "6.0.1",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-biome": "1.9.4",
|
||||
"eslint-plugin-react-compiler": "19.1.0-rc.2",
|
||||
@@ -123,13 +122,11 @@
|
||||
"format": "biome format ./src --write",
|
||||
"knip": "knip",
|
||||
"lint": "biome check ./src && knip && eslint ./src",
|
||||
"playwright": "playwright test",
|
||||
"postbuild": "next-sitemap",
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"start": "next start",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"update-checkly": "npx checkly deploy"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"version": "3.0.0"
|
||||
}
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
},
|
||||
|
||||
{
|
||||
name: "firefox",
|
||||
use: { ...devices["Desktop Firefox"] },
|
||||
},
|
||||
|
||||
{
|
||||
name: "webkit",
|
||||
use: { ...devices["Desktop Safari"] },
|
||||
},
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: { ...devices['Pixel 5'] },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: { ...devices['iPhone 12'] },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||
// },
|
||||
],
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: "html",
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
testDir: "./tests",
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: process.env.ENVIRONMENT_URL || "http://127.0.0.1:3000",
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "on-first-retry",
|
||||
},
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: process.env.CI
|
||||
? undefined
|
||||
: {
|
||||
command: "pnpm run start",
|
||||
reuseExistingServer: !process.env.CI,
|
||||
url: "http://127.0.0.1:3000",
|
||||
},
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
@@ -115,6 +115,16 @@ const projectPageRedirects = [
|
||||
permanent: false,
|
||||
source: `${projectRoute}/connect/analytics`,
|
||||
},
|
||||
{
|
||||
destination: `${projectRoute}/gateway/indexer/:path*`,
|
||||
permanent: false,
|
||||
source: `${projectRoute}/insight/:path*`,
|
||||
},
|
||||
{
|
||||
destination: `${projectRoute}/gateway/rpc/:path*`,
|
||||
permanent: false,
|
||||
source: `${projectRoute}/rpc/:path*`,
|
||||
},
|
||||
];
|
||||
|
||||
const teamPageRedirects = [
|
||||
@@ -479,6 +489,11 @@ async function redirects() {
|
||||
destination: "/tokens",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/payments/x402",
|
||||
destination: "/x402",
|
||||
permanent: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
"use server";
|
||||
|
||||
import { createVaultClient, listEoas } from "@thirdweb-dev/vault-sdk";
|
||||
import { NEXT_PUBLIC_THIRDWEB_VAULT_URL } from "@/constants/public-envs";
|
||||
import type { ProjectWalletSummary } from "@/lib/server/project-wallet";
|
||||
|
||||
interface VaultWalletListItem {
|
||||
id: string;
|
||||
address: string;
|
||||
metadata?: {
|
||||
label?: string;
|
||||
projectId?: string;
|
||||
teamId?: string;
|
||||
type?: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export async function listProjectServerWallets(params: {
|
||||
managementAccessToken: string;
|
||||
projectId: string;
|
||||
pageSize?: number;
|
||||
}): Promise<ProjectWalletSummary[]> {
|
||||
const { managementAccessToken, projectId, pageSize = 100 } = params;
|
||||
|
||||
if (!managementAccessToken || !NEXT_PUBLIC_THIRDWEB_VAULT_URL) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const vaultClient = await createVaultClient({
|
||||
baseUrl: NEXT_PUBLIC_THIRDWEB_VAULT_URL,
|
||||
});
|
||||
|
||||
const response = await listEoas({
|
||||
client: vaultClient,
|
||||
request: {
|
||||
auth: {
|
||||
accessToken: managementAccessToken,
|
||||
},
|
||||
options: {
|
||||
page: 0,
|
||||
// @ts-expect-error - Vault SDK expects snake_case pagination fields
|
||||
page_size: pageSize,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.success || !response.data?.items) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const items = response.data.items as VaultWalletListItem[];
|
||||
|
||||
return items
|
||||
.filter((item) => {
|
||||
return (
|
||||
item.metadata?.projectId === projectId &&
|
||||
(!item.metadata?.type || item.metadata.type === "server-wallet")
|
||||
);
|
||||
})
|
||||
.map<ProjectWalletSummary>((item) => ({
|
||||
id: item.id,
|
||||
address: item.address,
|
||||
label: item.metadata?.label ?? undefined,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error("Failed to list project server wallets", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
"use server";
|
||||
|
||||
import { configure, sendTokens } from "@thirdweb-dev/api";
|
||||
import { THIRDWEB_API_HOST } from "@/constants/urls";
|
||||
|
||||
configure({
|
||||
override: {
|
||||
baseUrl: THIRDWEB_API_HOST,
|
||||
},
|
||||
});
|
||||
|
||||
export async function sendProjectWalletTokens(options: {
|
||||
walletAddress: string;
|
||||
recipientAddress: string;
|
||||
chainId: number;
|
||||
quantityWei: string;
|
||||
publishableKey: string;
|
||||
teamId: string;
|
||||
tokenAddress?: string;
|
||||
secretKey: string;
|
||||
vaultAccessToken?: string;
|
||||
}) {
|
||||
const {
|
||||
walletAddress,
|
||||
recipientAddress,
|
||||
chainId,
|
||||
quantityWei,
|
||||
publishableKey,
|
||||
teamId,
|
||||
tokenAddress,
|
||||
secretKey,
|
||||
vaultAccessToken,
|
||||
} = options;
|
||||
|
||||
if (!secretKey) {
|
||||
return {
|
||||
error: "A project secret key is required to send funds.",
|
||||
ok: false,
|
||||
} as const;
|
||||
}
|
||||
|
||||
const response = await sendTokens({
|
||||
body: {
|
||||
chainId,
|
||||
from: walletAddress,
|
||||
recipients: [
|
||||
{
|
||||
address: recipientAddress,
|
||||
quantity: quantityWei,
|
||||
},
|
||||
],
|
||||
...(tokenAddress ? { tokenAddress } : {}),
|
||||
},
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": publishableKey,
|
||||
"x-secret-key": secretKey,
|
||||
"x-team-id": teamId,
|
||||
...(vaultAccessToken ? { "x-vault-access-token": vaultAccessToken } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
if (response.error || !response.data) {
|
||||
return {
|
||||
error: response.error || "Failed to submit transfer request.",
|
||||
ok: false,
|
||||
} as const;
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
transactionIds: response.data.result?.transactionIds ?? [],
|
||||
} as const;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { revalidatePath, revalidateTag } from "next/cache";
|
||||
|
||||
export async function revalidatePathAction(
|
||||
path: string,
|
||||
@@ -8,3 +8,7 @@ export async function revalidatePathAction(
|
||||
) {
|
||||
revalidatePath(path, type);
|
||||
}
|
||||
|
||||
export async function revalidateCacheTagAction(tag: string) {
|
||||
revalidateTag(tag);
|
||||
}
|
||||
|
||||
@@ -683,7 +683,22 @@ export function reportProductFeedback(properties: {
|
||||
*
|
||||
*/
|
||||
export function reportBridgePageLinkClick(params: {
|
||||
linkType: "integrate-bridge" | "trending-tokens";
|
||||
linkType: "bridge-docs" | "trending-tokens";
|
||||
}) {
|
||||
posthog.capture("bridge page link clicked", params);
|
||||
}
|
||||
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
* - To track which tokens are users are clicking on in the token list page
|
||||
*
|
||||
* ### Who is responsible for this event?
|
||||
* @MananTank
|
||||
*
|
||||
*/
|
||||
export function reportTokenRowClicked(params: {
|
||||
chainId: number;
|
||||
tokenAddress: string;
|
||||
}) {
|
||||
posthog.capture("token row clicked", params);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ import type {
|
||||
WalletStats,
|
||||
WebhookLatencyStats,
|
||||
WebhookSummaryStats,
|
||||
X402QueryParams,
|
||||
X402SettlementStats,
|
||||
} from "@/types/analytics";
|
||||
import { getChains } from "./chain";
|
||||
|
||||
@@ -184,7 +186,7 @@ const cached_getInAppWalletUsage = unstable_cache(
|
||||
if (res?.status !== 200) {
|
||||
const reason = await res?.text();
|
||||
console.error(
|
||||
`Failed to fetch in-app wallet usage, ${res?.status} - ${res.statusText} - ${reason}`,
|
||||
`Failed to fetch user wallets usage, ${res?.status} - ${res.statusText} - ${reason}`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
@@ -289,8 +291,11 @@ const cached_getAggregateUserOpUsage = unstable_cache(
|
||||
]);
|
||||
|
||||
// Aggregate stats across wallet types
|
||||
return userOpStats.reduce(
|
||||
(acc, curr) => {
|
||||
const aggregated = userOpStats.reduce(
|
||||
(
|
||||
acc: UserOpStats & { gasPriceSum: number; gasPriceCount: number },
|
||||
curr,
|
||||
) => {
|
||||
// Skip testnets from the aggregated stats
|
||||
if (curr.chainId) {
|
||||
const chain = chains.data.find(
|
||||
@@ -304,6 +309,10 @@ const cached_getAggregateUserOpUsage = unstable_cache(
|
||||
acc.successful += curr.successful;
|
||||
acc.failed += curr.failed;
|
||||
acc.sponsoredUsd += curr.sponsoredUsd;
|
||||
acc.gasUnits += curr.gasUnits;
|
||||
// For avgGasPrice, we'll track sum and count for proper averaging
|
||||
acc.gasPriceSum += curr.avgGasPrice * curr.successful;
|
||||
acc.gasPriceCount += curr.successful;
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
@@ -311,8 +320,28 @@ const cached_getAggregateUserOpUsage = unstable_cache(
|
||||
failed: 0,
|
||||
sponsoredUsd: 0,
|
||||
successful: 0,
|
||||
gasUnits: 0,
|
||||
avgGasPrice: 0,
|
||||
gasPriceSum: 0,
|
||||
gasPriceCount: 0,
|
||||
},
|
||||
);
|
||||
|
||||
// Calculate the proper average gas price
|
||||
aggregated.avgGasPrice =
|
||||
aggregated.gasPriceCount > 0
|
||||
? aggregated.gasPriceSum / aggregated.gasPriceCount
|
||||
: 0;
|
||||
|
||||
// Return only the UserOpStats fields
|
||||
return {
|
||||
date: aggregated.date,
|
||||
failed: aggregated.failed,
|
||||
sponsoredUsd: aggregated.sponsoredUsd,
|
||||
successful: aggregated.successful,
|
||||
gasUnits: aggregated.gasUnits,
|
||||
avgGasPrice: aggregated.avgGasPrice,
|
||||
};
|
||||
},
|
||||
["getAggregateUserOpUsage"],
|
||||
{
|
||||
@@ -448,6 +477,7 @@ type ActiveStatus = {
|
||||
pay: boolean;
|
||||
inAppWallet: boolean;
|
||||
ecosystemWallet: boolean;
|
||||
engineCloud: boolean;
|
||||
};
|
||||
|
||||
export const isProjectActive = unstable_cache(
|
||||
@@ -479,6 +509,7 @@ export const isProjectActive = unstable_cache(
|
||||
pay: false,
|
||||
rpc: false,
|
||||
sdk: false,
|
||||
engineCloud: false,
|
||||
storage: false,
|
||||
} as ActiveStatus;
|
||||
}
|
||||
@@ -488,7 +519,7 @@ export const isProjectActive = unstable_cache(
|
||||
},
|
||||
["isProjectActive"],
|
||||
{
|
||||
revalidate: 60 * 60, // 1 hour
|
||||
revalidate: 30, // 30 seconds
|
||||
},
|
||||
);
|
||||
|
||||
@@ -904,3 +935,43 @@ export function getInsightUsage(
|
||||
) {
|
||||
return cached_getInsightUsage(normalizedParams(params), authToken);
|
||||
}
|
||||
|
||||
const cached_getX402Settlements = unstable_cache(
|
||||
async (
|
||||
params: X402QueryParams,
|
||||
authToken: string,
|
||||
): Promise<X402SettlementStats[]> => {
|
||||
const searchParams = buildSearchParams(params);
|
||||
|
||||
if (params.groupBy) {
|
||||
searchParams.append("groupBy", params.groupBy);
|
||||
}
|
||||
|
||||
const res = await fetchAnalytics({
|
||||
authToken,
|
||||
url: `v2/x402/settlements?${searchParams.toString()}`,
|
||||
init: {
|
||||
method: "GET",
|
||||
},
|
||||
});
|
||||
|
||||
if (res?.status !== 200) {
|
||||
const reason = await res?.text();
|
||||
console.error(
|
||||
`Failed to fetch x402 settlements: ${res?.status} - ${res.statusText} - ${reason}`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return json.data as X402SettlementStats[];
|
||||
},
|
||||
["getX402Settlements"],
|
||||
{
|
||||
revalidate: 60 * 60, // 1 hour
|
||||
},
|
||||
);
|
||||
|
||||
export function getX402Settlements(params: X402QueryParams, authToken: string) {
|
||||
return cached_getX402Settlements(normalizedParams(params), authToken);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export function projectContractsCacheTag(params: {
|
||||
teamId: string;
|
||||
projectId: string;
|
||||
}) {
|
||||
return `${params.teamId}/${params.projectId}/project-contracts`;
|
||||
}
|
||||
@@ -1,58 +1,111 @@
|
||||
import { unstable_cache } from "next/cache";
|
||||
import pLimit from "p-limit";
|
||||
import { defineChain } from "thirdweb";
|
||||
import { getContract } from "thirdweb/contract";
|
||||
import {
|
||||
getProjectContracts,
|
||||
type ProjectContract,
|
||||
} from "@/api/project/getProjectContracts";
|
||||
import { fetchChainWithLocalOverrides } from "@/utils/fetchChainWithLocalOverrides";
|
||||
import { supportedERCs } from "@/utils/supportedERCs";
|
||||
import { serverThirdwebClient } from "../../constants/thirdweb-client.server";
|
||||
import { resolveFunctionSelectors } from "../../lib/selectors";
|
||||
import { projectContractsCacheTag } from "./cache-tag";
|
||||
|
||||
export async function getSortedDeployedContracts(params: {
|
||||
onlyMainnet?: boolean;
|
||||
type GetFilteredProjectContractsParams = {
|
||||
teamId: string;
|
||||
projectId: string;
|
||||
authToken: string;
|
||||
deploymentType: string | undefined;
|
||||
}) {
|
||||
};
|
||||
|
||||
type ProjectContractWithIsToken = ProjectContract & { isToken: boolean };
|
||||
|
||||
async function getProjectContractsWithIsTokenInfo(
|
||||
params: GetFilteredProjectContractsParams,
|
||||
): Promise<ProjectContractWithIsToken[]> {
|
||||
const contracts = await getProjectContracts({
|
||||
authToken: params.authToken,
|
||||
deploymentType: params.deploymentType,
|
||||
deploymentType: undefined, // don't pass it to api to fetch all contracts, we'll filter/merge manually here
|
||||
projectId: params.projectId,
|
||||
teamId: params.teamId,
|
||||
});
|
||||
|
||||
const chainIds = Array.from(new Set(contracts.map((c) => c.chainId)));
|
||||
const chains = (
|
||||
await Promise.allSettled(
|
||||
chainIds.map((chainId) =>
|
||||
fetchChainWithLocalOverrides(chainId.toString()),
|
||||
),
|
||||
)
|
||||
)
|
||||
.filter((c) => c.status === "fulfilled")
|
||||
.map((c) => c.value)
|
||||
.filter((c) => c !== null);
|
||||
const assetContracts = contracts.filter((c) => c.deploymentType === "asset");
|
||||
const otherContracts = contracts.filter((c) => c.deploymentType !== "asset");
|
||||
|
||||
const mainnetContracts: ProjectContract[] = [];
|
||||
const testnetContracts: ProjectContract[] = [];
|
||||
// "asset" deployment type contract is assumed to be a "token" type contract already
|
||||
// for others, check if its a "token" type contract (isERC20, isERC721 or isERC1155)
|
||||
|
||||
for (const contract of contracts) {
|
||||
const chain = chains.find(
|
||||
(chain) => contract.chainId === chain.chainId.toString(),
|
||||
const limit = pLimit(20);
|
||||
|
||||
const othersContractWithIsTokenInfo: ProjectContractWithIsToken[] =
|
||||
await Promise.all(
|
||||
otherContracts.map(async (c) => {
|
||||
return limit(async () => {
|
||||
try {
|
||||
const contract = getContract({
|
||||
address: c.contractAddress,
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
chain: defineChain(Number(c.chainId)),
|
||||
client: serverThirdwebClient,
|
||||
});
|
||||
|
||||
const functionSelectors = await resolveFunctionSelectors(contract);
|
||||
const ercs = supportedERCs(functionSelectors);
|
||||
if (ercs.isERC20 || ercs.isERC721 || ercs.isERC1155) {
|
||||
return {
|
||||
...c,
|
||||
isToken: true,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...c,
|
||||
isToken: false,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
...c,
|
||||
isToken: false,
|
||||
};
|
||||
}
|
||||
});
|
||||
}),
|
||||
);
|
||||
if (chain && chain.status !== "deprecated") {
|
||||
if (chain.testnet) {
|
||||
testnetContracts.push(contract);
|
||||
} else {
|
||||
mainnetContracts.push(contract);
|
||||
}
|
||||
|
||||
for (const c of othersContractWithIsTokenInfo) {
|
||||
if (c.deploymentType === "asset") {
|
||||
assetContracts.push(c);
|
||||
} else {
|
||||
otherContracts.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
mainnetContracts.sort((a, b) => Number(a.chainId) - Number(b.chainId));
|
||||
|
||||
if (params.onlyMainnet) {
|
||||
return mainnetContracts;
|
||||
}
|
||||
|
||||
testnetContracts.sort((a, b) => Number(a.chainId) - Number(b.chainId));
|
||||
|
||||
return [...mainnetContracts, ...testnetContracts];
|
||||
return [
|
||||
...assetContracts.map(
|
||||
(c) => ({ ...c, isToken: true }) as ProjectContractWithIsToken,
|
||||
),
|
||||
...othersContractWithIsTokenInfo,
|
||||
];
|
||||
}
|
||||
|
||||
export async function getFilteredProjectContracts(
|
||||
params: GetFilteredProjectContractsParams & {
|
||||
type: "token-contracts" | "non-token-contracts";
|
||||
},
|
||||
) {
|
||||
const cached_getProjectContractsWithIsTokenInfo = unstable_cache(
|
||||
getProjectContractsWithIsTokenInfo,
|
||||
["get-project-contracts-with-is-token-info"],
|
||||
{
|
||||
revalidate: 3600, // 1 hour
|
||||
tags: [projectContractsCacheTag(params)],
|
||||
},
|
||||
);
|
||||
|
||||
const contracts = await cached_getProjectContractsWithIsTokenInfo(params);
|
||||
|
||||
if (params.type === "token-contracts") {
|
||||
return contracts.filter((c) => c.isToken);
|
||||
} else {
|
||||
return contracts.filter((c) => !c.isToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,8 @@ export type AuthOption =
|
||||
| "steam"
|
||||
| "apple"
|
||||
| "coinbase"
|
||||
| "line";
|
||||
| "line"
|
||||
| "epic";
|
||||
|
||||
export type Ecosystem = {
|
||||
name: string;
|
||||
|
||||
@@ -82,7 +82,7 @@ const CHAIN_PLAN_TO_INFRA = {
|
||||
],
|
||||
};
|
||||
|
||||
export async function getChainSubscriptions(slug: string) {
|
||||
async function getChainSubscriptions(slug: string) {
|
||||
const allSubscriptions = await getTeamSubscriptions(slug);
|
||||
if (!allSubscriptions) {
|
||||
return null;
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
"use server";
|
||||
import type { Address } from "thirdweb";
|
||||
import { getAuthToken } from "@/api/auth-token";
|
||||
import { NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST } from "@/constants/public-envs";
|
||||
|
||||
const UB_BASE_URL = NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST;
|
||||
|
||||
type Webhook = {
|
||||
export type Webhook = {
|
||||
url: string;
|
||||
label: string;
|
||||
active: boolean;
|
||||
createdAt: string;
|
||||
id: string;
|
||||
secret: string;
|
||||
version?: number; // TODO (UB) make this mandatory after migration
|
||||
};
|
||||
|
||||
export async function getWebhooks(props: { clientId: string; teamId: string }) {
|
||||
const authToken = await getAuthToken();
|
||||
export async function getWebhooks(params: {
|
||||
clientId: string;
|
||||
teamId: string;
|
||||
authToken: string;
|
||||
}) {
|
||||
const res = await fetch(`${UB_BASE_URL}/v1/developer/webhooks`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
Authorization: `Bearer ${params.authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": props.clientId,
|
||||
"x-team-id": props.teamId,
|
||||
"x-client-id": params.clientId,
|
||||
"x-team-id": params.teamId,
|
||||
},
|
||||
method: "GET",
|
||||
});
|
||||
@@ -36,28 +36,55 @@ export async function getWebhooks(props: { clientId: string; teamId: string }) {
|
||||
return json.data as Array<Webhook>;
|
||||
}
|
||||
|
||||
export async function createWebhook(props: {
|
||||
export async function getWebhookById(params: {
|
||||
clientId: string;
|
||||
teamId: string;
|
||||
authToken: string;
|
||||
webhookId: string;
|
||||
}) {
|
||||
const res = await fetch(
|
||||
`${UB_BASE_URL}/v1/developer/webhooks/${encodeURIComponent(params.webhookId)}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${params.authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": params.clientId,
|
||||
"x-team-id": params.teamId,
|
||||
},
|
||||
method: "GET",
|
||||
},
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(text);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return json.data as Webhook;
|
||||
}
|
||||
|
||||
export async function createWebhook(params: {
|
||||
clientId: string;
|
||||
teamId: string;
|
||||
version?: number;
|
||||
url: string;
|
||||
label: string;
|
||||
secret?: string;
|
||||
authToken: string;
|
||||
}) {
|
||||
const authToken = await getAuthToken();
|
||||
|
||||
const res = await fetch(`${UB_BASE_URL}/v1/developer/webhooks`, {
|
||||
body: JSON.stringify({
|
||||
label: props.label,
|
||||
secret: props.secret,
|
||||
url: props.url,
|
||||
version: props.version,
|
||||
label: params.label,
|
||||
secret: params.secret,
|
||||
url: params.url,
|
||||
version: params.version,
|
||||
}),
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
Authorization: `Bearer ${params.authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": props.clientId,
|
||||
"x-team-id": props.teamId,
|
||||
"x-client-id": params.clientId,
|
||||
"x-team-id": params.teamId,
|
||||
},
|
||||
method: "POST",
|
||||
});
|
||||
@@ -67,23 +94,23 @@ export async function createWebhook(props: {
|
||||
throw new Error(text);
|
||||
}
|
||||
|
||||
return;
|
||||
return (await res.json()) as Webhook;
|
||||
}
|
||||
|
||||
export async function deleteWebhook(props: {
|
||||
export async function deleteWebhook(params: {
|
||||
clientId: string;
|
||||
teamId: string;
|
||||
webhookId: string;
|
||||
authToken: string;
|
||||
}) {
|
||||
const authToken = await getAuthToken();
|
||||
const res = await fetch(
|
||||
`${UB_BASE_URL}/v1/developer/webhooks/${props.webhookId}`,
|
||||
`${UB_BASE_URL}/v1/developer/webhooks/${encodeURIComponent(params.webhookId)}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
Authorization: `Bearer ${params.authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": props.clientId,
|
||||
"x-team-id": props.teamId,
|
||||
"x-client-id": params.clientId,
|
||||
"x-team-id": params.teamId,
|
||||
},
|
||||
method: "DELETE",
|
||||
},
|
||||
@@ -94,7 +121,151 @@ export async function deleteWebhook(props: {
|
||||
throw new Error(text);
|
||||
}
|
||||
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function updateWebhook(params: {
|
||||
clientId: string;
|
||||
teamId: string;
|
||||
webhookId: string;
|
||||
authToken: string;
|
||||
body: {
|
||||
version?: number;
|
||||
url: string;
|
||||
label: string;
|
||||
};
|
||||
}) {
|
||||
const res = await fetch(
|
||||
`${UB_BASE_URL}/v1/developer/webhooks/${encodeURIComponent(params.webhookId)}`,
|
||||
{
|
||||
method: "PUT",
|
||||
body: JSON.stringify(params.body),
|
||||
headers: {
|
||||
Authorization: `Bearer ${params.authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": params.clientId,
|
||||
"x-team-id": params.teamId,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(text);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
return json.data as Webhook;
|
||||
}
|
||||
|
||||
export type WebhookSend = {
|
||||
id: string;
|
||||
webhookId: string;
|
||||
webhookUrl: string;
|
||||
createdAt: string;
|
||||
onrampId: string | null;
|
||||
paymentId: string | null;
|
||||
response: string | null;
|
||||
responseStatus: number;
|
||||
status: "PENDING" | "COMPLETED" | "FAILED";
|
||||
success: boolean;
|
||||
transactionId: string | null;
|
||||
body: unknown;
|
||||
};
|
||||
|
||||
type WebhookSendsResponse = {
|
||||
data: WebhookSend[];
|
||||
pagination: {
|
||||
limit: number;
|
||||
offset: number;
|
||||
total: number;
|
||||
};
|
||||
};
|
||||
|
||||
export async function getWebhookSends(options: {
|
||||
authToken: string;
|
||||
projectClientId: string;
|
||||
teamId: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
webhookId: string;
|
||||
success?: boolean;
|
||||
}): Promise<WebhookSendsResponse> {
|
||||
const { limit, offset, success, webhookId, authToken } = options;
|
||||
|
||||
const url = new URL(
|
||||
`${NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST}/v1/developer/webhook-sends`,
|
||||
);
|
||||
|
||||
url.searchParams.set("webhookId", webhookId);
|
||||
|
||||
if (limit !== undefined) {
|
||||
url.searchParams.set("limit", limit.toString());
|
||||
}
|
||||
if (offset !== undefined) {
|
||||
url.searchParams.set("offset", offset.toString());
|
||||
}
|
||||
if (success !== undefined) {
|
||||
url.searchParams.set("success", success.toString());
|
||||
}
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"x-client-id": options.projectClientId,
|
||||
"x-team-id": options.teamId,
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorJson = await response.json();
|
||||
throw new Error(errorJson.message);
|
||||
}
|
||||
|
||||
return (await response.json()) as WebhookSendsResponse;
|
||||
}
|
||||
|
||||
export async function resendWebhook(
|
||||
params: {
|
||||
authToken: string;
|
||||
projectClientId: string;
|
||||
teamId: string;
|
||||
} & (
|
||||
| {
|
||||
paymentId: string;
|
||||
}
|
||||
| {
|
||||
onrampId: string;
|
||||
}
|
||||
),
|
||||
) {
|
||||
const url = new URL(
|
||||
`${NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST}/v1/developer/webhooks/retry`,
|
||||
);
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
method: "POST",
|
||||
body: JSON.stringify(
|
||||
"paymentId" in params
|
||||
? { paymentId: params.paymentId }
|
||||
: { onrampId: params.onrampId },
|
||||
),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": params.projectClientId,
|
||||
"x-team-id": params.teamId,
|
||||
Authorization: `Bearer ${params.authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorJson = await response.json();
|
||||
throw new Error(errorJson.message);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
type PaymentLink = {
|
||||
@@ -116,17 +287,17 @@ type PaymentLink = {
|
||||
amount: bigint;
|
||||
};
|
||||
|
||||
export async function getPaymentLinks(props: {
|
||||
export async function getPaymentLinks(params: {
|
||||
clientId: string;
|
||||
teamId: string;
|
||||
authToken: string;
|
||||
}): Promise<Array<PaymentLink>> {
|
||||
const authToken = await getAuthToken();
|
||||
const res = await fetch(`${UB_BASE_URL}/v1/developer/links`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
Authorization: `Bearer ${params.authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": props.clientId,
|
||||
"x-team-id": props.teamId,
|
||||
"x-client-id": params.clientId,
|
||||
"x-team-id": params.teamId,
|
||||
},
|
||||
method: "GET",
|
||||
});
|
||||
@@ -159,7 +330,7 @@ export async function getPaymentLinks(props: {
|
||||
}));
|
||||
}
|
||||
|
||||
export async function createPaymentLink(props: {
|
||||
export async function createPaymentLink(params: {
|
||||
clientId: string;
|
||||
teamId: string;
|
||||
title: string;
|
||||
@@ -171,26 +342,25 @@ export async function createPaymentLink(props: {
|
||||
amount: bigint;
|
||||
purchaseData?: unknown;
|
||||
};
|
||||
authToken: string;
|
||||
}) {
|
||||
const authToken = await getAuthToken();
|
||||
|
||||
const res = await fetch(`${UB_BASE_URL}/v1/developer/links`, {
|
||||
body: JSON.stringify({
|
||||
title: props.title,
|
||||
imageUrl: props.imageUrl,
|
||||
title: params.title,
|
||||
imageUrl: params.imageUrl,
|
||||
intent: {
|
||||
destinationChainId: props.intent.destinationChainId,
|
||||
destinationTokenAddress: props.intent.destinationTokenAddress,
|
||||
receiver: props.intent.receiver,
|
||||
amount: props.intent.amount.toString(),
|
||||
purchaseData: props.intent.purchaseData,
|
||||
destinationChainId: params.intent.destinationChainId,
|
||||
destinationTokenAddress: params.intent.destinationTokenAddress,
|
||||
receiver: params.intent.receiver,
|
||||
amount: params.intent.amount.toString(),
|
||||
purchaseData: params.intent.purchaseData,
|
||||
},
|
||||
}),
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
Authorization: `Bearer ${params.authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": props.clientId,
|
||||
"x-team-id": props.teamId,
|
||||
"x-client-id": params.clientId,
|
||||
"x-team-id": params.teamId,
|
||||
},
|
||||
method: "POST",
|
||||
});
|
||||
@@ -207,20 +377,20 @@ export async function createPaymentLink(props: {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function deletePaymentLink(props: {
|
||||
export async function deletePaymentLink(params: {
|
||||
clientId: string;
|
||||
teamId: string;
|
||||
paymentLinkId: string;
|
||||
authToken: string;
|
||||
}) {
|
||||
const authToken = await getAuthToken();
|
||||
const res = await fetch(
|
||||
`${UB_BASE_URL}/v1/developer/links/${props.paymentLinkId}`,
|
||||
`${UB_BASE_URL}/v1/developer/links/${encodeURIComponent(params.paymentLinkId)}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
Authorization: `Bearer ${params.authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": props.clientId,
|
||||
"x-team-id": props.teamId,
|
||||
"x-client-id": params.clientId,
|
||||
"x-team-id": params.teamId,
|
||||
},
|
||||
method: "DELETE",
|
||||
},
|
||||
@@ -231,7 +401,7 @@ export async function deletePaymentLink(props: {
|
||||
throw new Error(text);
|
||||
}
|
||||
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
export type Fee = {
|
||||
@@ -241,14 +411,17 @@ export type Fee = {
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export async function getFees(props: { clientId: string; teamId: string }) {
|
||||
const authToken = await getAuthToken();
|
||||
export async function getFees(params: {
|
||||
clientId: string;
|
||||
teamId: string;
|
||||
authToken: string;
|
||||
}) {
|
||||
const res = await fetch(`${UB_BASE_URL}/v1/developer/fees`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
Authorization: `Bearer ${params.authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": props.clientId,
|
||||
"x-team-id": props.teamId,
|
||||
"x-client-id": params.clientId,
|
||||
"x-team-id": params.teamId,
|
||||
},
|
||||
method: "GET",
|
||||
});
|
||||
@@ -262,23 +435,23 @@ export async function getFees(props: { clientId: string; teamId: string }) {
|
||||
return json.data as Fee;
|
||||
}
|
||||
|
||||
export async function updateFee(props: {
|
||||
export async function updateFee(params: {
|
||||
clientId: string;
|
||||
teamId: string;
|
||||
feeRecipient: string;
|
||||
feeBps: number;
|
||||
authToken: string;
|
||||
}) {
|
||||
const authToken = await getAuthToken();
|
||||
const res = await fetch(`${UB_BASE_URL}/v1/developer/fees`, {
|
||||
body: JSON.stringify({
|
||||
feeBps: props.feeBps,
|
||||
feeRecipient: props.feeRecipient,
|
||||
feeBps: params.feeBps,
|
||||
feeRecipient: params.feeRecipient,
|
||||
}),
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
Authorization: `Bearer ${params.authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": props.clientId,
|
||||
"x-team-id": props.teamId,
|
||||
"x-client-id": params.clientId,
|
||||
"x-team-id": params.teamId,
|
||||
},
|
||||
method: "PUT",
|
||||
});
|
||||
@@ -288,7 +461,7 @@ export async function updateFee(props: {
|
||||
throw new Error(text);
|
||||
}
|
||||
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
export type PaymentsResponse = {
|
||||
@@ -345,36 +518,34 @@ export type Payment = {
|
||||
|
||||
export type BridgePayment = Extract<Payment, { type: BridgePaymentType }>;
|
||||
|
||||
export async function getPayments(props: {
|
||||
export async function getPayments(params: {
|
||||
clientId: string;
|
||||
teamId: string;
|
||||
paymentLinkId?: string;
|
||||
limit?: number;
|
||||
authToken: string;
|
||||
offset?: number;
|
||||
}) {
|
||||
const authToken = await getAuthToken();
|
||||
|
||||
// Build URL with query parameters if provided
|
||||
const url = new URL(`${UB_BASE_URL}/v1/developer/payments`);
|
||||
|
||||
if (props.limit) {
|
||||
url.searchParams.append("limit", props.limit.toString());
|
||||
if (params.limit) {
|
||||
url.searchParams.append("limit", params.limit.toString());
|
||||
}
|
||||
|
||||
if (props.offset) {
|
||||
url.searchParams.append("offset", props.offset.toString());
|
||||
if (params.offset) {
|
||||
url.searchParams.append("offset", params.offset.toString());
|
||||
}
|
||||
|
||||
if (props.paymentLinkId) {
|
||||
url.searchParams.append("paymentLinkId", props.paymentLinkId);
|
||||
if (params.paymentLinkId) {
|
||||
url.searchParams.append("paymentLinkId", params.paymentLinkId);
|
||||
}
|
||||
|
||||
const res = await fetch(url.toString(), {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
Authorization: `Bearer ${params.authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": props.clientId,
|
||||
"x-team-id": props.teamId,
|
||||
"x-client-id": params.clientId,
|
||||
"x-team-id": params.teamId,
|
||||
},
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ export async function getUniversalBridgeTokens(props: {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": NEXT_PUBLIC_DASHBOARD_CLIENT_ID,
|
||||
} as Record<string, string>,
|
||||
},
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ export async function addUniversalBridgeTokenRoute(props: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": props.project.publishableKey,
|
||||
} as Record<string, string>,
|
||||
},
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { Project } from "@/api/project/projects";
|
||||
|
||||
type X402Fee = {
|
||||
feeRecipient: string;
|
||||
feeBps: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract x402 fee configuration from project's engineCloud service
|
||||
*/
|
||||
export function getX402Fees(project: Project): X402Fee {
|
||||
const engineCloudService = project.services.find(
|
||||
(service) => service.name === "engineCloud",
|
||||
);
|
||||
|
||||
if (!engineCloudService) {
|
||||
return {
|
||||
feeRecipient: "",
|
||||
feeBps: 0,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
feeRecipient: engineCloudService.x402FeeRecipient || "",
|
||||
feeBps: engineCloudService.x402FeeBPS || 0,
|
||||
};
|
||||
}
|
||||
@@ -1,192 +0,0 @@
|
||||
"use client";
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
import { Mesh, Program, Renderer, Triangle, Vec2 } from "ogl";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
const vertex = `
|
||||
attribute vec2 position;
|
||||
void main(){gl_Position=vec4(position,0.0,1.0);}
|
||||
`;
|
||||
|
||||
const fragment = `
|
||||
#ifdef GL_ES
|
||||
precision lowp float;
|
||||
#endif
|
||||
uniform vec2 uResolution;
|
||||
uniform float uTime;
|
||||
uniform float uHueShift;
|
||||
uniform float uNoise;
|
||||
uniform float uScan;
|
||||
uniform float uScanFreq;
|
||||
uniform float uWarp;
|
||||
uniform vec3 uBackgroundColor;
|
||||
uniform float uBackgroundOpacity;
|
||||
uniform float uPatternLightness;
|
||||
#define iTime uTime
|
||||
#define iResolution uResolution
|
||||
|
||||
vec4 buf[8];
|
||||
float rand(vec2 c){return fract(sin(dot(c,vec2(12.9898,78.233)))*43758.5453);}
|
||||
|
||||
mat3 rgb2yiq=mat3(0.299,0.587,0.114,0.596,-0.274,-0.322,0.211,-0.523,0.312);
|
||||
mat3 yiq2rgb=mat3(1.0,0.956,0.621,1.0,-0.272,-0.647,1.0,-1.106,1.703);
|
||||
|
||||
vec3 hueShiftRGB(vec3 col,float deg){
|
||||
vec3 yiq=rgb2yiq*col;
|
||||
float rad=radians(deg);
|
||||
float cosh=cos(rad),sinh=sin(rad);
|
||||
vec3 yiqShift=vec3(yiq.x,yiq.y*cosh-yiq.z*sinh,yiq.y*sinh+yiq.z*cosh);
|
||||
return clamp(yiq2rgb*yiqShift,0.0,1.0);
|
||||
}
|
||||
|
||||
vec4 sigmoid(vec4 x){return 1./(1.+exp(-x));}
|
||||
|
||||
vec4 cppn_fn(vec2 coordinate,float in0,float in1,float in2){
|
||||
buf[6]=vec4(coordinate.x,coordinate.y,0.3948333106474662+in0,0.36+in1);
|
||||
buf[7]=vec4(0.14+in2,sqrt(coordinate.x*coordinate.x+coordinate.y*coordinate.y),0.,0.);
|
||||
buf[0]=mat4(vec4(6.5404263,-3.6126034,0.7590882,-1.13613),vec4(2.4582713,3.1660357,1.2219609,0.06276096),vec4(-5.478085,-6.159632,1.8701609,-4.7742867),vec4(6.039214,-5.542865,-0.90925294,3.251348))*buf[6]+mat4(vec4(0.8473259,-5.722911,3.975766,1.6522468),vec4(-0.24321538,0.5839259,-1.7661959,-5.350116),vec4(0.,0.,0.,0.),vec4(0.,0.,0.,0.))*buf[7]+vec4(0.21808943,1.1243913,-1.7969975,5.0294676);
|
||||
buf[1]=mat4(vec4(-3.3522482,-6.0612736,0.55641043,-4.4719114),vec4(0.8631464,1.7432913,5.643898,1.6106541),vec4(2.4941394,-3.5012043,1.7184316,6.357333),vec4(3.310376,8.209261,1.1355612,-1.165539))*buf[6]+mat4(vec4(5.24046,-13.034365,0.009859298,15.870829),vec4(2.987511,3.129433,-0.89023495,-1.6822904),vec4(0.,0.,0.,0.),vec4(0.,0.,0.,0.))*buf[7]+vec4(-5.9457836,-6.573602,-0.8812491,1.5436668);
|
||||
buf[0]=sigmoid(buf[0]);buf[1]=sigmoid(buf[1]);
|
||||
buf[2]=mat4(vec4(-15.219568,8.095543,-2.429353,-1.9381982),vec4(-5.951362,4.3115187,2.6393783,1.274315),vec4(-7.3145227,6.7297835,5.2473326,5.9411426),vec4(5.0796127,8.979051,-1.7278991,-1.158976))*buf[6]+mat4(vec4(-11.967154,-11.608155,6.1486754,11.237008),vec4(2.124141,-6.263192,-1.7050359,-0.7021966),vec4(0.,0.,0.,0.),vec4(0.,0.,0.,0.))*buf[7]+vec4(-4.17164,-3.2281182,-4.576417,-3.6401186);
|
||||
buf[3]=mat4(vec4(3.1832156,-13.738922,1.879223,3.233465),vec4(0.64300746,12.768129,1.9141049,0.50990224),vec4(-0.049295485,4.4807224,1.4733979,1.801449),vec4(5.0039253,13.000481,3.3991797,-4.5561905))*buf[6]+mat4(vec4(-0.1285731,7.720628,-3.1425676,4.742367),vec4(0.6393625,3.714393,-0.8108378,-0.39174938),vec4(0.,0.,0.,0.),vec4(0.,0.,0.,0.))*buf[7]+vec4(-1.1811101,-21.621881,0.7851888,1.2329718);
|
||||
buf[2]=sigmoid(buf[2]);buf[3]=sigmoid(buf[3]);
|
||||
buf[4]=mat4(vec4(5.214916,-7.183024,2.7228765,2.6592617),vec4(-5.601878,-25.3591,4.067988,0.4602802),vec4(-10.57759,24.286327,21.102104,37.546658),vec4(4.3024497,-1.9625226,2.3458803,-1.372816))*buf[0]+mat4(vec4(-17.6526,-10.507558,2.2587414,12.462782),vec4(6.265566,-502.75443,-12.642513,0.9112289),vec4(-10.983244,20.741234,-9.701768,-0.7635988),vec4(5.383626,1.4819539,-4.1911616,-4.8444734))*buf[1]+mat4(vec4(12.785233,-16.345072,-0.39901125,1.7955981),vec4(-30.48365,-1.8345358,1.4542528,-1.1118771),vec4(19.872723,-7.337935,-42.941723,-98.52709),vec4(8.337645,-2.7312303,-2.2927687,-36.142323))*buf[2]+mat4(vec4(-16.298317,3.5471997,-0.44300047,-9.444417),vec4(57.5077,-35.609753,16.163465,-4.1534753),vec4(-0.07470326,-3.8656476,-7.0901804,3.1523974),vec4(-12.559385,-7.077619,1.490437,-0.8211543))*buf[3]+vec4(-7.67914,15.927437,1.3207729,-1.6686112);
|
||||
buf[5]=mat4(vec4(-1.4109162,-0.372762,-3.770383,-21.367174),vec4(-6.2103205,-9.35908,0.92529047,8.82561),vec4(11.460242,-22.348068,13.625772,-18.693201),vec4(-0.3429052,-3.9905605,-2.4626114,-0.45033523))*buf[0]+mat4(vec4(7.3481627,-4.3661838,-6.3037653,-3.868115),vec4(1.5462853,6.5488915,1.9701879,-0.58291394),vec4(6.5858274,-2.2180402,3.7127688,-1.3730392),vec4(-5.7973905,10.134961,-2.3395722,-5.965605))*buf[1]+mat4(vec4(-2.5132585,-6.6685553,-1.4029363,-0.16285264),vec4(-0.37908727,0.53738135,4.389061,-1.3024765),vec4(-0.70647055,2.0111287,-5.1659346,-3.728635),vec4(-13.562562,10.487719,-0.9173751,-2.6487076))*buf[2]+mat4(vec4(-8.645013,6.5546675,-6.3944063,-5.5933375),vec4(-0.57783127,-1.077275,36.91025,5.736769),vec4(14.283112,3.7146652,7.1452246,-4.5958776),vec4(2.7192075,3.6021907,-4.366337,-2.3653464))*buf[3]+vec4(-5.9000807,-4.329569,1.2427121,8.59503);
|
||||
buf[4]=sigmoid(buf[4]);buf[5]=sigmoid(buf[5]);
|
||||
buf[6]=mat4(vec4(-1.61102,0.7970257,1.4675229,0.20917463),vec4(-28.793737,-7.1390953,1.5025433,4.656581),vec4(-10.94861,39.66238,0.74318546,-10.095605),vec4(-0.7229728,-1.5483948,0.7301322,2.1687684))*buf[0]+mat4(vec4(3.2547753,21.489103,-1.0194173,-3.3100595),vec4(-3.7316632,-3.3792162,-7.223193,-0.23685838),vec4(13.1804495,0.7916005,5.338587,5.687114),vec4(-4.167605,-17.798311,-6.815736,-1.6451967))*buf[1]+mat4(vec4(0.604885,-7.800309,-7.213122,-2.741014),vec4(-3.522382,-0.12359311,-0.5258442,0.43852118),vec4(9.6752825,-22.853785,2.062431,0.099892326),vec4(-4.3196306,-17.730087,2.5184598,5.30267))*buf[2]+mat4(vec4(-6.545563,-15.790176,-6.0438633,-5.415399),vec4(-43.591583,28.551912,-16.00161,18.84728),vec4(4.212382,8.394307,3.0958717,8.657522),vec4(-5.0237565,-4.450633,-4.4768,-5.5010443))*buf[3]+mat4(vec4(1.6985557,-67.05806,6.897715,1.9004834),vec4(1.8680354,2.3915145,2.5231109,4.081538),vec4(11.158006,1.7294737,2.0738268,7.386411),vec4(-4.256034,-306.24686,8.258898,-17.132736))*buf[4]+mat4(vec4(1.6889864,-4.5852966,3.8534803,-6.3482175),vec4(1.3543309,-1.2640043,9.932754,2.9079645),vec4(-5.2770967,0.07150358,-0.13962056,3.3269649),vec4(28.34703,-4.918278,6.1044083,4.085355))*buf[5]+vec4(6.6818056,12.522166,-3.7075126,-4.104386);
|
||||
buf[7]=mat4(vec4(-8.265602,-4.7027016,5.098234,0.7509808),vec4(8.6507845,-17.15949,16.51939,-8.884479),vec4(-4.036479,-2.3946867,-2.6055532,-1.9866527),vec4(-2.2167742,-1.8135649,-5.9759874,4.8846445))*buf[0]+mat4(vec4(6.7790847,3.5076547,-2.8191125,-2.7028968),vec4(-5.743024,-0.27844876,1.4958696,-5.0517144),vec4(13.122226,15.735168,-2.9397483,-4.101023),vec4(-14.375265,-5.030483,-6.2599335,2.9848232))*buf[1]+mat4(vec4(4.0950394,-0.94011575,-5.674733,4.755022),vec4(4.3809423,4.8310084,1.7425908,-3.437416),vec4(2.117492,0.16342592,-104.56341,16.949184),vec4(-5.22543,-2.994248,3.8350096,-1.9364246))*buf[2]+mat4(vec4(-5.900337,1.7946124,-13.604192,-3.8060522),vec4(6.6583457,31.911177,25.164474,91.81147),vec4(11.840538,4.1503043,-0.7314397,6.768467),vec4(-6.3967767,4.034772,6.1714606,-0.32874924))*buf[3]+mat4(vec4(3.4992442,-196.91893,-8.923708,2.8142626),vec4(3.4806502,-3.1846354,5.1725626,5.1804223),vec4(-2.4009497,15.585794,1.2863957,2.0252278),vec4(-71.25271,-62.441242,-8.138444,0.50670296))*buf[4]+mat4(vec4(-12.291733,-11.176166,-7.3474145,4.390294),vec4(10.805477,5.6337385,-0.9385842,-4.7348723),vec4(-12.869276,-7.039391,5.3029537,7.5436664),vec4(1.4593618,8.91898,3.5101583,5.840625))*buf[5]+vec4(2.2415268,-6.705987,-0.98861027,-2.117676);
|
||||
buf[6]=sigmoid(buf[6]);buf[7]=sigmoid(buf[7]);
|
||||
buf[0]=mat4(vec4(1.6794263,1.3817469,2.9625452,0.),vec4(-1.8834411,-1.4806935,-3.5924516,0.),vec4(-1.3279216,-1.0918057,-2.3124623,0.),vec4(0.2662234,0.23235129,0.44178495,0.))*buf[0]+mat4(vec4(-0.6299101,-0.5945583,-0.9125601,0.),vec4(0.17828953,0.18300213,0.18182953,0.),vec4(-2.96544,-2.5819945,-4.9001055,0.),vec4(1.4195864,1.1868085,2.5176322,0.))*buf[1]+mat4(vec4(-1.2584374,-1.0552157,-2.1688404,0.),vec4(-0.7200217,-0.52666044,-1.438251,0.),vec4(0.15345335,0.15196142,0.272854,0.),vec4(0.945728,0.8861938,1.2766753,0.))*buf[2]+mat4(vec4(-2.4218085,-1.968602,-4.35166,0.),vec4(-22.683098,-18.0544,-41.954372,0.),vec4(0.63792,0.5470648,1.1078634,0.),vec4(-1.5489894,-1.3075932,-2.6444845,0.))*buf[3]+mat4(vec4(-0.49252132,-0.39877754,-0.91366625,0.),vec4(0.95609266,0.7923952,1.640221,0.),vec4(0.30616966,0.15693925,0.8639857,0.),vec4(1.1825981,0.94504964,2.176963,0.))*buf[4]+mat4(vec4(0.35446745,0.3293795,0.59547555,0.),vec4(-0.58784515,-0.48177817,-1.0614829,0.),vec4(2.5271258,1.9991658,4.6846647,0.),vec4(0.13042648,0.08864098,0.30187556,0.))*buf[5]+mat4(vec4(-1.7718065,-1.4033192,-3.3355875,0.),vec4(3.1664357,2.638297,5.378702,0.),vec4(-3.1724713,-2.6107926,-5.549295,0.),vec4(-2.851368,-2.249092,-5.3013067,0.))*buf[6]+mat4(vec4(1.5203838,1.2212278,2.8404984,0.),vec4(1.5210563,1.2651345,2.683903,0.),vec4(2.9789467,2.4364579,5.2347264,0.),vec4(2.2270417,1.8825914,3.8028636,0.))*buf[7]+vec4(-1.5468478,-3.6171484,0.24762098,0.);
|
||||
buf[0]=sigmoid(buf[0]);
|
||||
return vec4(buf[0].x,buf[0].y,buf[0].z,1.);
|
||||
}
|
||||
|
||||
void mainImage(out vec4 fragColor,in vec2 fragCoord){
|
||||
vec2 uv=fragCoord/uResolution.xy*2.-1.;
|
||||
uv.y*=-1.;
|
||||
uv+=uWarp*vec2(sin(uv.y*6.283+uTime*0.5),cos(uv.x*6.283+uTime*0.5))*0.05;
|
||||
fragColor=cppn_fn(uv,0.1*sin(0.3*uTime),0.1*sin(0.69*uTime),0.1*sin(0.44*uTime));
|
||||
}
|
||||
|
||||
void main(){
|
||||
vec4 col;mainImage(col,gl_FragCoord.xy);
|
||||
col.rgb=hueShiftRGB(col.rgb,uHueShift);
|
||||
float scanline_val=sin(gl_FragCoord.y*uScanFreq)*0.5+0.5;
|
||||
col.rgb*=1.-(scanline_val*scanline_val)*uScan;
|
||||
col.rgb+=(rand(gl_FragCoord.xy+uTime)-0.5)*uNoise;
|
||||
|
||||
// Apply lightness control to pattern
|
||||
col.rgb = col.rgb * uPatternLightness;
|
||||
|
||||
// Use background color as base, then add pattern on top
|
||||
vec3 finalColor = uBackgroundColor + col.rgb * uBackgroundOpacity;
|
||||
|
||||
gl_FragColor=vec4(clamp(finalColor,0.0,1.0),1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
hueShift?: number;
|
||||
noiseIntensity?: number;
|
||||
scanlineIntensity?: number;
|
||||
speed?: number;
|
||||
scanlineFrequency?: number;
|
||||
warpAmount?: number;
|
||||
resolutionScale?: number;
|
||||
backgroundColor?: [number, number, number];
|
||||
backgroundOpacity?: number;
|
||||
patternLightness?: number;
|
||||
};
|
||||
|
||||
export function DarkVeil({
|
||||
hueShift = 0,
|
||||
noiseIntensity = 0,
|
||||
scanlineIntensity = 0,
|
||||
speed = 0.5,
|
||||
scanlineFrequency = 0,
|
||||
warpAmount = 0,
|
||||
resolutionScale = 1,
|
||||
backgroundColor = [0, 0, 0],
|
||||
backgroundOpacity = 1,
|
||||
patternLightness = 1.0,
|
||||
}: Props) {
|
||||
const ref = useRef<HTMLCanvasElement>(null);
|
||||
useEffect(() => {
|
||||
const canvas = ref.current as HTMLCanvasElement;
|
||||
const parent = canvas.parentElement as HTMLElement;
|
||||
|
||||
const renderer = new Renderer({
|
||||
dpr: Math.min(window.devicePixelRatio, 2),
|
||||
canvas,
|
||||
});
|
||||
|
||||
const gl = renderer.gl;
|
||||
const geometry = new Triangle(gl);
|
||||
|
||||
const program = new Program(gl, {
|
||||
vertex,
|
||||
fragment,
|
||||
uniforms: {
|
||||
uTime: { value: 0 },
|
||||
uResolution: { value: new Vec2() },
|
||||
uHueShift: { value: hueShift },
|
||||
uNoise: { value: noiseIntensity },
|
||||
uScan: { value: scanlineIntensity },
|
||||
uScanFreq: { value: scanlineFrequency },
|
||||
uWarp: { value: warpAmount },
|
||||
uBackgroundColor: { value: backgroundColor },
|
||||
uBackgroundOpacity: { value: backgroundOpacity },
|
||||
uPatternLightness: { value: patternLightness },
|
||||
},
|
||||
});
|
||||
|
||||
const mesh = new Mesh(gl, { geometry, program });
|
||||
|
||||
const resize = () => {
|
||||
const w = parent.clientWidth,
|
||||
h = parent.clientHeight;
|
||||
renderer.setSize(w * resolutionScale, h * resolutionScale);
|
||||
program.uniforms.uResolution.value.set(w, h);
|
||||
};
|
||||
|
||||
window.addEventListener("resize", resize);
|
||||
resize();
|
||||
|
||||
const start = performance.now();
|
||||
let frame = 0;
|
||||
|
||||
const loop = () => {
|
||||
program.uniforms.uTime.value =
|
||||
((performance.now() - start) / 1000) * speed;
|
||||
program.uniforms.uHueShift.value = hueShift;
|
||||
program.uniforms.uNoise.value = noiseIntensity;
|
||||
program.uniforms.uScan.value = scanlineIntensity;
|
||||
program.uniforms.uScanFreq.value = scanlineFrequency;
|
||||
program.uniforms.uWarp.value = warpAmount;
|
||||
program.uniforms.uBackgroundColor.value = backgroundColor;
|
||||
program.uniforms.uBackgroundOpacity.value = backgroundOpacity;
|
||||
program.uniforms.uPatternLightness.value = patternLightness;
|
||||
renderer.render({ scene: mesh });
|
||||
frame = requestAnimationFrame(loop);
|
||||
};
|
||||
|
||||
loop();
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(frame);
|
||||
window.removeEventListener("resize", resize);
|
||||
};
|
||||
}, [
|
||||
hueShift,
|
||||
noiseIntensity,
|
||||
scanlineIntensity,
|
||||
speed,
|
||||
scanlineFrequency,
|
||||
warpAmount,
|
||||
resolutionScale,
|
||||
backgroundColor,
|
||||
backgroundOpacity,
|
||||
patternLightness,
|
||||
]);
|
||||
return <canvas ref={ref} className="w-full h-full block bg-background" />;
|
||||
}
|
||||
@@ -95,7 +95,7 @@ function SkeletonBarChart(props: {
|
||||
fill={`url(#${fillAreaSkeletonId})`}
|
||||
radius={8}
|
||||
stroke="hsl(var(--muted-foreground))"
|
||||
type="natural"
|
||||
type="monotone"
|
||||
/>
|
||||
</AreaChart>
|
||||
)}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { Chain } from "thirdweb";
|
||||
import { defineChain } from "thirdweb";
|
||||
import { BuyWidget, SwapWidget } from "thirdweb/react";
|
||||
import type { Wallet } from "thirdweb/wallets";
|
||||
import {
|
||||
reportAssetBuyCancelled,
|
||||
reportAssetBuyFailed,
|
||||
@@ -25,17 +27,46 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
import { parseError } from "@/utils/errorParser";
|
||||
import { getSDKTheme } from "@/utils/sdk-component-theme";
|
||||
import { appMetadata } from "../../constants/connect";
|
||||
import { getConfiguredThirdwebClient } from "../../constants/thirdweb.server";
|
||||
|
||||
type PageType = "asset" | "bridge" | "chain";
|
||||
|
||||
export function BuyAndSwapEmbed(props: {
|
||||
chain: Chain;
|
||||
tokenAddress: string | undefined;
|
||||
buyAmount: string | undefined;
|
||||
export type BuyAndSwapEmbedProps = {
|
||||
buyTab:
|
||||
| {
|
||||
buyToken:
|
||||
| {
|
||||
tokenAddress: string;
|
||||
chainId: number;
|
||||
amount?: string;
|
||||
}
|
||||
| undefined;
|
||||
}
|
||||
| undefined;
|
||||
swapTab:
|
||||
| {
|
||||
sellToken:
|
||||
| {
|
||||
chainId: number;
|
||||
tokenAddress: string;
|
||||
amount?: string;
|
||||
}
|
||||
| undefined;
|
||||
buyToken:
|
||||
| {
|
||||
chainId: number;
|
||||
tokenAddress: string;
|
||||
amount?: string;
|
||||
}
|
||||
| undefined;
|
||||
}
|
||||
| undefined;
|
||||
pageType: PageType;
|
||||
isTestnet: boolean | undefined;
|
||||
}) {
|
||||
wallets?: Wallet[];
|
||||
};
|
||||
|
||||
export function BuyAndSwapEmbed(props: BuyAndSwapEmbedProps) {
|
||||
const { theme } = useTheme();
|
||||
const [tab, setTab] = useState<"buy" | "swap">("swap");
|
||||
const themeObj = getSDKTheme(theme === "light" ? "light" : "dark");
|
||||
@@ -84,24 +115,39 @@ export function BuyAndSwapEmbed(props: {
|
||||
|
||||
{tab === "buy" && (
|
||||
<BuyWidget
|
||||
amount={props.buyAmount || "1"}
|
||||
chain={props.chain}
|
||||
amount={props.buyTab?.buyToken?.amount || "1"}
|
||||
chain={
|
||||
props.buyTab?.buyToken?.chainId
|
||||
? defineChain(props.buyTab.buyToken.chainId)
|
||||
: undefined
|
||||
}
|
||||
tokenAddress={
|
||||
props.buyTab?.buyToken?.tokenAddress as `0x${string}` | undefined
|
||||
}
|
||||
className="!rounded-2xl !border-none"
|
||||
title=""
|
||||
client={client}
|
||||
connectOptions={{
|
||||
autoConnect: false,
|
||||
wallets: props.wallets,
|
||||
appMetadata: appMetadata,
|
||||
}}
|
||||
onError={(e, quote) => {
|
||||
const errorMessage = parseError(e);
|
||||
|
||||
const buyChainId =
|
||||
quote?.type === "buy"
|
||||
? quote.intent.destinationChainId
|
||||
: quote?.type === "onramp"
|
||||
? quote.intent.chainId
|
||||
: undefined;
|
||||
|
||||
if (!buyChainId) {
|
||||
return;
|
||||
}
|
||||
|
||||
reportTokenBuyFailed({
|
||||
buyTokenChainId:
|
||||
quote?.type === "buy"
|
||||
? quote.intent.destinationChainId
|
||||
: quote?.type === "onramp"
|
||||
? quote.intent.chainId
|
||||
: undefined,
|
||||
buyTokenChainId: buyChainId,
|
||||
buyTokenAddress:
|
||||
quote?.type === "buy"
|
||||
? quote.intent.destinationTokenAddress
|
||||
@@ -114,21 +160,27 @@ export function BuyAndSwapEmbed(props: {
|
||||
if (props.pageType === "asset") {
|
||||
reportAssetBuyFailed({
|
||||
assetType: "coin",
|
||||
chainId: props.chain.id,
|
||||
chainId: buyChainId,
|
||||
error: errorMessage,
|
||||
contractType: undefined,
|
||||
is_testnet: props.isTestnet,
|
||||
is_testnet: false,
|
||||
});
|
||||
}
|
||||
}}
|
||||
onCancel={(quote) => {
|
||||
const buyChainId =
|
||||
quote?.type === "buy"
|
||||
? quote.intent.destinationChainId
|
||||
: quote?.type === "onramp"
|
||||
? quote.intent.chainId
|
||||
: undefined;
|
||||
|
||||
if (!buyChainId) {
|
||||
return;
|
||||
}
|
||||
|
||||
reportTokenBuyCancelled({
|
||||
buyTokenChainId:
|
||||
quote?.type === "buy"
|
||||
? quote.intent.destinationChainId
|
||||
: quote?.type === "onramp"
|
||||
? quote.intent.chainId
|
||||
: undefined,
|
||||
buyTokenChainId: buyChainId,
|
||||
buyTokenAddress:
|
||||
quote?.type === "buy"
|
||||
? quote.intent.destinationTokenAddress
|
||||
@@ -141,24 +193,30 @@ export function BuyAndSwapEmbed(props: {
|
||||
if (props.pageType === "asset") {
|
||||
reportAssetBuyCancelled({
|
||||
assetType: "coin",
|
||||
chainId: props.chain.id,
|
||||
chainId: buyChainId,
|
||||
contractType: undefined,
|
||||
is_testnet: props.isTestnet,
|
||||
is_testnet: false,
|
||||
});
|
||||
}
|
||||
}}
|
||||
onSuccess={(quote) => {
|
||||
onSuccess={({ quote }) => {
|
||||
const buyChainId =
|
||||
quote?.type === "buy"
|
||||
? quote.intent.destinationChainId
|
||||
: quote?.type === "onramp"
|
||||
? quote.intent.chainId
|
||||
: undefined;
|
||||
|
||||
if (!buyChainId) {
|
||||
return;
|
||||
}
|
||||
|
||||
reportTokenBuySuccessful({
|
||||
buyTokenChainId:
|
||||
quote.type === "buy"
|
||||
? quote.intent.destinationChainId
|
||||
: quote.type === "onramp"
|
||||
? quote.intent.chainId
|
||||
: undefined,
|
||||
buyTokenChainId: buyChainId,
|
||||
buyTokenAddress:
|
||||
quote.type === "buy"
|
||||
quote?.type === "buy"
|
||||
? quote.intent.destinationTokenAddress
|
||||
: quote.type === "onramp"
|
||||
: quote?.type === "onramp"
|
||||
? quote.intent.tokenAddress
|
||||
: undefined,
|
||||
pageType: props.pageType,
|
||||
@@ -167,14 +225,13 @@ export function BuyAndSwapEmbed(props: {
|
||||
if (props.pageType === "asset") {
|
||||
reportAssetBuySuccessful({
|
||||
assetType: "coin",
|
||||
chainId: props.chain.id,
|
||||
chainId: buyChainId,
|
||||
contractType: undefined,
|
||||
is_testnet: props.isTestnet,
|
||||
is_testnet: false,
|
||||
});
|
||||
}
|
||||
}}
|
||||
theme={themeObj}
|
||||
tokenAddress={props.tokenAddress as `0x${string}`}
|
||||
paymentMethods={["card"]}
|
||||
/>
|
||||
)}
|
||||
@@ -184,18 +241,14 @@ export function BuyAndSwapEmbed(props: {
|
||||
client={client}
|
||||
theme={themeObj}
|
||||
className="!rounded-2xl !border-none"
|
||||
connectOptions={{
|
||||
autoConnect: false,
|
||||
wallets: props.wallets,
|
||||
appMetadata: appMetadata,
|
||||
}}
|
||||
prefill={{
|
||||
// buy this token by default
|
||||
buyToken: {
|
||||
chainId: props.chain.id,
|
||||
tokenAddress: props.tokenAddress,
|
||||
},
|
||||
// sell the native token by default (but if buytoken is a native token, don't set)
|
||||
sellToken: props.tokenAddress
|
||||
? {
|
||||
chainId: props.chain.id,
|
||||
}
|
||||
: undefined,
|
||||
buyToken: props.swapTab?.buyToken,
|
||||
sellToken: props.swapTab?.sellToken,
|
||||
}}
|
||||
onError={(error, quote) => {
|
||||
const errorMessage = parseError(error);
|
||||
@@ -208,7 +261,7 @@ export function BuyAndSwapEmbed(props: {
|
||||
pageType: props.pageType,
|
||||
});
|
||||
}}
|
||||
onSuccess={(quote) => {
|
||||
onSuccess={({ quote }) => {
|
||||
reportTokenSwapSuccessful({
|
||||
buyTokenChainId: quote.intent.destinationChainId,
|
||||
buyTokenAddress: quote.intent.destinationTokenAddress,
|
||||
@@ -240,9 +293,10 @@ function TabButton(props: {
|
||||
return (
|
||||
<Button
|
||||
onClick={props.onClick}
|
||||
size="sm"
|
||||
className={cn(
|
||||
"rounded-full text-muted-foreground px-5 text-base bg-accent",
|
||||
props.isActive && "text-foreground border-foreground",
|
||||
"rounded-full text-muted-foreground px-4 bg-accent",
|
||||
props.isActive && "text-foreground border-foreground/50",
|
||||
)}
|
||||
variant="outline"
|
||||
>
|
||||
|
||||
@@ -172,7 +172,7 @@ export function ThirdwebAreaChart<TConfig extends ChartConfig>(
|
||||
key={key}
|
||||
stackId={props.variant !== "stacked" ? undefined : "a"}
|
||||
stroke={`var(--color-${key})`}
|
||||
type="natural"
|
||||
type="monotone"
|
||||
/>
|
||||
),
|
||||
)}
|
||||
|
||||
@@ -5,25 +5,20 @@ import { Button } from "@/components/ui/button";
|
||||
import { DynamicHeight } from "@/components/ui/DynamicHeight";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function FaqSection(props: {
|
||||
export function FaqAccordion(props: {
|
||||
faqs: Array<{ title: string; description: string }>;
|
||||
}) {
|
||||
return (
|
||||
<section>
|
||||
<h2 className="text-2xl md:text-3xl font-semibold mb-4 tracking-tight">
|
||||
Frequently asked questions
|
||||
</h2>
|
||||
<div className="flex flex-col">
|
||||
{props.faqs.map((faq, faqIndex) => (
|
||||
<FaqItem
|
||||
key={faq.title}
|
||||
title={faq.title}
|
||||
description={faq.description}
|
||||
className={cn(faqIndex === props.faqs.length - 1 && "border-b-0")}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<div className="flex flex-col">
|
||||
{props.faqs.map((faq, faqIndex) => (
|
||||
<FaqItem
|
||||
key={faq.title}
|
||||
title={faq.title}
|
||||
description={faq.description}
|
||||
className={cn(faqIndex === props.faqs.length - 1 && "border-b-0")}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -38,11 +38,11 @@ type ShadcnSidebarBaseLink = {
|
||||
isActive?: (pathname: string) => boolean;
|
||||
};
|
||||
|
||||
type ShadcnSidebarLink =
|
||||
export type ShadcnSidebarLink =
|
||||
| ShadcnSidebarBaseLink
|
||||
| {
|
||||
group: string;
|
||||
links: ShadcnSidebarBaseLink[];
|
||||
links: ShadcnSidebarLink[];
|
||||
}
|
||||
| {
|
||||
separator: true;
|
||||
@@ -67,7 +67,7 @@ export function FullWidthSidebarLayout(props: {
|
||||
)}
|
||||
>
|
||||
{/* left - sidebar */}
|
||||
<Sidebar className="pt-2" collapsible="icon" side="left">
|
||||
<Sidebar collapsible="icon" side="left">
|
||||
<SidebarContent className="p-2">
|
||||
<RenderSidebarMenu links={contentSidebarLinks} />
|
||||
</SidebarContent>
|
||||
@@ -97,10 +97,7 @@ export function FullWidthSidebarLayout(props: {
|
||||
|
||||
function MobileSidebarTrigger(props: { links: ShadcnSidebarLink[] }) {
|
||||
const activeLink = useActiveShadcnSidebarLink(props.links);
|
||||
const parentSubNav = props.links.find(
|
||||
(link) =>
|
||||
"subMenu" in link && link.links.some((l) => l.href === activeLink?.href),
|
||||
);
|
||||
const parentSubNav = findParentSubmenu(props.links, activeLink?.href);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3 border-b px-4 py-4 lg:hidden">
|
||||
@@ -109,7 +106,7 @@ function MobileSidebarTrigger(props: { links: ShadcnSidebarLink[] }) {
|
||||
className="h-4 bg-muted-foreground/50"
|
||||
orientation="vertical"
|
||||
/>
|
||||
{parentSubNav && "subMenu" in parentSubNav && (
|
||||
{parentSubNav && (
|
||||
<>
|
||||
<span className="text-sm">{parentSubNav.subMenu.label}</span>
|
||||
<ChevronRightIcon className="size-4 text-muted-foreground/50 -mx-1.5" />
|
||||
@@ -131,24 +128,65 @@ function useActiveShadcnSidebarLink(links: ShadcnSidebarLink[]) {
|
||||
return pathname?.startsWith(link.href);
|
||||
}
|
||||
|
||||
for (const link of links) {
|
||||
if ("links" in link) {
|
||||
for (const subLink of link.links) {
|
||||
if (isActive(subLink)) {
|
||||
return subLink;
|
||||
function walk(
|
||||
navLinks: ShadcnSidebarLink[],
|
||||
): ShadcnSidebarBaseLink | undefined {
|
||||
for (const link of navLinks) {
|
||||
if ("subMenu" in link) {
|
||||
for (const subLink of link.links) {
|
||||
if (isActive(subLink)) {
|
||||
return subLink;
|
||||
}
|
||||
}
|
||||
} else if ("href" in link) {
|
||||
if (isActive(link)) {
|
||||
return link;
|
||||
}
|
||||
}
|
||||
} else if ("href" in link) {
|
||||
if (isActive(link)) {
|
||||
return link;
|
||||
|
||||
if ("links" in link && !("subMenu" in link)) {
|
||||
const nested = walk(link.links);
|
||||
if (nested) {
|
||||
return nested;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return walk(links);
|
||||
}, [links, pathname]);
|
||||
|
||||
return activeLink;
|
||||
}
|
||||
|
||||
function findParentSubmenu(
|
||||
links: ShadcnSidebarLink[],
|
||||
activeHref: string | undefined,
|
||||
): Extract<ShadcnSidebarLink, { subMenu: unknown }> | undefined {
|
||||
if (!activeHref) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
for (const link of links) {
|
||||
if ("subMenu" in link) {
|
||||
if (link.links.some((subLink) => subLink.href === activeHref)) {
|
||||
return link;
|
||||
}
|
||||
}
|
||||
|
||||
if ("links" in link && !("subMenu" in link)) {
|
||||
const nested = findParentSubmenu(link.links, activeHref);
|
||||
if (nested) {
|
||||
return nested;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function useIsSubnavActive(links: ShadcnSidebarBaseLink[]) {
|
||||
const pathname = usePathname();
|
||||
|
||||
|
||||
@@ -66,13 +66,16 @@ export type ProjectPageHeaderProps = {
|
||||
href: string;
|
||||
};
|
||||
|
||||
className?: string;
|
||||
// TODO: add task card component
|
||||
task?: never;
|
||||
};
|
||||
|
||||
export function ProjectPageHeader(props: ProjectPageHeaderProps) {
|
||||
return (
|
||||
<header className="container max-w-7xl py-6 relative">
|
||||
<header
|
||||
className={cn("container max-w-7xl py-6 relative", props.className)}
|
||||
>
|
||||
{/* top row */}
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
{/* left - icon */}
|
||||
|
||||
@@ -12,18 +12,20 @@ import {
|
||||
type ProjectPageProps = {
|
||||
header: ProjectPageHeaderProps;
|
||||
footer?: ProjectPageFooterProps;
|
||||
/**
|
||||
* @deprecated only for legacy pages where we still need internal tabs for the moment, currently:
|
||||
* - /webhooks
|
||||
*/
|
||||
tabs?: TabPathLink[];
|
||||
containerClassName?: string;
|
||||
};
|
||||
|
||||
export function ProjectPage(props: React.PropsWithChildren<ProjectPageProps>) {
|
||||
return (
|
||||
<div className="flex flex-col grow">
|
||||
<div className={cn(!props.tabs && "border-b")}>
|
||||
<ProjectPageHeader {...props.header} />
|
||||
<ProjectPageHeader
|
||||
{...props.header}
|
||||
className={cn(
|
||||
props.tabs && props.tabs.length > 0 ? "pt-8 pb-6" : "py-8",
|
||||
)}
|
||||
/>
|
||||
{props.tabs && (
|
||||
<TabPathLinks
|
||||
scrollableClassName="container max-w-7xl"
|
||||
@@ -32,7 +34,11 @@ export function ProjectPage(props: React.PropsWithChildren<ProjectPageProps>) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<main className="container max-w-7xl pt-6">{props.children}</main>
|
||||
<main
|
||||
className={cn("container max-w-7xl pt-6", props.containerClassName)}
|
||||
>
|
||||
{props.children}
|
||||
</main>
|
||||
<div className="h-20" />
|
||||
{props.footer && (
|
||||
<div className="border-t mt-auto">
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from "@/components/ui/hover-card";
|
||||
import { useClipboard } from "@/hooks/useClipboard";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function SolanaAddress(props: {
|
||||
address: string;
|
||||
shortenAddress?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
const shortenedAddress = useMemo(() => {
|
||||
return props.shortenAddress !== false
|
||||
? `${props.address.slice(0, 4)}...${props.address.slice(-4)}`
|
||||
: props.address;
|
||||
}, [props.address, props.shortenAddress]);
|
||||
|
||||
const lessShortenedAddress = useMemo(() => {
|
||||
return `${props.address.slice(0, 8)}...${props.address.slice(-8)}`;
|
||||
}, [props.address]);
|
||||
|
||||
const { onCopy, hasCopied } = useClipboard(props.address, 2000);
|
||||
|
||||
return (
|
||||
<HoverCard>
|
||||
<HoverCardTrigger asChild tabIndex={-1}>
|
||||
<Button
|
||||
className={cn(
|
||||
"flex flex-row items-center gap-2 px-0",
|
||||
props.className,
|
||||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
variant="link"
|
||||
>
|
||||
<div className="flex size-5 items-center justify-center rounded-full bg-gradient-to-br from-purple-500 to-blue-500" />
|
||||
<span className="cursor-pointer font-mono text-sm">
|
||||
{shortenedAddress}
|
||||
</span>
|
||||
</Button>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent
|
||||
className="w-80 border-border"
|
||||
onClick={(e) => {
|
||||
// do not close the hover card when clicking anywhere in the content
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-semibold text-lg">Solana Public Key</h3>
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
onClick={onCopy}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
{hasCopied ? (
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
)}
|
||||
{hasCopied ? "Copied!" : "Copy"}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="break-all rounded bg-muted p-3 text-center font-mono text-xs leading-relaxed">
|
||||
{lessShortenedAddress}
|
||||
</p>
|
||||
<div className="rounded-lg bg-muted/50 p-3">
|
||||
<p className="text-muted-foreground text-xs leading-relaxed">
|
||||
Solana public key for blockchain transactions.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import type { Meta, StoryObj } from "@storybook/nextjs";
|
||||
import { type ThirdwebClient, ZERO_ADDRESS } from "thirdweb";
|
||||
import type { SocialProfile } from "thirdweb/react";
|
||||
import { BadgeContainer, storybookThirdwebClient } from "@/storybook/utils";
|
||||
import { WalletAddress, WalletAddressUI } from "./wallet-address";
|
||||
|
||||
const meta = {
|
||||
component: Story,
|
||||
parameters: {
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
},
|
||||
title: "blocks/WalletAddress",
|
||||
} satisfies Meta<typeof Story>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
function Story() {
|
||||
const client = storybookThirdwebClient as unknown as ThirdwebClient;
|
||||
|
||||
return (
|
||||
<div className="container flex max-w-4xl flex-col gap-8 py-10">
|
||||
<BadgeContainer label="Social Profiles Loaded">
|
||||
<WalletAddressUI
|
||||
address="0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
|
||||
client={client}
|
||||
profiles={{
|
||||
data: vitalikEth,
|
||||
isPending: false,
|
||||
}}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="No Social Profiles">
|
||||
<WalletAddressUI
|
||||
address="0x83Dd93fA5D8343094f850f90B3fb90088C1bB425"
|
||||
client={client}
|
||||
shortenAddress
|
||||
profiles={{
|
||||
data: [],
|
||||
isPending: false,
|
||||
}}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Loading Social Profiles">
|
||||
<WalletAddressUI
|
||||
address="0x83Dd93fA5D8343094f850f90B3fb90088C1bB425"
|
||||
client={client}
|
||||
shortenAddress
|
||||
profiles={{
|
||||
data: [],
|
||||
isPending: true,
|
||||
}}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Zero address">
|
||||
<WalletAddressUI
|
||||
address={ZERO_ADDRESS}
|
||||
client={client}
|
||||
profiles={{
|
||||
data: [],
|
||||
isPending: false,
|
||||
}}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Real Component - Invalid Address">
|
||||
<WalletAddress address="not-an-address" client={client} />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Real Component - vitalik.eth">
|
||||
<WalletAddress
|
||||
address="0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
|
||||
client={client}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const vitalikEth: SocialProfile[] = [
|
||||
{
|
||||
type: "ens",
|
||||
name: "vitalik.eth",
|
||||
bio: "mi pinxe lo crino tcati",
|
||||
avatar: "https://euc.li/vitalik.eth",
|
||||
metadata: {
|
||||
name: "vitalik.eth",
|
||||
address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
|
||||
avatar: "https://euc.li/vitalik.eth",
|
||||
description: "mi pinxe lo crino tcati",
|
||||
url: "https://vitalik.ca",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "farcaster",
|
||||
name: "vitalik.eth",
|
||||
bio: undefined,
|
||||
avatar:
|
||||
"https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b663cd63-fecf-4d0f-7f87-0e0b6fd42800/original",
|
||||
metadata: {
|
||||
fid: 5650,
|
||||
bio: undefined,
|
||||
pfp: "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b663cd63-fecf-4d0f-7f87-0e0b6fd42800/original",
|
||||
username: "vitalik.eth",
|
||||
addresses: [
|
||||
"0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
|
||||
"0x96B6bB2bd2Eba3b4Fbefd7DbAC448ad7B6CBf279",
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,11 +1,10 @@
|
||||
"use client";
|
||||
import { CheckIcon, CircleSlashIcon, CopyIcon, XIcon } from "lucide-react";
|
||||
import { CircleSlashIcon, XIcon } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { isAddress, type ThirdwebClient, ZERO_ADDRESS } from "thirdweb";
|
||||
import { Blobbie, type SocialProfile, useSocialProfiles } from "thirdweb/react";
|
||||
import { Img } from "@/components/blocks/Img";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
HoverCard,
|
||||
@@ -13,41 +12,56 @@ import {
|
||||
HoverCardTrigger,
|
||||
} from "@/components/ui/hover-card";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { useClipboard } from "@/hooks/useClipboard";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
|
||||
import { CopyTextButton } from "../ui/CopyTextButton";
|
||||
import { Skeleton } from "../ui/skeleton";
|
||||
|
||||
export function WalletAddress(props: {
|
||||
type WalletAddressProps = {
|
||||
address: string | undefined;
|
||||
shortenAddress?: boolean;
|
||||
className?: string;
|
||||
iconClassName?: string;
|
||||
client: ThirdwebClient;
|
||||
fallbackIcon?: React.ReactNode;
|
||||
}) {
|
||||
// default back to zero address if no address provided
|
||||
const address = useMemo(() => props.address || ZERO_ADDRESS, [props.address]);
|
||||
|
||||
const [shortenedAddress, lessShortenedAddress] = useMemo(() => {
|
||||
return [
|
||||
props.shortenAddress !== false
|
||||
? `${address.slice(0, 6)}...${address.slice(-4)}`
|
||||
: address,
|
||||
`${address.slice(0, 14)}...${address.slice(-12)}`,
|
||||
];
|
||||
}, [address, props.shortenAddress]);
|
||||
};
|
||||
|
||||
export function WalletAddress(props: WalletAddressProps) {
|
||||
const profiles = useSocialProfiles({
|
||||
address: address,
|
||||
address: props.address,
|
||||
client: props.client,
|
||||
});
|
||||
|
||||
const { onCopy, hasCopied } = useClipboard(address, 2000);
|
||||
return (
|
||||
<WalletAddressUI
|
||||
{...props}
|
||||
profiles={{
|
||||
data: profiles.data || [],
|
||||
isPending: profiles.isPending,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function WalletAddressUI(
|
||||
props: WalletAddressProps & {
|
||||
profiles: {
|
||||
data: SocialProfile[];
|
||||
isPending: boolean;
|
||||
};
|
||||
},
|
||||
) {
|
||||
const address = props.address || ZERO_ADDRESS;
|
||||
|
||||
const shortenedAddress =
|
||||
props.shortenAddress !== false
|
||||
? `${address.slice(0, 6)}...${address.slice(-4)}`
|
||||
: address;
|
||||
|
||||
if (!isAddress(address)) {
|
||||
return (
|
||||
<ToolTipLabel hoverable label={address}>
|
||||
<span className="flex items-center gap-2 underline-offset-4 hover:underline">
|
||||
<span className="flex items-center gap-2 underline-offset-4 hover:underline w-fit">
|
||||
<div className="flex size-5 items-center justify-center rounded-full border bg-background">
|
||||
<XIcon className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
@@ -78,7 +92,7 @@ export function WalletAddress(props: {
|
||||
<HoverCardTrigger asChild tabIndex={-1}>
|
||||
<Button
|
||||
className={cn(
|
||||
"flex flex-row items-center gap-2 px-0",
|
||||
"flex flex-row items-center gap-2 px-0 max-w-full truncate",
|
||||
props.className,
|
||||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
@@ -88,89 +102,82 @@ export function WalletAddress(props: {
|
||||
<WalletAvatar
|
||||
address={address}
|
||||
iconClassName={props.iconClassName}
|
||||
profiles={profiles.data || []}
|
||||
profiles={props.profiles.data}
|
||||
thirdwebClient={props.client}
|
||||
fallbackIcon={props.fallbackIcon}
|
||||
/>
|
||||
)}
|
||||
<span className="cursor-pointer font-mono">
|
||||
{profiles.data?.[0]?.name || shortenedAddress}
|
||||
<span className="cursor-pointer font-mono max-w-full truncate">
|
||||
{props.profiles.data?.[0]?.name || shortenedAddress}
|
||||
</span>
|
||||
</Button>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent
|
||||
className="w-80 border-border"
|
||||
className="w-[calc(100vw-2rem)] lg:w-[450px] border-border rounded-xl p-4 lg:p-6"
|
||||
onClick={(e) => {
|
||||
// do not close the hover card when clicking anywhere in the content
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-semibold text-lg">Wallet Address</h3>
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
onClick={onCopy}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
{hasCopied ? (
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
)}
|
||||
{hasCopied ? "Copied!" : "Copy"}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="rounded bg-muted p-2 text-center font-mono text-sm">
|
||||
{lessShortenedAddress}
|
||||
</p>
|
||||
<h3 className="font-semibold text-lg">Social Profiles</h3>
|
||||
{profiles.isPending ? (
|
||||
<p className="text-muted-foreground text-sm">Loading profiles...</p>
|
||||
) : !profiles.data?.length ? (
|
||||
<p className="text-muted-foreground text-sm">No profiles found</p>
|
||||
) : (
|
||||
profiles.data?.map((profile) => {
|
||||
const walletAvatarLink = resolveSchemeWithErrorHandler({
|
||||
client: props.client,
|
||||
uri: profile.avatar,
|
||||
});
|
||||
<div className="space-y-1">
|
||||
<h3 className="font-medium text-sm">Wallet Address</h3>
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-row items-center gap-2"
|
||||
key={profile.type + profile.name}
|
||||
>
|
||||
{walletAvatarLink && (
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
alt={profile.name}
|
||||
className="object-cover"
|
||||
src={walletAvatarLink}
|
||||
/>
|
||||
{profile.name && (
|
||||
<AvatarFallback>
|
||||
{profile.name.slice(0, 2)}
|
||||
</AvatarFallback>
|
||||
)}
|
||||
</Avatar>
|
||||
)}
|
||||
<div className="flex w-full flex-col gap-1">
|
||||
<div className="flex w-full flex-row items-center justify-between gap-4">
|
||||
<h4 className="font-semibold text-md">{profile.name}</h4>
|
||||
<Badge variant="outline">{profile.type}</Badge>
|
||||
<CopyTextButton
|
||||
textToShow={address}
|
||||
textToCopy={address}
|
||||
tooltip="Copy address"
|
||||
copyIconPosition="right"
|
||||
variant="ghost"
|
||||
className="text-muted-foreground -translate-x-1.5"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 border-t pt-5 border-dashed">
|
||||
<h3 className="font-medium text-sm">Social Profiles</h3>
|
||||
|
||||
{props.profiles.isPending ? (
|
||||
<Skeleton className="h-4 w-[50%]" />
|
||||
) : !props.profiles.data?.length ? (
|
||||
<p className="text-muted-foreground text-sm">No profiles found</p>
|
||||
) : (
|
||||
<div className="!mt-2">
|
||||
{props.profiles.data?.map((profile) => {
|
||||
const walletAvatarLink = resolveSchemeWithErrorHandler({
|
||||
client: props.client,
|
||||
uri: profile.avatar,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-row items-center gap-3 py-2"
|
||||
key={profile.type + profile.name}
|
||||
>
|
||||
<Avatar className="size-9">
|
||||
<AvatarImage
|
||||
alt={profile.name}
|
||||
className="object-cover"
|
||||
src={walletAvatarLink}
|
||||
/>
|
||||
{profile.name && (
|
||||
<AvatarFallback>
|
||||
{profile.name.slice(0, 2)}
|
||||
</AvatarFallback>
|
||||
)}
|
||||
</Avatar>
|
||||
|
||||
<div className="space-y-0.5">
|
||||
<h4 className="text-sm leading-none">{profile.name}</h4>
|
||||
<span className="text-muted-foreground text-xs leading-none capitalize">
|
||||
{profile.type === "ens" ? "ENS" : profile.type}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{profile.bio && (
|
||||
<p className="line-clamp-1 whitespace-normal text-muted-foreground text-sm">
|
||||
{profile.bio}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
|
||||
@@ -17,6 +17,7 @@ import { resetAnalytics } from "@/analytics/reset";
|
||||
import { CustomChainRenderer } from "@/components/misc/CustomChainRenderer";
|
||||
import { LazyConfigureNetworkModal } from "@/components/misc/configure-networks/LazyConfigureNetworkModal";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { appMetadata } from "@/constants/connect";
|
||||
import { popularChains } from "@/constants/popularChains";
|
||||
import { useAllChainsData } from "@/hooks/chains/allChains";
|
||||
import { useFavoriteChainIds } from "@/hooks/favorite-chains";
|
||||
@@ -138,11 +139,7 @@ export const CustomConnectWallet = (props: {
|
||||
return (
|
||||
<>
|
||||
<ConnectButton
|
||||
appMetadata={{
|
||||
logoUrl: "https://thirdweb.com/favicon.ico",
|
||||
name: "thirdweb",
|
||||
url: "https://thirdweb.com",
|
||||
}}
|
||||
appMetadata={appMetadata}
|
||||
chain={props.chain}
|
||||
chains={allChainsV5}
|
||||
client={client}
|
||||
@@ -280,11 +277,7 @@ export function useCustomConnectModal() {
|
||||
return useCallback(
|
||||
(options: { chain?: Chain; client: ThirdwebClient }) => {
|
||||
return connect({
|
||||
appMetadata: {
|
||||
logoUrl: "https://thirdweb.com/favicon.ico",
|
||||
name: "thirdweb",
|
||||
url: "https://thirdweb.com",
|
||||
},
|
||||
appMetadata,
|
||||
chain: options?.chain,
|
||||
client: options.client,
|
||||
privacyPolicyUrl: "/privacy-policy",
|
||||
|
||||
@@ -62,6 +62,8 @@ function Story() {
|
||||
client={storybookThirdwebClient}
|
||||
contracts={[]}
|
||||
pageSize={10}
|
||||
projectId="project-id"
|
||||
teamId="team-id"
|
||||
projectSlug={projectSlug}
|
||||
removeContractFromProject={removeContractStub}
|
||||
teamSlug={teamSlug}
|
||||
@@ -74,6 +76,8 @@ function Story() {
|
||||
client={storybookThirdwebClient}
|
||||
contracts={[popularPolygonNFTs[0] as ProjectContract]}
|
||||
pageSize={10}
|
||||
projectId="project-id"
|
||||
teamId="team-id"
|
||||
projectSlug={projectSlug}
|
||||
removeContractFromProject={removeContractStub}
|
||||
teamSlug={teamSlug}
|
||||
@@ -86,6 +90,8 @@ function Story() {
|
||||
client={storybookThirdwebClient}
|
||||
contracts={popularPolygonNFTs}
|
||||
pageSize={10}
|
||||
projectId="project-id"
|
||||
teamId="team-id"
|
||||
projectSlug={projectSlug}
|
||||
removeContractFromProject={removeContractStub}
|
||||
teamSlug={teamSlug}
|
||||
@@ -95,6 +101,8 @@ function Story() {
|
||||
|
||||
<BadgeContainer label="10 Contracts, 2 chains">
|
||||
<ContractTableUI
|
||||
projectId="project-id"
|
||||
teamId="team-id"
|
||||
client={storybookThirdwebClient}
|
||||
contracts={[...popularPolygonNFTs, ...EthereumPopularNFTs]}
|
||||
pageSize={10}
|
||||
@@ -114,6 +122,8 @@ function Story() {
|
||||
...basePopularTokens,
|
||||
]}
|
||||
pageSize={10}
|
||||
projectId="project-id"
|
||||
teamId="team-id"
|
||||
projectSlug={projectSlug}
|
||||
removeContractFromProject={removeContractStub}
|
||||
teamSlug={teamSlug}
|
||||
|
||||
@@ -32,6 +32,8 @@ import { useAllChainsData } from "@/hooks/chains/allChains";
|
||||
import { removeContractFromProject } from "@/hooks/project-contracts";
|
||||
import { ChainIconClient } from "@/icons/ChainIcon";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { revalidateCacheTagAction } from "../../../actions/revalidate";
|
||||
import { projectContractsCacheTag } from "../../../api/project/cache-tag";
|
||||
import {
|
||||
ContractNameCell,
|
||||
ContractTypeCell,
|
||||
@@ -57,6 +59,8 @@ export function ContractTable(props: {
|
||||
client={props.client}
|
||||
contracts={props.contracts}
|
||||
pageSize={props.pageSize}
|
||||
projectId={props.projectId}
|
||||
teamId={props.teamId}
|
||||
projectSlug={props.projectSlug}
|
||||
removeContractFromProject={async (contractId) => {
|
||||
await removeContractFromProject({
|
||||
@@ -77,6 +81,8 @@ export function ContractTableUI(props: {
|
||||
removeContractFromProject: (contractId: string) => Promise<void>;
|
||||
client: ThirdwebClient;
|
||||
variant: "asset" | "contract";
|
||||
teamId: string;
|
||||
projectId: string;
|
||||
teamSlug: string;
|
||||
projectSlug: string;
|
||||
}) {
|
||||
@@ -243,6 +249,13 @@ export function ContractTableUI(props: {
|
||||
<ContractActionsCell
|
||||
contractId={contract.id}
|
||||
onContractRemoved={() => {
|
||||
revalidateCacheTagAction(
|
||||
projectContractsCacheTag({
|
||||
teamId: props.teamId,
|
||||
projectId: props.projectId,
|
||||
}),
|
||||
);
|
||||
|
||||
setDeletedContractIds((v) => {
|
||||
return [...v, contract.id];
|
||||
});
|
||||
|
||||
@@ -178,7 +178,7 @@ export async function ContractCard({
|
||||
{auditLink && !isBeta && (
|
||||
<>
|
||||
<Link
|
||||
className="relative z-1 flex items-center gap-1 font-medium text-sm text-success-text hover:underline"
|
||||
className="relative z-10 flex items-center gap-1 font-medium text-sm text-success-text hover:underline"
|
||||
href={auditLink}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { ArrowDownToLineIcon, ExternalLinkIcon } from "lucide-react";
|
||||
import { UnderlineLink } from "@workspace/ui/components/UnderlineLink";
|
||||
import {
|
||||
ArrowDownToLineIcon,
|
||||
CircleAlertIcon,
|
||||
ExternalLinkIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { getAddress, isAddress, type ThirdwebClient } from "thirdweb";
|
||||
import {
|
||||
getAddress,
|
||||
getContract,
|
||||
isAddress,
|
||||
type ThirdwebClient,
|
||||
} from "thirdweb";
|
||||
import { defineChain } from "thirdweb/chains";
|
||||
import { useActiveWalletChain } from "thirdweb/react";
|
||||
import { z } from "zod";
|
||||
import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors";
|
||||
@@ -31,6 +43,11 @@ import { Spinner } from "@/components/ui/Spinner";
|
||||
import { useChainSlug } from "@/hooks/chains/chainSlug";
|
||||
import { useAddContractToProject } from "@/hooks/project-contracts";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { revalidateCacheTagAction } from "../../../actions/revalidate";
|
||||
import { projectContractsCacheTag } from "../../../api/project/cache-tag";
|
||||
import { resolveFunctionSelectors } from "../../../lib/selectors";
|
||||
import { supportedERCs } from "../../../utils/supportedERCs";
|
||||
import { Alert, AlertDescription, AlertTitle } from "../../ui/alert";
|
||||
|
||||
type ImportModalProps = {
|
||||
isOpen: boolean;
|
||||
@@ -42,6 +59,7 @@ type ImportModalProps = {
|
||||
client: ThirdwebClient;
|
||||
type: "contract" | "asset";
|
||||
onSuccess?: () => void;
|
||||
allowedContractType: "token" | "non-token";
|
||||
};
|
||||
|
||||
export const ImportModal: React.FC<ImportModalProps> = (props) => {
|
||||
@@ -75,6 +93,7 @@ export const ImportModal: React.FC<ImportModalProps> = (props) => {
|
||||
teamId={props.teamId}
|
||||
teamSlug={props.teamSlug}
|
||||
type={props.type}
|
||||
allowedContractType={props.allowedContractType}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
@@ -105,6 +124,7 @@ function ImportForm(props: {
|
||||
client: ThirdwebClient;
|
||||
type: "contract" | "asset";
|
||||
onSuccess?: () => void;
|
||||
allowedContractType: "token" | "non-token";
|
||||
}) {
|
||||
const router = useDashboardRouter();
|
||||
const activeChainId = useActiveWalletChain()?.id;
|
||||
@@ -119,9 +139,14 @@ function ImportForm(props: {
|
||||
const chainSlug = useChainSlug(form.watch("chainId"));
|
||||
const addContractToProject = useAddContractToProject();
|
||||
|
||||
const [notAllowedError, setNotAllowedError] = useState(false);
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onChange={() => {
|
||||
setNotAllowedError(false);
|
||||
}}
|
||||
onSubmit={form.handleSubmit(async (data) => {
|
||||
const { chainId } = data;
|
||||
let contractAddress: string;
|
||||
@@ -158,6 +183,27 @@ function ImportForm(props: {
|
||||
return;
|
||||
}
|
||||
|
||||
// check if the contract matches the requirement
|
||||
const contract = getContract({
|
||||
address: contractAddress,
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
chain: defineChain(chainId),
|
||||
client: props.client,
|
||||
});
|
||||
|
||||
const functionSelectors = await resolveFunctionSelectors(contract);
|
||||
const ercs = supportedERCs(functionSelectors);
|
||||
|
||||
const isToken = ercs.isERC20 || ercs.isERC721 || ercs.isERC1155;
|
||||
|
||||
if (
|
||||
(props.allowedContractType === "token" && !isToken) ||
|
||||
(props.allowedContractType === "non-token" && isToken)
|
||||
) {
|
||||
setNotAllowedError(true);
|
||||
return;
|
||||
}
|
||||
|
||||
addContractToProject.mutate(
|
||||
{
|
||||
chainId: chainId.toString(),
|
||||
@@ -177,6 +223,12 @@ function ImportForm(props: {
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
revalidateCacheTagAction(
|
||||
projectContractsCacheTag({
|
||||
teamId: props.teamId,
|
||||
projectId: props.projectId,
|
||||
}),
|
||||
);
|
||||
router.refresh();
|
||||
toast.success("Contract imported successfully");
|
||||
props.onSuccess?.();
|
||||
@@ -217,6 +269,34 @@ function ImportForm(props: {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{notAllowedError && (
|
||||
<div className="px-6 pt-6 text-sm text-destructive-text">
|
||||
<Alert variant="destructive">
|
||||
<CircleAlertIcon className="size-5" />
|
||||
<AlertTitle> Invalid Contract </AlertTitle>
|
||||
<AlertDescription>
|
||||
{props.allowedContractType === "token" && (
|
||||
<span>
|
||||
This contract is not a token contract. <br /> Only ERC20,
|
||||
ERC721 and ERC1155 contracts can be imported as tokens.
|
||||
</span>
|
||||
)}
|
||||
{props.allowedContractType === "non-token" && (
|
||||
<span>
|
||||
This contract is a token contract. <br /> Go to the{" "}
|
||||
<UnderlineLink
|
||||
href={`/team/${props.teamSlug}/${props.projectSlug}/tokens`}
|
||||
>
|
||||
Tokens
|
||||
</UnderlineLink>{" "}
|
||||
page to import in dashboard.
|
||||
</span>
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-8 flex justify-end border-t bg-card p-6">
|
||||
{addContractToProject.isSuccess &&
|
||||
addContractToProject.data?.result ? (
|
||||
|
||||
+12
-2
@@ -14,6 +14,15 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import type { SearchType } from "./types";
|
||||
|
||||
const searchTypeLabels: Record<SearchType, string> = {
|
||||
email: "Email",
|
||||
phone: "Phone",
|
||||
id: "Auth Identifier",
|
||||
address: "Address",
|
||||
externalWallet: "External Wallet",
|
||||
userId: "User Identifier",
|
||||
};
|
||||
|
||||
export function AdvancedSearchInput(props: {
|
||||
onSearch: (searchType: SearchType, query: string) => void;
|
||||
onClear: () => void;
|
||||
@@ -52,7 +61,8 @@ export function AdvancedSearchInput(props: {
|
||||
<SelectContent>
|
||||
<SelectItem value="email">Email</SelectItem>
|
||||
<SelectItem value="phone">Phone</SelectItem>
|
||||
<SelectItem value="id">ID</SelectItem>
|
||||
<SelectItem value="id">Auth Identifier</SelectItem>
|
||||
<SelectItem value="userId">User Identifier</SelectItem>
|
||||
<SelectItem value="address">Address</SelectItem>
|
||||
<SelectItem value="externalWallet">External Wallet</SelectItem>
|
||||
</SelectContent>
|
||||
@@ -62,7 +72,7 @@ export function AdvancedSearchInput(props: {
|
||||
<div className="relative flex-1">
|
||||
<Input
|
||||
className="bg-background pl-9 border-r-0 rounded-r-none rounded-l-full"
|
||||
placeholder={`Search by ${searchType}...`}
|
||||
placeholder={`Search by ${searchTypeLabels[searchType]}...`}
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
|
||||
@@ -12,18 +12,25 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { CopyTextButton } from "../ui/CopyTextButton";
|
||||
|
||||
const getUserIdentifier = (user: WalletUser) => {
|
||||
const getAuthIdentifier = (user: WalletUser) => {
|
||||
const mainDetail = user.linkedAccounts[0]?.details;
|
||||
return (
|
||||
mainDetail?.id ??
|
||||
mainDetail?.email ??
|
||||
mainDetail?.phone ??
|
||||
mainDetail?.address ??
|
||||
mainDetail?.id ??
|
||||
user.id
|
||||
mainDetail?.address
|
||||
);
|
||||
};
|
||||
|
||||
const truncateIdentifier = (value: string) => {
|
||||
if (value.length <= 18) {
|
||||
return value;
|
||||
}
|
||||
return `${value.slice(0, 8)}...${value.slice(-4)}`;
|
||||
};
|
||||
|
||||
export function SearchResults(props: {
|
||||
results: WalletUser[];
|
||||
client: ThirdwebClient;
|
||||
@@ -51,6 +58,8 @@ export function SearchResults(props: {
|
||||
const mainDetail = user.linkedAccounts?.[0]?.details;
|
||||
const email = mainDetail?.email as string | undefined;
|
||||
const phone = mainDetail?.phone as string | undefined;
|
||||
const authIdentifier = getAuthIdentifier(user);
|
||||
const userIdentifier = user.id;
|
||||
|
||||
// Get external wallet addresses from linkedAccounts where type is 'siwe'
|
||||
const externalWalletAccounts =
|
||||
@@ -68,7 +77,34 @@ export function SearchResults(props: {
|
||||
<p className="text-sm font-medium text-muted-foreground">
|
||||
User Identifier
|
||||
</p>
|
||||
<p className="text-sm">{getUserIdentifier(user)}</p>
|
||||
{userIdentifier ? (
|
||||
<CopyTextButton
|
||||
textToShow={truncateIdentifier(userIdentifier)}
|
||||
textToCopy={userIdentifier}
|
||||
tooltip="Copy User Identifier"
|
||||
copyIconPosition="left"
|
||||
variant="ghost"
|
||||
/>
|
||||
) : (
|
||||
<p className="text-sm">N/A</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">
|
||||
Auth Identifier
|
||||
</p>
|
||||
{authIdentifier ? (
|
||||
<CopyTextButton
|
||||
textToShow={truncateIdentifier(authIdentifier)}
|
||||
textToCopy={authIdentifier}
|
||||
tooltip="Copy Auth Identifier"
|
||||
copyIconPosition="left"
|
||||
variant="ghost"
|
||||
/>
|
||||
) : (
|
||||
<p className="text-sm">N/A</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{walletAddress && (
|
||||
|
||||
+70
-45
@@ -28,22 +28,42 @@ import { SearchResults } from "./SearchResults";
|
||||
import { searchUsers } from "./searchUsers";
|
||||
import type { SearchType } from "./types";
|
||||
|
||||
const getUserIdentifier = (accounts: WalletUser["linkedAccounts"]) => {
|
||||
const getAuthIdentifier = (accounts: WalletUser["linkedAccounts"]) => {
|
||||
const mainDetail = accounts[0]?.details;
|
||||
return (
|
||||
mainDetail?.id ??
|
||||
mainDetail?.email ??
|
||||
mainDetail?.phone ??
|
||||
mainDetail?.address ??
|
||||
mainDetail?.id
|
||||
mainDetail?.address
|
||||
);
|
||||
};
|
||||
|
||||
const getExternalWallets = (accounts: WalletUser["linkedAccounts"]) => {
|
||||
return accounts?.filter((account) => account.type === "siwe") || [];
|
||||
const getPrimaryEmail = (accounts: WalletUser["linkedAccounts"]) => {
|
||||
const emailFromPrimary = accounts[0]?.details?.email;
|
||||
if (emailFromPrimary) {
|
||||
return emailFromPrimary;
|
||||
}
|
||||
|
||||
const emailAccount = accounts.find((account) => {
|
||||
return typeof account.details?.email === "string" && account.details.email;
|
||||
});
|
||||
|
||||
if (emailAccount && typeof emailAccount.details.email === "string") {
|
||||
return emailAccount.details.email;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const columnHelper = createColumnHelper<WalletUser>();
|
||||
|
||||
const truncateIdentifier = (value: string) => {
|
||||
if (value.length <= 18) {
|
||||
return value;
|
||||
}
|
||||
return `${value.slice(0, 8)}...${value.slice(-4)}`;
|
||||
};
|
||||
|
||||
export function InAppWalletUsersPageContent(
|
||||
props: {
|
||||
authToken: string;
|
||||
@@ -56,9 +76,30 @@ export function InAppWalletUsersPageContent(
|
||||
) {
|
||||
const columns = useMemo(() => {
|
||||
return [
|
||||
columnHelper.accessor("id", {
|
||||
cell: (cell) => {
|
||||
const userId = cell.getValue();
|
||||
|
||||
if (!userId) {
|
||||
return "N/A";
|
||||
}
|
||||
|
||||
return (
|
||||
<CopyTextButton
|
||||
textToShow={truncateIdentifier(userId)}
|
||||
textToCopy={userId}
|
||||
tooltip="Copy User Identifier"
|
||||
copyIconPosition="left"
|
||||
variant="ghost"
|
||||
/>
|
||||
);
|
||||
},
|
||||
header: "User Identifier",
|
||||
id: "user_identifier",
|
||||
}),
|
||||
columnHelper.accessor("linkedAccounts", {
|
||||
cell: (cell) => {
|
||||
const identifier = getUserIdentifier(cell.getValue());
|
||||
const identifier = getAuthIdentifier(cell.getValue());
|
||||
|
||||
if (!identifier) {
|
||||
return "N/A";
|
||||
@@ -66,21 +107,17 @@ export function InAppWalletUsersPageContent(
|
||||
|
||||
return (
|
||||
<CopyTextButton
|
||||
textToShow={
|
||||
identifier.length > 30
|
||||
? `${identifier.slice(0, 30)}...`
|
||||
: identifier
|
||||
}
|
||||
textToShow={truncateIdentifier(identifier)}
|
||||
textToCopy={identifier}
|
||||
tooltip="Copy User Identifier"
|
||||
tooltip="Copy Auth Identifier"
|
||||
copyIconPosition="left"
|
||||
variant="ghost"
|
||||
/>
|
||||
);
|
||||
},
|
||||
enableColumnFilter: true,
|
||||
header: "User Identifier",
|
||||
id: "user_identifier",
|
||||
header: "Auth Identifier",
|
||||
id: "auth_identifier",
|
||||
}),
|
||||
columnHelper.accessor("wallets", {
|
||||
cell: (cell) => {
|
||||
@@ -94,33 +131,24 @@ export function InAppWalletUsersPageContent(
|
||||
}),
|
||||
columnHelper.accessor("linkedAccounts", {
|
||||
cell: (cell) => {
|
||||
const externalWallets = getExternalWallets(cell.getValue());
|
||||
if (externalWallets.length === 0) {
|
||||
return <span className="text-muted-foreground text-sm">None</span>;
|
||||
const email = getPrimaryEmail(cell.getValue());
|
||||
|
||||
if (!email) {
|
||||
return <span className="text-muted-foreground text-sm">N/A</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{externalWallets.slice(0, 2).map((account) => {
|
||||
const address = account.details?.address as string | undefined;
|
||||
return address ? (
|
||||
<div
|
||||
key={`external-${address}-${account.details?.id}`}
|
||||
className="text-xs"
|
||||
>
|
||||
<WalletAddress address={address} client={props.client} />
|
||||
</div>
|
||||
) : null;
|
||||
})}
|
||||
{externalWallets.length > 2 && (
|
||||
<span className="text-muted-foreground text-xs">
|
||||
+{externalWallets.length - 2} more
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<CopyTextButton
|
||||
textToShow={email}
|
||||
textToCopy={email}
|
||||
tooltip="Copy Email"
|
||||
copyIconPosition="left"
|
||||
variant="ghost"
|
||||
/>
|
||||
);
|
||||
},
|
||||
header: "External Wallets",
|
||||
id: "external_wallets",
|
||||
header: "Email",
|
||||
id: "email",
|
||||
}),
|
||||
columnHelper.accessor("wallets", {
|
||||
cell: (cell) => {
|
||||
@@ -230,20 +258,17 @@ export function InAppWalletUsersPageContent(
|
||||
});
|
||||
const csv = Papa.unparse(
|
||||
usersWallets.map((row) => {
|
||||
const externalWallets = getExternalWallets(row.linkedAccounts);
|
||||
const externalWalletAddresses = externalWallets
|
||||
.map((account) => account.details?.address)
|
||||
.filter(Boolean)
|
||||
.join(", ");
|
||||
const email = getPrimaryEmail(row.linkedAccounts);
|
||||
|
||||
return {
|
||||
address: row.wallets[0]?.address || "Uninitialized",
|
||||
created: row.wallets[0]?.createdAt
|
||||
? new Date(row.wallets[0].createdAt).toISOString()
|
||||
: "Wallet not created yet",
|
||||
external_wallets: externalWalletAddresses || "None",
|
||||
email: email || "N/A",
|
||||
login_methods: row.linkedAccounts.map((acc) => acc.type).join(", "),
|
||||
user_identifier: getUserIdentifier(row.linkedAccounts),
|
||||
auth_identifier: getAuthIdentifier(row.linkedAccounts) || "N/A",
|
||||
user_identifier: row.id || "N/A",
|
||||
};
|
||||
}),
|
||||
);
|
||||
@@ -312,7 +337,7 @@ export function InAppWalletUsersPageContent(
|
||||
isFetched={walletsQuery.isFetched}
|
||||
isPending={walletsQuery.isPending}
|
||||
tableContainerClassName="rounded-none border-x-0 border-b-0"
|
||||
title="in-app wallets"
|
||||
title="User wallets"
|
||||
/>
|
||||
|
||||
<div className="flex justify-center gap-3 border-t bg-card p-6">
|
||||
|
||||
@@ -18,10 +18,10 @@ configure({
|
||||
type APIWallet = ListUserWalletsResponses[200]["result"]["wallets"][0];
|
||||
type APIProfile = APIWallet["profiles"][0];
|
||||
|
||||
// Transform API response to match existing WalletUser format
|
||||
// Transform API response to match wallet user format
|
||||
function transformToWalletUser(apiWallet: APIWallet): WalletUser {
|
||||
return {
|
||||
id: getProfileId(apiWallet.profiles[0]) || "",
|
||||
id: apiWallet.userId || getProfileId(apiWallet.profiles[0]) || "",
|
||||
linkedAccounts: apiWallet.profiles.map((profile) => {
|
||||
// Create details object based on the profile data
|
||||
let details:
|
||||
@@ -105,6 +105,9 @@ export async function searchUsers(
|
||||
case "externalWallet":
|
||||
queryParams.externalWalletAddress = query;
|
||||
break;
|
||||
case "userId":
|
||||
queryParams.userId = query;
|
||||
break;
|
||||
}
|
||||
|
||||
// Use the generated API function with Bearer authentication
|
||||
|
||||
@@ -3,4 +3,5 @@ export type SearchType =
|
||||
| "phone"
|
||||
| "id"
|
||||
| "address"
|
||||
| "externalWallet";
|
||||
| "externalWallet"
|
||||
| "userId";
|
||||
|
||||
@@ -44,12 +44,11 @@ function AnnouncementBannerUI(props: {
|
||||
}
|
||||
|
||||
export function AnnouncementBanner() {
|
||||
// return (
|
||||
// <AnnouncementBannerUI
|
||||
// href="https://blog.thirdweb.com/the-fastest-way-to-build-web3-applications/"
|
||||
// label="We have re-branded our Engine, Payments, and Connect products. Please read the full blog post for details on changes"
|
||||
// trackingLabel="product-rebrand"
|
||||
// />
|
||||
// );
|
||||
return null;
|
||||
return (
|
||||
<AnnouncementBannerUI
|
||||
href="https://blog.thirdweb.com/changelog/project-view-update-wallets/"
|
||||
label="Transactions, Vault, and Account Abstraction are now part of Wallets in Project Dashboard. View changes"
|
||||
trackingLabel="project-view-update-wallets"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ const CreateProjectDialog = (props: CreateProjectDialogProps) => {
|
||||
<CreateProjectDialogUI
|
||||
createProject={async (params) => {
|
||||
const res = await createProjectClient(props.teamId, params);
|
||||
await createVaultAccountAndAccessToken({
|
||||
const vaultTokens = await createVaultAccountAndAccessToken({
|
||||
project: res.project,
|
||||
projectSecretKey: res.secret,
|
||||
}).catch((error) => {
|
||||
@@ -74,6 +74,13 @@ const CreateProjectDialog = (props: CreateProjectDialogProps) => {
|
||||
);
|
||||
throw error;
|
||||
});
|
||||
|
||||
const managementAccessToken = vaultTokens.managementToken?.accessToken;
|
||||
|
||||
if (!managementAccessToken) {
|
||||
throw new Error("Missing management access token for project wallet");
|
||||
}
|
||||
|
||||
return {
|
||||
project: res.project,
|
||||
secret: res.secret,
|
||||
|
||||
@@ -41,7 +41,7 @@ export function CopyTextButton(props: {
|
||||
<Button
|
||||
aria-label={props.tooltip}
|
||||
className={cn(
|
||||
"flex h-auto w-auto gap-2 rounded-lg px-1.5 py-0.5 font-normal text-foreground",
|
||||
"flex h-auto w-auto gap-2 rounded-lg px-1.5 py-0.5 font-normal text-foreground max-w-full truncate",
|
||||
props.className,
|
||||
)}
|
||||
onClick={(e) => {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
"use client";
|
||||
import { ArrowUpRightIcon, CheckIcon, CopyIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "./button";
|
||||
import { ToolTipLabel } from "./tooltip";
|
||||
|
||||
export function LinkWithCopyButton(props: {
|
||||
href: string;
|
||||
textToShow: string;
|
||||
textToCopy: string;
|
||||
copyTooltip: string;
|
||||
className?: string;
|
||||
linkIconClassName?: string;
|
||||
}) {
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const Icon = isCopied ? CheckIcon : CopyIcon;
|
||||
|
||||
return (
|
||||
<div className={cn("flex items-center gap-1", props.className)}>
|
||||
<ToolTipLabel label={props.copyTooltip}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="p-1 h-auto shrink-0 opacity-70 hover:opacity-100 text-muted-foreground"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(props.textToCopy);
|
||||
setIsCopied(true);
|
||||
setTimeout(() => setIsCopied(false), 1000);
|
||||
}}
|
||||
>
|
||||
<Icon className="size-3" />
|
||||
</Button>
|
||||
</ToolTipLabel>
|
||||
<Link
|
||||
href={props.href}
|
||||
target="_blank"
|
||||
className="text-sm text-muted-foreground hover:underline flex items-center gap-1 tabular-nums flex-1 truncate hover:text-foreground group"
|
||||
>
|
||||
<span className="max-w-full truncate tabular-nums">
|
||||
{props.textToShow}
|
||||
</span>
|
||||
<ArrowUpRightIcon
|
||||
className={cn(
|
||||
"size-3.5 opacity-70 shrink-0 group-hover:opacity-100",
|
||||
props.linkIconClassName,
|
||||
)}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export const appMetadata = {
|
||||
logoUrl: "https://thirdweb.com/favicon.ico",
|
||||
name: "thirdweb",
|
||||
url: "https://thirdweb.com",
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
export const siwaExamplePrompts = [
|
||||
"How do I add in-app wallet with sign in with google to my react app?",
|
||||
"How do I add user wallet with sign in with google to my react app?",
|
||||
"How do I send a transaction in Unity?",
|
||||
"What does this contract revert error mean?",
|
||||
"I see thirdweb support id in my console log, can you help me?",
|
||||
|
||||
@@ -119,12 +119,17 @@ export function useCsvUpload<
|
||||
}
|
||||
Papa.parse(csv, {
|
||||
complete: (results) => {
|
||||
const data = props.csvParser(results.data as T[]);
|
||||
if (!data[0]?.address) {
|
||||
try {
|
||||
const data = props.csvParser(results.data as T[]);
|
||||
if (!data[0]?.address) {
|
||||
setNoCsv(true);
|
||||
return;
|
||||
}
|
||||
setRawData(data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setNoCsv(true);
|
||||
return;
|
||||
}
|
||||
setRawData(data);
|
||||
},
|
||||
header: true,
|
||||
});
|
||||
|
||||
@@ -59,7 +59,10 @@ export async function fetchDashboardContractMetadata(
|
||||
|
||||
return {
|
||||
contractType: compilerMetadata?.name || "",
|
||||
image: contractMetadata.image || "",
|
||||
image:
|
||||
contractMetadata.image && typeof contractMetadata.image === "string"
|
||||
? contractMetadata.image
|
||||
: "",
|
||||
name: contractName,
|
||||
symbol: contractSymbol,
|
||||
};
|
||||
|
||||
@@ -76,10 +76,10 @@ const fetchAccountList = ({
|
||||
};
|
||||
};
|
||||
|
||||
// Transform API response to match existing WalletUser format
|
||||
// Transform API response to match the wallet user format
|
||||
function transformToWalletUser(apiWallet: APIWallet): WalletUser {
|
||||
return {
|
||||
id: getProfileId(apiWallet.profiles[0]) || "",
|
||||
id: apiWallet.userId || getProfileId(apiWallet.profiles[0]) || "",
|
||||
linkedAccounts: apiWallet.profiles.map((profile) => {
|
||||
// Create details object based on the profile data
|
||||
let details:
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
export function ConnectSDKIcon(props: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={props.className}
|
||||
fill="none"
|
||||
height="24"
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M16 8.00007V4.50073C16 3.66899 16 3.25312 15.8248 2.99755C15.6717 2.77425 15.4346 2.62257 15.1678 2.57715C14.8623 2.52517 14.4847 2.69944 13.7295 3.04799L4.85901 7.14206C4.18551 7.45291 3.84875 7.60834 3.60211 7.84939C3.38406 8.06249 3.21762 8.32262 3.1155 8.6099C3 8.93486 3 9.30575 3 10.0475V15.0001M16.5 14.5001H16.51M3 11.2001L3 17.8001C3 18.9202 3 19.4802 3.21799 19.9081C3.40973 20.2844 3.71569 20.5903 4.09202 20.7821C4.51984 21.0001 5.07989 21.0001 6.2 21.0001H17.8C18.9201 21.0001 19.4802 21.0001 19.908 20.7821C20.2843 20.5903 20.5903 20.2844 20.782 19.9081C21 19.4802 21 18.9202 21 17.8001V11.2001C21 10.08 21 9.51992 20.782 9.09209C20.5903 8.71577 20.2843 8.40981 19.908 8.21806C19.4802 8.00007 18.9201 8.00007 17.8 8.00007L6.2 8.00007C5.0799 8.00007 4.51984 8.00007 4.09202 8.21806C3.7157 8.40981 3.40973 8.71577 3.21799 9.09209C3 9.51991 3 10.08 3 11.2001ZM17 14.5001C17 14.7762 16.7761 15.0001 16.5 15.0001C16.2239 15.0001 16 14.7762 16 14.5001C16 14.2239 16.2239 14.0001 16.5 14.0001C16.7761 14.0001 17 14.2239 17 14.5001Z"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
export function NebulaIcon(props: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={props.className}
|
||||
fill="none"
|
||||
height="24"
|
||||
role="presentation"
|
||||
viewBox="0 0 153 153"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M76.3162 90.8232C82.4613 90.8232 87.4429 85.8416 87.4429 79.6965C87.4429 73.5514 82.4613 68.5698 76.3162 68.5698C70.171 68.5698 65.1895 73.5514 65.1895 79.6965C65.1895 85.8416 70.171 90.8232 76.3162 90.8232Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M91.9481 29.4892C78.7023 32.4916 63.6745 40.7425 50.4663 53.9508C46.781 57.636 43.4817 61.4629 40.5746 65.3532C44.5585 68.8492 47.0735 73.9786 47.0735 79.6955C47.0735 90.2299 38.5336 98.7698 27.9992 98.7698C27.1107 98.7698 26.2364 98.7091 25.3803 98.5915C23.3955 110.414 25.9579 119.384 31.3537 124.78C37.2202 130.646 47.3123 133.164 60.701 130.129C73.9468 127.126 88.9745 118.875 102.183 105.667C105.888 101.962 109.204 98.1131 112.122 94.2007C108.103 90.7037 105.562 85.551 105.562 79.8048C105.562 69.2704 114.102 60.7305 124.636 60.7305C125.536 60.7305 126.421 60.7928 127.288 60.9133C129.237 49.147 126.674 40.2167 121.295 34.8381C115.429 28.9716 105.337 26.4545 91.9481 29.4892ZM16.3791 94.8229C13.4047 109.493 15.9004 122.818 24.608 131.526C42.6147 149.532 80.3662 140.975 108.928 112.413C113.381 107.96 117.348 103.284 120.803 98.494C122.042 98.7466 123.324 98.8792 124.636 98.8792C135.171 98.8792 143.711 90.3393 143.711 79.8048C143.711 73.6582 140.803 68.1906 136.289 64.7023C139.234 50.0687 136.73 36.7817 128.041 28.0925C110.034 10.0859 72.2829 18.6429 43.7206 47.2051C39.2988 51.627 35.3564 56.2691 31.9179 61.024C30.6531 60.7599 29.3423 60.6211 27.9992 60.6211C17.4647 60.6211 8.9248 69.161 8.9248 79.6955C8.9248 85.8567 11.8461 91.3357 16.3791 94.8229ZM124.636 70.2677C119.369 70.2677 115.099 74.5376 115.099 79.8048C115.099 85.0721 119.369 89.342 124.636 89.342C129.904 89.342 134.174 85.0721 134.174 79.8048C134.174 74.5376 129.904 70.2677 124.636 70.2677ZM18.462 79.6955C18.462 74.4282 22.7319 70.1583 27.9992 70.1583C33.2664 70.1583 37.5363 74.4282 37.5363 79.6955C37.5363 84.9627 33.2664 89.2326 27.9992 89.2326C22.7319 89.2326 18.462 84.9627 18.462 79.6955Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
export function ExpoIcon(props: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
width="800px"
|
||||
height="800px"
|
||||
viewBox="0 -12.5 256 256"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
className={props.className}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
d="M121.309004,84.6732585 C123.402504,81.5874152 125.694292,81.1950171 127.553451,81.1950171 C129.41261,81.1950171 132.509843,81.5874152 134.604162,84.6732585 C151.106348,107.339593 178.345607,152.492 198.439108,185.798721 C211.542532,207.519499 221.6069,224.201947 223.671721,226.324944 C231.422996,234.294992 242.053551,229.327949 248.230809,220.287799 C254.312201,211.387762 256.000111,205.138399 256.000111,198.471155 C256.000111,193.930186 167.895315,30.0714244 159.022317,16.4322117 C150.48936,3.31359639 147.710044,0 133.105527,0 L122.176721,0 C107.615631,0 105.511479,3.31359639 96.9777022,16.4322117 C88.1055238,30.0714244 0.0001105152,193.930186 0.0001105152,198.471155 C0.0001105152,205.138399 1.68839227,211.387762 7.76991495,220.287799 C13.9471241,229.327949 24.5775965,234.294992 32.3286259,226.324944 C34.3936934,224.201947 44.4580605,207.519499 57.5616485,185.798721 C77.654822,152.492 104.806818,107.339593 121.309004,84.6732585 Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
export function NextjsIcon(props: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
data-testid="geist-icon"
|
||||
strokeLinejoin="round"
|
||||
style={{
|
||||
color: "currentColor",
|
||||
}}
|
||||
viewBox="3 3 10 10"
|
||||
aria-hidden="true"
|
||||
className={props.className}
|
||||
>
|
||||
<g clipPath="url(#clip0_53_108)">
|
||||
<path
|
||||
d="M10.63 11V5"
|
||||
stroke="url(#paint0_linear_53_108vsxrmxu21)"
|
||||
strokeWidth={1.25}
|
||||
strokeMiterlimit={Math.SQRT2}
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M5.995 5.00087V5H4.745V11H5.995V6.96798L12.3615 14.7076C12.712 14.4793 13.0434 14.2242 13.353 13.9453L5.99527 5.00065L5.995 5.00087Z"
|
||||
fill="url(#paint1_linear_53_108vsxrmxu21)"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_53_108vsxrmxu21"
|
||||
x1={11.13}
|
||||
y1={5}
|
||||
x2={11.13}
|
||||
y2={11}
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="currentColor" />
|
||||
<stop offset={0.609375} stopColor="currentColor" stopOpacity={0.57} />
|
||||
<stop offset={0.796875} stopColor="currentColor" stopOpacity={0} />
|
||||
<stop offset={1} stopColor="currentColor" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_53_108vsxrmxu21"
|
||||
x1={9.9375}
|
||||
y1={9.0625}
|
||||
x2={13.5574}
|
||||
y2={13.3992}
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="currentColor" />
|
||||
<stop offset={1} stopColor="currentColor" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_53_108">
|
||||
<rect width={16} height={16} fill="red" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
export function NodeJSIcon(props: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 32 32"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
className={props.className}
|
||||
>
|
||||
<path
|
||||
d="M17.1725 2.29872C16.4627 1.89953 15.5373 1.90132 14.8269 2.29872C11.2689 4.26227 7.71082 6.22641 4.15216 8.18906C3.45969 8.55335 2.99264 9.29698 3.00009 10.0688V21.9328C2.99509 22.7197 3.48622 23.4705 4.19655 23.8298C5.21871 24.3736 6.2118 24.9726 7.25244 25.4802C8.45451 26.0709 9.95843 26.2015 11.1752 25.5855C12.1629 25.075 12.6016 23.9395 12.6003 22.896C12.6083 18.9806 12.6016 15.0651 12.6034 11.1496C12.6269 10.9756 12.4962 10.7896 12.3064 10.7938C11.8517 10.7866 11.3964 10.7896 10.9417 10.7926C10.7699 10.7764 10.6022 10.9191 10.6152 11.0918C10.6091 14.982 10.6164 18.8734 10.6115 22.7642C10.6214 23.3024 10.2578 23.8196 9.73913 24.0014C8.5412 24.4213 5.12198 22.2012 5.12198 22.2012C4.9965 22.1431 4.91682 22.007 4.92912 21.8718C4.92912 17.9576 4.92973 14.0433 4.92912 10.1297C4.91187 9.97191 5.00912 9.8298 5.15402 9.76538C8.70033 7.8134 12.2448 5.85654 15.7911 3.90336C15.9143 3.82115 16.086 3.8214 16.2089 3.90396C19.7552 5.85654 23.3003 7.81161 26.8472 9.76368C26.9926 9.828 27.0857 9.9725 27.0709 10.1297C27.0703 14.0433 27.0721 17.9576 27.0697 21.8713C27.0802 22.0098 27.0086 22.144 26.8793 22.2048C23.3661 24.1462 19.8129 26.025 16.3315 28.0228C16.1796 28.1099 16.0075 28.2086 15.8373 28.1126C14.9218 27.6062 14.0174 27.0801 13.1049 26.5688C13.0057 26.5069 12.8794 26.4803 12.7759 26.5496C12.3668 26.7652 11.982 26.9398 11.5122 27.1258C10.8524 27.387 10.9578 27.4938 11.5529 27.8405C12.62 28.4444 13.6889 29.0459 14.756 29.6504C15.4585 30.0888 16.4024 30.12 17.1275 29.7149C20.6861 27.7538 24.2436 25.7904 27.8029 23.8293C28.5113 23.468 29.0049 22.7202 28.9999 21.9327V10.0688C29.0068 9.31264 28.5576 8.58227 27.886 8.21259C24.3156 6.23947 20.7435 4.27064 17.1725 2.29872Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M22.5419 11.2062C21.1452 10.459 19.4836 10.4192 17.9315 10.5169C16.8102 10.6277 15.6309 10.9371 14.814 11.7409C13.9761 12.5489 13.7937 13.8537 14.1917 14.9085C14.4769 15.6539 15.1948 16.1386 15.9372 16.395C16.8935 16.7326 17.8979 16.837 18.9026 16.9414C19.819 17.0366 20.7357 17.1319 21.6165 17.4042C21.9763 17.5234 22.3953 17.7058 22.5055 18.0973C22.6073 18.5609 22.4957 19.0998 22.1193 19.4219C20.9237 20.3682 17.5979 20.2232 16.4166 19.4784C15.939 19.1611 15.7332 18.5994 15.6495 18.0641C15.6402 17.8973 15.5059 17.7443 15.3248 17.757C14.8713 17.7516 14.4178 17.7528 13.9643 17.7564C13.8061 17.7431 13.6416 17.8557 13.6329 18.0172C13.5397 20.4689 15.7914 21.5377 17.9039 21.773C19.1108 21.888 20.3442 21.8814 21.5327 21.6224C22.4261 21.419 23.3219 21.0444 23.9369 20.3563C24.6953 19.52 24.8444 18.2749 24.5043 17.2332C24.2443 16.4559 23.5012 15.9573 22.7416 15.7008C21.7086 15.3466 20.4844 15.1562 19.5488 15.0671C18.1889 14.9376 16.5729 14.9905 16.188 14.0969C16.0345 13.629 16.1651 13.048 16.5951 12.7602C17.7328 11.9885 20.0483 12.091 21.2265 12.6675C21.7675 12.9384 22.081 13.4948 22.2104 14.0565C22.2344 14.2215 22.3454 14.3937 22.5364 14.3865C22.9868 14.3955 23.4372 14.3889 23.8875 14.3895C24.0422 14.4003 24.2116 14.313 24.2418 14.1546C24.2227 12.9806 23.6232 11.7788 22.5419 11.2062Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
export function ViteIcon(props: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="-0.5 0 257 257"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
aria-hidden="true"
|
||||
className={props.className}
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
d="M255.152904,37.937763 L134.896865,252.97646 C132.413943,257.416178 126.035075,257.442321 123.5149,253.02417 L0.87443175,37.9584812 C-1.87111536,33.1438084 2.24595371,27.3119153 7.70191187,28.2871109 L128.086639,49.8052023 C128.854587,49.9424525 129.640835,49.9411454 130.408783,49.8012155 L248.276014,28.3179595 C253.713738,27.3268821 257.850198,33.1136134 255.152904,37.937763 Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M185.432401,0.0631038902 L96.4393502,17.500942 C94.9766549,17.7875335 93.8936852,19.0270992 93.8054529,20.5146956 L88.3311293,112.971419 C88.2023755,115.149123 90.2023075,116.839261 92.3277254,116.349082 L117.10466,110.630976 C119.422882,110.096354 121.517582,112.138114 121.041128,114.469407 L113.67994,150.515893 C113.184532,152.941955 115.462232,155.016394 117.831433,154.29681 L133.134834,149.647295 C135.507302,148.927059 137.786963,151.00738 137.285019,153.435402 L125.586724,210.056351 C124.854723,213.598061 129.565674,215.529368 131.530313,212.49287 L132.842687,210.464834 L205.359174,65.745575 C206.573511,63.3224548 204.479465,60.5594769 201.818118,61.0730542 L176.31441,65.9952397 C173.91776,66.4573155 171.878614,64.2253653 172.555061,61.8805431 L189.2009,4.17570253 C189.878001,1.82692623 187.831665,-0.406957894 185.432401,0.0631038902 Z"
|
||||
fill="hsl(var(--background))"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
const PROJECT_WALLET_LABEL_SUFFIX = " Wallet";
|
||||
const PROJECT_WALLET_LABEL_MAX_LENGTH = 32;
|
||||
|
||||
/**
|
||||
* Builds the default label for a project's primary server wallet.
|
||||
* Ensures a stable naming convention so the wallet can be identified across flows.
|
||||
*/
|
||||
export function getProjectWalletLabel(projectName: string | undefined) {
|
||||
const baseName = projectName?.trim() || "Project";
|
||||
const maxBaseLength = Math.max(
|
||||
1,
|
||||
PROJECT_WALLET_LABEL_MAX_LENGTH - PROJECT_WALLET_LABEL_SUFFIX.length,
|
||||
);
|
||||
|
||||
const normalizedBase =
|
||||
baseName.length > maxBaseLength
|
||||
? baseName.slice(0, maxBaseLength).trimEnd()
|
||||
: baseName;
|
||||
|
||||
return `${normalizedBase}${PROJECT_WALLET_LABEL_SUFFIX}`;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import "server-only";
|
||||
|
||||
import { createVaultClient, listEoas } from "@thirdweb-dev/vault-sdk";
|
||||
import type { Project } from "@/api/project/projects";
|
||||
import { NEXT_PUBLIC_THIRDWEB_VAULT_URL } from "@/constants/public-envs";
|
||||
import { getProjectWalletLabel } from "@/lib/project-wallet";
|
||||
|
||||
export type ProjectWalletSummary = {
|
||||
id: string;
|
||||
address: string;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
type VaultWalletListItem = {
|
||||
id: string;
|
||||
address: string;
|
||||
metadata?: {
|
||||
label?: string;
|
||||
projectId?: string;
|
||||
teamId?: string;
|
||||
type?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export async function getProjectWallet(
|
||||
project: Project,
|
||||
): Promise<ProjectWalletSummary | undefined> {
|
||||
const engineCloudService = project.services.find(
|
||||
(service) => service.name === "engineCloud",
|
||||
);
|
||||
|
||||
const managementAccessToken =
|
||||
engineCloudService?.managementAccessToken || undefined;
|
||||
const projectWalletAddress = (
|
||||
engineCloudService as { projectWalletAddress?: string } | undefined
|
||||
)?.projectWalletAddress;
|
||||
|
||||
if (
|
||||
!managementAccessToken ||
|
||||
!NEXT_PUBLIC_THIRDWEB_VAULT_URL ||
|
||||
!projectWalletAddress
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const vaultClient = await createVaultClient({
|
||||
baseUrl: NEXT_PUBLIC_THIRDWEB_VAULT_URL,
|
||||
});
|
||||
|
||||
const response = await listEoas({
|
||||
client: vaultClient,
|
||||
request: {
|
||||
auth: {
|
||||
accessToken: managementAccessToken,
|
||||
},
|
||||
options: {
|
||||
page: 0,
|
||||
// @ts-expect-error - SDK expects snake_case for pagination arguments
|
||||
page_size: 100,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.success || !response.data) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const items = response.data.items as VaultWalletListItem[] | undefined;
|
||||
|
||||
if (!items?.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const defaultLabel = getProjectWalletLabel(project.name);
|
||||
|
||||
const serverWallets = items.filter(
|
||||
(item) => item.metadata?.projectId === project.id,
|
||||
);
|
||||
|
||||
const defaultWallet = serverWallets.find(
|
||||
(item) =>
|
||||
item.address.toLowerCase() === projectWalletAddress.toLowerCase(),
|
||||
);
|
||||
|
||||
if (!defaultWallet) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
id: defaultWallet.id,
|
||||
address: defaultWallet.address,
|
||||
label: defaultWallet.metadata?.label ?? defaultLabel,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Failed to load project wallet", error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -248,7 +248,7 @@ export function teamSubscriptionsStub(
|
||||
// In-App Wallets
|
||||
{
|
||||
amount: usage.inAppWalletAmount?.amount || 0,
|
||||
description: `${usage.inAppWalletAmount?.quantity || 0} x In-App Wallets (Tier 1 at $0.00 / month)`,
|
||||
description: `${usage.inAppWalletAmount?.quantity || 0} x User Wallets (Tier 1 at $0.00 / month)`,
|
||||
thirdwebSku: "usage:in_app_wallet",
|
||||
},
|
||||
// AA Sponsorship
|
||||
|
||||
@@ -23,6 +23,8 @@ export interface UserOpStats {
|
||||
successful: number;
|
||||
failed: number;
|
||||
sponsoredUsd: number;
|
||||
gasUnits: number;
|
||||
avgGasPrice: number;
|
||||
chainId?: string;
|
||||
}
|
||||
|
||||
@@ -94,3 +96,62 @@ export interface AnalyticsQueryParams {
|
||||
period?: "day" | "week" | "month" | "year" | "all";
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface X402SettlementsOverall {
|
||||
date: string;
|
||||
totalRequests: number;
|
||||
totalValue: number;
|
||||
totalValueUSD: number;
|
||||
}
|
||||
|
||||
export interface X402SettlementsByChainId {
|
||||
date: string;
|
||||
chainId: string;
|
||||
totalRequests: number;
|
||||
totalValue: number;
|
||||
totalValueUSD: number;
|
||||
}
|
||||
|
||||
export interface X402SettlementsByPayer {
|
||||
date: string;
|
||||
payer: string;
|
||||
totalRequests: number;
|
||||
totalValue: number;
|
||||
totalValueUSD: number;
|
||||
}
|
||||
|
||||
interface X402SettlementsByReceiver {
|
||||
date: string;
|
||||
receiver: string;
|
||||
totalRequests: number;
|
||||
totalValue: number;
|
||||
totalValueUSD: number;
|
||||
}
|
||||
|
||||
export interface X402SettlementsByResource {
|
||||
date: string;
|
||||
resource: string;
|
||||
totalRequests: number;
|
||||
totalValue: number;
|
||||
totalValueUSD: number;
|
||||
}
|
||||
|
||||
interface X402SettlementsByAsset {
|
||||
date: string;
|
||||
asset: string;
|
||||
totalRequests: number;
|
||||
totalValue: number;
|
||||
totalValueUSD: number;
|
||||
}
|
||||
|
||||
export type X402SettlementStats =
|
||||
| X402SettlementsOverall
|
||||
| X402SettlementsByChainId
|
||||
| X402SettlementsByPayer
|
||||
| X402SettlementsByReceiver
|
||||
| X402SettlementsByResource
|
||||
| X402SettlementsByAsset;
|
||||
|
||||
export interface X402QueryParams extends AnalyticsQueryParams {
|
||||
groupBy?: "overall" | "chainId" | "payer" | "resource" | "asset" | "receiver";
|
||||
}
|
||||
|
||||
@@ -5,6 +5,10 @@ const customClaimAmounts: Record<number, number> = {
|
||||
631571: 0.1,
|
||||
// Sophon testnet
|
||||
531050104: 5,
|
||||
// Arc testnet
|
||||
5042002: 1,
|
||||
// Injective testnet
|
||||
1439: 1,
|
||||
};
|
||||
|
||||
const defaultClaimAmount = 0.01;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const usdCurrencyFormatter = new Intl.NumberFormat("en-US", {
|
||||
currency: "USD",
|
||||
maximumFractionDigits: 2, // prefix with $
|
||||
maximumFractionDigits: 6, // prefix with $
|
||||
minimumFractionDigits: 0, // don't show decimal places if value is a whole number
|
||||
notation: "compact", // at max 2 decimal places
|
||||
roundingMode: "halfEven", // round to nearest even number, standard practice for financial calculations
|
||||
|
||||
@@ -23,7 +23,7 @@ export const TEAM_PLANS: Record<
|
||||
features: [
|
||||
"Email Support",
|
||||
"48hr Guaranteed Response",
|
||||
"Custom In-App Wallet Auth",
|
||||
"Custom User Wallet Auth",
|
||||
],
|
||||
price: 99,
|
||||
subTitle: "Everything in Starter, plus:",
|
||||
|
||||
@@ -21,8 +21,8 @@ export function getSDKTheme(theme: "light" | "dark"): Theme {
|
||||
primaryButtonText: "hsl(var(--inverted-foreground))",
|
||||
primaryText: "hsl(var(--foreground))",
|
||||
scrollbarBg: "hsl(var(--muted))",
|
||||
secondaryButtonBg: "hsl(var(--secondary))",
|
||||
secondaryButtonHoverBg: "hsl(var(--secondary)/80%)",
|
||||
secondaryButtonBg: "hsl(var(--secondary)/70%)",
|
||||
secondaryButtonHoverBg: "hsl(var(--secondary))",
|
||||
secondaryButtonText: "hsl(var(--secondary-foreground))",
|
||||
secondaryIconColor: "hsl(var(--secondary-foreground))",
|
||||
secondaryIconHoverBg: "hsl(var(--accent))",
|
||||
@@ -33,7 +33,7 @@ export function getSDKTheme(theme: "light" | "dark"): Theme {
|
||||
separatorLine: "hsl(var(--border))",
|
||||
skeletonBg: "hsl(var(--secondary-foreground)/15%)",
|
||||
success: "hsl(var(--success-text))",
|
||||
tertiaryBg: "hsl(var(--muted)/30%)",
|
||||
tertiaryBg: "hsl(var(--muted)/50%)",
|
||||
tooltipBg: "hsl(var(--popover))",
|
||||
tooltipText: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
|
||||
+14
-6
@@ -1,18 +1,26 @@
|
||||
"use client";
|
||||
import { NATIVE_TOKEN_ADDRESS } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { BuyAndSwapEmbed } from "@/components/blocks/BuyAndSwapEmbed";
|
||||
import { GridPatternEmbedContainer } from "@/components/blocks/grid-pattern-embed-container";
|
||||
import { defineDashboardChain } from "@/lib/defineDashboardChain";
|
||||
|
||||
export function BuyFundsSection(props: { chain: ChainMetadata }) {
|
||||
return (
|
||||
<GridPatternEmbedContainer>
|
||||
<BuyAndSwapEmbed
|
||||
isTestnet={props.chain.testnet}
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
chain={defineDashboardChain(props.chain.chainId, props.chain)}
|
||||
buyAmount={undefined}
|
||||
tokenAddress={undefined}
|
||||
swapTab={{
|
||||
sellToken: {
|
||||
chainId: props.chain.chainId,
|
||||
tokenAddress: NATIVE_TOKEN_ADDRESS,
|
||||
},
|
||||
buyToken: undefined,
|
||||
}}
|
||||
buyTab={{
|
||||
buyToken: {
|
||||
chainId: props.chain.chainId,
|
||||
tokenAddress: NATIVE_TOKEN_ADDRESS,
|
||||
},
|
||||
}}
|
||||
pageType="chain"
|
||||
/>
|
||||
</GridPatternEmbedContainer>
|
||||
|
||||
+15
-4
@@ -3,6 +3,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Turnstile } from "@marsidev/react-turnstile";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { CanClaimResponseType } from "app/(app)/api/testnet-faucet/can-claim/CanClaimResponseType";
|
||||
import { ArrowUpRightIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
@@ -192,6 +193,20 @@ export function FaucetButton({
|
||||
|
||||
// Can not claim
|
||||
if (canClaimFaucetQuery.data && canClaimFaucetQuery.data.canClaim === false) {
|
||||
if (canClaimFaucetQuery.data.type === "paid-plan-required") {
|
||||
return (
|
||||
<Button
|
||||
asChild
|
||||
className="w-full gap-2 whitespace-pre-wrap h-auto min-h-10 text-center bg-background"
|
||||
variant="outline"
|
||||
>
|
||||
<Link href="/team/~/~/billing">
|
||||
Faucet is not available on Free plan. Upgrade your plan to continue
|
||||
<ArrowUpRightIcon className="size-4 shrink-0 text-muted-foreground" />
|
||||
</Link>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Button className="!opacity-100 w-full " disabled variant="outline">
|
||||
{canClaimFaucetQuery.data.type === "throttle" && (
|
||||
@@ -203,10 +218,6 @@ export function FaucetButton({
|
||||
|
||||
{canClaimFaucetQuery.data.type === "unsupported-chain" &&
|
||||
"Faucet is empty right now"}
|
||||
|
||||
{/* TODO: add an upsell path here to subscribe to one of these plans */}
|
||||
{canClaimFaucetQuery.data.type === "paid-plan-required" &&
|
||||
"Faucet is only available on Starter, Growth, Scale and Pro plans."}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
+96
-39
@@ -1,55 +1,112 @@
|
||||
import { BotIcon, CodeIcon, CoinsIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import type { ChainMetadataWithServices } from "@/types/chain";
|
||||
import { products } from "../../../../components/server/products";
|
||||
import { useMemo } from "react";
|
||||
import { BridgeIcon } from "@/icons/BridgeIcon";
|
||||
import { PayIcon } from "@/icons/PayIcon";
|
||||
import { WalletProductIcon } from "@/icons/WalletProductIcon";
|
||||
import type {
|
||||
ChainMetadataWithServices,
|
||||
ChainSupportedService,
|
||||
} from "@/types/chain";
|
||||
import { SectionTitle } from "./SectionTitle";
|
||||
|
||||
export function SupportedProductsSection(props: {
|
||||
services: ChainMetadataWithServices["services"];
|
||||
}) {
|
||||
const enabledProducts = products.filter((product) => {
|
||||
return props.services.find(
|
||||
(service) => service.service === product.id && service.enabled,
|
||||
const enabledServices = useMemo(() => {
|
||||
return props.services.reduce(
|
||||
(acc, service) => {
|
||||
acc[service.service] = service.enabled;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<ChainSupportedService, boolean>,
|
||||
);
|
||||
});
|
||||
|
||||
if (enabledProducts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
}, [props.services]);
|
||||
|
||||
return (
|
||||
<section>
|
||||
<SectionTitle title="thirdweb Products" />
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
|
||||
{enabledProducts.map((product) => {
|
||||
return (
|
||||
<div
|
||||
className="relative rounded-xl border bg-card p-4 hover:border-active-border"
|
||||
key={product.id}
|
||||
>
|
||||
<div className="flex mb-4">
|
||||
<div className="p-2 rounded-full border bg-background">
|
||||
<product.icon className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="mb-1">
|
||||
<Link
|
||||
className="before:absolute before:inset-0 text-base font-medium"
|
||||
href={product.link}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{product.name}
|
||||
</Link>
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{product.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{enabledServices["connect-sdk"] && (
|
||||
<ProductCard
|
||||
icon={WalletProductIcon}
|
||||
name="Wallets"
|
||||
description="Create wallets to read and transact"
|
||||
link="https://thirdweb.com/wallets"
|
||||
/>
|
||||
)}
|
||||
|
||||
{enabledServices["account-abstraction"] && (
|
||||
<ProductCard
|
||||
icon={PayIcon}
|
||||
name="x402"
|
||||
description="Create internet native payments with x402"
|
||||
link="https://thirdweb.com/x402"
|
||||
/>
|
||||
)}
|
||||
|
||||
{enabledServices.pay && (
|
||||
<ProductCard
|
||||
icon={BridgeIcon}
|
||||
name="Bridge"
|
||||
description="Bridge and swap tokens across chains"
|
||||
link="https://thirdweb.com/monetize/bridge"
|
||||
/>
|
||||
)}
|
||||
|
||||
{enabledServices.contracts && (
|
||||
<ProductCard
|
||||
icon={CoinsIcon}
|
||||
name="Tokens"
|
||||
description="Launch tokens and markets"
|
||||
link="https://thirdweb.com/token"
|
||||
/>
|
||||
)}
|
||||
|
||||
<ProductCard
|
||||
icon={BotIcon}
|
||||
name="AI"
|
||||
description="Read and write onchain via natural language"
|
||||
link="https://thirdweb.com/ai"
|
||||
/>
|
||||
|
||||
<ProductCard
|
||||
icon={CodeIcon}
|
||||
name="HTTP API"
|
||||
description="Build products with our HTTP API"
|
||||
link="https://portal.thirdweb.com/reference"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function ProductCard(props: {
|
||||
icon: React.FC<{ className?: string }>;
|
||||
link: string;
|
||||
name: string;
|
||||
description: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="relative rounded-xl border bg-card p-4 hover:border-active-border">
|
||||
<div className="flex mb-4">
|
||||
<div className="p-2 rounded-full border bg-background">
|
||||
<props.icon className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="mb-1">
|
||||
<Link
|
||||
className="before:absolute before:inset-0 text-base font-medium"
|
||||
href={props.link}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{props.name}
|
||||
</Link>
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-sm">{props.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CircleAlertIcon } from "lucide-react";
|
||||
import { getRawAccount } from "@/api/account/get-account";
|
||||
import { FaqSection } from "@/components/blocks/faq-section";
|
||||
import { FaqAccordion } from "@/components/blocks/faq-section";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { getChain, getCustomChainMetadata } from "../../utils";
|
||||
import { fetchChainSeo } from "./apis/chain-seo";
|
||||
@@ -67,10 +67,21 @@ export default async function Page(props: Props) {
|
||||
)}
|
||||
|
||||
{chainSeo?.faqs && chainSeo.faqs.length > 0 && (
|
||||
<div className="py-10">
|
||||
<FaqSection faqs={chainSeo.faqs} />
|
||||
</div>
|
||||
<FaqSection faqs={chainSeo.faqs} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FaqSection(props: {
|
||||
faqs: Array<{ title: string; description: string }>;
|
||||
}) {
|
||||
return (
|
||||
<section className="py-10">
|
||||
<h2 className="text-2xl md:text-3xl font-semibold mb-4 tracking-tight">
|
||||
Frequently asked questions
|
||||
</h2>
|
||||
<FaqAccordion faqs={props.faqs} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
+2
-3
@@ -10,8 +10,7 @@ import * as ERC20Ext from "thirdweb/extensions/erc20";
|
||||
import * as ERC721Ext from "thirdweb/extensions/erc721";
|
||||
import * as ERC1155Ext from "thirdweb/extensions/erc1155";
|
||||
import { download } from "thirdweb/storage";
|
||||
import type { OverrideEntry } from "thirdweb/utils";
|
||||
import { maxUint256 } from "thirdweb/utils";
|
||||
import { isRecord, maxUint256, type OverrideEntry } from "thirdweb/utils";
|
||||
import type { z } from "zod";
|
||||
import type {
|
||||
ClaimCondition as LegacyClaimCondition,
|
||||
@@ -86,7 +85,7 @@ export async function getClaimPhasesInLegacyFormat(
|
||||
]);
|
||||
const snapshot = await fetchSnapshot(
|
||||
condition.merkleRoot,
|
||||
contractMetadata.merkle,
|
||||
isRecord(contractMetadata.merkle) ? contractMetadata.merkle : {},
|
||||
options.contract.client,
|
||||
);
|
||||
return {
|
||||
|
||||
+1
-1
@@ -2,6 +2,7 @@ import type { ThirdwebContract } from "thirdweb";
|
||||
import { contractType as getContractType } from "thirdweb/extensions/thirdweb";
|
||||
import { getDeployedEntrypointERC20 } from "thirdweb/tokens";
|
||||
import { resolveFunctionSelectors } from "@/lib/selectors";
|
||||
import { supportedERCs } from "@/utils/supportedERCs";
|
||||
import { getValidReward } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/utils/rewards";
|
||||
import {
|
||||
isERC20ClaimConditionsSupported,
|
||||
@@ -21,7 +22,6 @@ import {
|
||||
isPermissionsEnumerableSupported,
|
||||
isPermissionsSupported,
|
||||
} from "./detectedFeatures/permissions";
|
||||
import { supportedERCs } from "./detectedFeatures/supportedERCs";
|
||||
import { type EmbedTypeToShow, getEmbedTypeToShow } from "./getEmbedTypeToShow";
|
||||
|
||||
type ContractPageMetadata = {
|
||||
|
||||
+1
-1
@@ -1,9 +1,9 @@
|
||||
import { supportedERCs } from "@/utils/supportedERCs";
|
||||
import { isClaimConditionsSupported } from "./detectedFeatures/claimConditions";
|
||||
import {
|
||||
isDirectListingSupported,
|
||||
isEnglishAuctionSupported,
|
||||
} from "./detectedFeatures/marketplace";
|
||||
import { supportedERCs } from "./detectedFeatures/supportedERCs";
|
||||
|
||||
export type EmbedTypeToShow =
|
||||
| "marketplace-v3"
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import type { ThirdwebContract } from "thirdweb";
|
||||
import { isGetNFTsSupported as ERC721_isGetNFTsSupported } from "thirdweb/extensions/erc721";
|
||||
import { isGetNFTsSupported as ERC1155_isGetNFTsSupported } from "thirdweb/extensions/erc1155";
|
||||
import { resolveFunctionSelectors } from "@/lib/selectors";
|
||||
import { supportedERCs } from "./detectedFeatures/supportedERCs";
|
||||
import { supportedERCs } from "@/utils/supportedERCs";
|
||||
|
||||
export type NewPublicPageType = "erc20" | "erc1155" | "erc721";
|
||||
|
||||
|
||||
+16
-9
@@ -225,6 +225,10 @@ export const InstallModuleForm = (props: InstallModuleFormProps) => {
|
||||
moduleInfo: {
|
||||
bytecodeUri: selectedModule.metadata.bytecodeUri,
|
||||
},
|
||||
isStylus:
|
||||
(selectedModule.metadata.compilers?.stylus &&
|
||||
selectedModule.metadata.compilers?.stylus?.length > 0) ||
|
||||
false,
|
||||
});
|
||||
},
|
||||
queryKey: [
|
||||
@@ -432,6 +436,7 @@ async function isModuleCompatible(options: {
|
||||
moduleInfo: {
|
||||
bytecodeUri: string;
|
||||
};
|
||||
isStylus: boolean;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
// 1. get module's bytecode
|
||||
@@ -444,15 +449,17 @@ async function isModuleCompatible(options: {
|
||||
|
||||
// 2. check compatibility with core and installed modules
|
||||
try {
|
||||
const isCompatible = await checkModulesCompatibility({
|
||||
chain: options.contractInfo.chain,
|
||||
client: options.client,
|
||||
coreBytecode: options.contractInfo.bytecode,
|
||||
moduleBytecodes: [
|
||||
moduleBytecode,
|
||||
...options.contractInfo.installedModuleBytecodes,
|
||||
],
|
||||
});
|
||||
const isCompatible =
|
||||
options.isStylus ||
|
||||
(await checkModulesCompatibility({
|
||||
chain: options.contractInfo.chain,
|
||||
client: options.client,
|
||||
coreBytecode: options.contractInfo.bytecode,
|
||||
moduleBytecodes: [
|
||||
moduleBytecode,
|
||||
...options.contractInfo.installedModuleBytecodes,
|
||||
],
|
||||
}));
|
||||
|
||||
return isCompatible;
|
||||
} catch (e) {
|
||||
|
||||
+21
-10
@@ -2,7 +2,7 @@
|
||||
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { Suspense, useState } from "react";
|
||||
import type { NFT, ThirdwebClient, ThirdwebContract } from "thirdweb";
|
||||
import { getNFT as getErc721NFT } from "thirdweb/extensions/erc721";
|
||||
import { getNFT as getErc1155NFT } from "thirdweb/extensions/erc1155";
|
||||
@@ -89,7 +89,7 @@ export const TokenIdPage: React.FC<TokenIdPageProps> = ({
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="flex h-[400px] items-center justify-center">
|
||||
<div className="flex h-[400px] items-center justify-center py-20">
|
||||
<Spinner className="size-10" />
|
||||
</div>
|
||||
);
|
||||
@@ -179,15 +179,26 @@ export const TokenIdPage: React.FC<TokenIdPageProps> = ({
|
||||
<NFTDetailsTab client={contract.client} nft={nft} />
|
||||
)}
|
||||
|
||||
{tabs.map((tb) => {
|
||||
return (
|
||||
tb.title === tab && (
|
||||
<div className="w-full" key={tb.title}>
|
||||
{tb.children}
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="h-full">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Spinner className="size-4" />
|
||||
<p className="text-sm text-muted-foreground"> Loading </p>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{tabs.map((tb) => {
|
||||
return (
|
||||
tb.title === tab && (
|
||||
<div className="w-full" key={tb.title}>
|
||||
{tb.children}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+4
-1
@@ -47,7 +47,10 @@ export default async function Image({
|
||||
chainName: info.chainMetadata.name,
|
||||
contractAddress: info.serverContract.address,
|
||||
displayName: contractDisplayName,
|
||||
logo: contractMetadata.image,
|
||||
logo:
|
||||
contractMetadata.image && typeof contractMetadata.image === "string"
|
||||
? contractMetadata.image
|
||||
: undefined,
|
||||
});
|
||||
} catch {
|
||||
return contractOGImageTemplate({
|
||||
|
||||
+5
-5
@@ -2,6 +2,8 @@
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { ConnectButton } from "thirdweb/react";
|
||||
import type { Wallet } from "thirdweb/wallets";
|
||||
import { appMetadata } from "@/constants/connect";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { useAllChainsData } from "@/hooks/chains/allChains";
|
||||
import { getSDKTheme } from "@/utils/sdk-component-theme";
|
||||
@@ -10,6 +12,7 @@ const client = getClientThirdwebClient();
|
||||
|
||||
export function PublicPageConnectButton(props: {
|
||||
connectButtonClassName?: string;
|
||||
wallets?: Wallet[];
|
||||
}) {
|
||||
const { theme } = useTheme();
|
||||
const t = theme === "light" ? "light" : "dark";
|
||||
@@ -17,11 +20,7 @@ export function PublicPageConnectButton(props: {
|
||||
|
||||
return (
|
||||
<ConnectButton
|
||||
appMetadata={{
|
||||
logoUrl: "https://thirdweb.com/favicon.ico",
|
||||
name: "thirdweb",
|
||||
url: "https://thirdweb.com",
|
||||
}}
|
||||
appMetadata={appMetadata}
|
||||
autoConnect={false}
|
||||
chains={allChainsV5}
|
||||
client={client}
|
||||
@@ -38,6 +37,7 @@ export function PublicPageConnectButton(props: {
|
||||
}}
|
||||
// we have an AutoConnect already added in root layout with AA configuration
|
||||
theme={getSDKTheme(t)}
|
||||
wallets={props.wallets}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+12
-1
@@ -1,4 +1,5 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getUnixTime, subDays } from "date-fns";
|
||||
import { isProd } from "@/constants/env-utils";
|
||||
import { NEXT_PUBLIC_DASHBOARD_CLIENT_ID } from "@/constants/public-envs";
|
||||
|
||||
@@ -35,6 +36,11 @@ export function useTokenTransfers(params: {
|
||||
url.searchParams.set("page", params.page.toString());
|
||||
url.searchParams.set("limit", params.limit.toString());
|
||||
url.searchParams.set("clientId", NEXT_PUBLIC_DASHBOARD_CLIENT_ID);
|
||||
const THIRTY_DAYS_AGO = subDays(new Date(), 30);
|
||||
url.searchParams.set(
|
||||
"block_timestamp_from",
|
||||
getUnixTime(THIRTY_DAYS_AGO).toString(),
|
||||
);
|
||||
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) {
|
||||
@@ -46,7 +52,12 @@ export function useTokenTransfers(params: {
|
||||
return data;
|
||||
},
|
||||
queryKey: ["token-transfers", params],
|
||||
refetchInterval: 5000,
|
||||
refetchInterval: (data) => {
|
||||
if (data?.state.error) {
|
||||
return false;
|
||||
}
|
||||
return 5000;
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
retry: false,
|
||||
retryOnMount: false,
|
||||
|
||||
+60
-28
@@ -89,16 +89,31 @@ export async function ERC20PublicPage(props: {
|
||||
chainMetadata={props.chainMetadata}
|
||||
clientContract={props.clientContract}
|
||||
contractCreator={contractCreator}
|
||||
image={contractMetadata.image}
|
||||
image={
|
||||
contractMetadata.image &&
|
||||
typeof contractMetadata.image === "string"
|
||||
? contractMetadata.image
|
||||
: undefined
|
||||
}
|
||||
isDashboardUser={isDashboardUser}
|
||||
name={contractMetadata.name}
|
||||
name={
|
||||
contractMetadata.name && typeof contractMetadata.name === "string"
|
||||
? contractMetadata.name
|
||||
: // if we do not have a contract name fall back to the address
|
||||
props.clientContract.address
|
||||
}
|
||||
socialUrls={
|
||||
typeof contractMetadata.social_urls === "object" &&
|
||||
contractMetadata.social_urls !== null
|
||||
? contractMetadata.social_urls
|
||||
: {}
|
||||
}
|
||||
symbol={contractMetadata.symbol}
|
||||
symbol={
|
||||
contractMetadata.symbol &&
|
||||
typeof contractMetadata.symbol === "string"
|
||||
? contractMetadata.symbol
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -143,21 +158,25 @@ export async function ERC20PublicPage(props: {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showBuyEmbed && (
|
||||
<div className="container max-w-7xl pb-10">
|
||||
<GridPatternEmbedContainer>
|
||||
<BuyEmbed
|
||||
chainMetadata={props.chainMetadata}
|
||||
claimConditionMeta={claimConditionMeta}
|
||||
clientContract={props.clientContract}
|
||||
tokenAddress={props.clientContract.address}
|
||||
tokenDecimals={tokenDecimals}
|
||||
tokenName={contractMetadata.name}
|
||||
tokenSymbol={contractMetadata.symbol}
|
||||
/>
|
||||
</GridPatternEmbedContainer>
|
||||
</div>
|
||||
)}
|
||||
{showBuyEmbed &&
|
||||
contractMetadata.name &&
|
||||
typeof contractMetadata.name === "string" &&
|
||||
contractMetadata.symbol &&
|
||||
typeof contractMetadata.symbol === "string" && (
|
||||
<div className="container max-w-7xl pb-10">
|
||||
<GridPatternEmbedContainer>
|
||||
<BuyEmbed
|
||||
chainMetadata={props.chainMetadata}
|
||||
claimConditionMeta={claimConditionMeta}
|
||||
clientContract={props.clientContract}
|
||||
tokenAddress={props.clientContract.address}
|
||||
tokenDecimals={tokenDecimals}
|
||||
tokenName={contractMetadata.name}
|
||||
tokenSymbol={contractMetadata.symbol}
|
||||
/>
|
||||
</GridPatternEmbedContainer>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="container flex max-w-7xl grow flex-col pb-10">
|
||||
<div className="flex grow flex-col gap-8">
|
||||
@@ -167,12 +186,15 @@ export async function ERC20PublicPage(props: {
|
||||
contractAddress={props.clientContract.address}
|
||||
/>
|
||||
|
||||
<RecentTransfers
|
||||
chainMetadata={props.chainMetadata}
|
||||
clientContract={props.clientContract}
|
||||
decimals={tokenDecimals}
|
||||
tokenSymbol={contractMetadata.symbol}
|
||||
/>
|
||||
{contractMetadata.symbol &&
|
||||
typeof contractMetadata.symbol === "string" && (
|
||||
<RecentTransfers
|
||||
chainMetadata={props.chainMetadata}
|
||||
clientContract={props.clientContract}
|
||||
decimals={tokenDecimals}
|
||||
tokenSymbol={contractMetadata.symbol}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -199,11 +221,21 @@ function BuyEmbed(props: {
|
||||
if (!props.claimConditionMeta) {
|
||||
return (
|
||||
<BuyAndSwapEmbed
|
||||
chain={props.clientContract.chain}
|
||||
tokenAddress={props.clientContract.address}
|
||||
buyAmount={undefined}
|
||||
// chain={props.clientContract.chain}
|
||||
swapTab={{
|
||||
sellToken: {
|
||||
chainId: props.clientContract.chain.id,
|
||||
tokenAddress: props.clientContract.address,
|
||||
},
|
||||
buyToken: undefined,
|
||||
}}
|
||||
buyTab={{
|
||||
buyToken: {
|
||||
chainId: props.clientContract.chain.id,
|
||||
tokenAddress: props.clientContract.address,
|
||||
},
|
||||
}}
|
||||
pageType="asset"
|
||||
isTestnet={props.chainMetadata.testnet}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+10
-3
@@ -53,6 +53,13 @@ export async function NFTPublicPage(props: {
|
||||
const _isTokenByIndexSupported =
|
||||
props.type === "erc721" && isTokenByIndexSupported(functionSelectors);
|
||||
|
||||
// FIXME: this is technically a bad fallback but we gotta do what we gotta do
|
||||
const contractMetadataWithNameAndSymbolFallback = {
|
||||
...contractMetadata,
|
||||
// fall back to the contract address if the name is not set
|
||||
name: contractMetadata.name || props.clientContract.address,
|
||||
symbol: contractMetadata.symbol || "",
|
||||
};
|
||||
const buyNFTDropCard = nftDropClaimParams ? (
|
||||
<BuyNFTDropCardServer
|
||||
chainMetadata={props.chainMetadata}
|
||||
@@ -73,7 +80,7 @@ export async function NFTPublicPage(props: {
|
||||
<NFTsGrid
|
||||
chainMetadata={props.chainMetadata}
|
||||
clientContract={props.clientContract}
|
||||
collectionMetadata={contractMetadata}
|
||||
collectionMetadata={contractMetadataWithNameAndSymbolFallback}
|
||||
gridClassName={
|
||||
buyNFTDropCard
|
||||
? "grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
|
||||
@@ -110,7 +117,7 @@ export async function NFTPublicPage(props: {
|
||||
chainMetadata={props.chainMetadata}
|
||||
clientContract={props.clientContract}
|
||||
contractCreator={contractCreator}
|
||||
contractMetadata={contractMetadata}
|
||||
contractMetadata={contractMetadataWithNameAndSymbolFallback}
|
||||
isDashboardUser={isDashboardUser}
|
||||
>
|
||||
<ResponsiveLayout
|
||||
@@ -138,7 +145,7 @@ export async function NFTPublicPage(props: {
|
||||
<PageLoadTokenViewerSheet
|
||||
chainMetadata={props.chainMetadata}
|
||||
clientContract={props.clientContract}
|
||||
collectionMetadata={contractMetadata}
|
||||
collectionMetadata={contractMetadataWithNameAndSymbolFallback}
|
||||
tokenByIndexSupported={_isTokenByIndexSupported}
|
||||
tokenId={BigInt(props.tokenId)}
|
||||
type={props.type}
|
||||
|
||||
+8
-3
@@ -37,7 +37,7 @@ export function TokenViewerSheet(
|
||||
clientContract: ThirdwebContract;
|
||||
chainMetadata: ChainMetadata;
|
||||
collectionMetadata: {
|
||||
name: string;
|
||||
name: string | null;
|
||||
image?: string;
|
||||
};
|
||||
type: "erc1155" | "erc721";
|
||||
@@ -57,6 +57,11 @@ export function TokenViewerSheet(
|
||||
) {
|
||||
const tokenId = props.variant === "fetch-data" ? props.tokenId : props.nft.id;
|
||||
|
||||
const collectionMetadataWithNameFallback = {
|
||||
...props.collectionMetadata,
|
||||
name: props.collectionMetadata.name || props.clientContract.address,
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
onOpenChange={(open) => {
|
||||
@@ -78,7 +83,7 @@ export function TokenViewerSheet(
|
||||
<FetchAndRenderTokenInfo
|
||||
chainMetadata={props.chainMetadata}
|
||||
clientContract={props.clientContract}
|
||||
collectionMetadata={props.collectionMetadata}
|
||||
collectionMetadata={collectionMetadataWithNameFallback}
|
||||
tokenByIndexSupported={props.tokenByIndexSupported}
|
||||
tokenId={props.tokenId}
|
||||
type={props.type}
|
||||
@@ -86,7 +91,7 @@ export function TokenViewerSheet(
|
||||
) : (
|
||||
<TokenInfoUI
|
||||
chainMetadata={props.chainMetadata}
|
||||
collectionMetadata={props.collectionMetadata}
|
||||
collectionMetadata={collectionMetadataWithNameFallback}
|
||||
contract={props.clientContract}
|
||||
data={props.nft}
|
||||
tokenId={tokenId}
|
||||
|
||||
+4
-1
@@ -84,7 +84,10 @@ export const SettingsMetadata = ({
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
let image: string | undefined = metadata.data?.image;
|
||||
let image: string | undefined =
|
||||
metadata.data?.image && typeof metadata.data.image === "string"
|
||||
? metadata.data.image
|
||||
: undefined;
|
||||
try {
|
||||
image = image
|
||||
? // eslint-disable-next-line no-restricted-syntax
|
||||
|
||||
+1
@@ -137,6 +137,7 @@ export function SettingsPlatformFees({
|
||||
onChange={(value) =>
|
||||
form.setValue("platform_fee_basis_points", value, {
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
value={form.watch("platform_fee_basis_points")}
|
||||
|
||||
+1
@@ -151,6 +151,7 @@ export function SettingsRoyalties({
|
||||
onChange={(value) =>
|
||||
form.setValue("seller_fee_basis_points", value, {
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
value={form.watch("seller_fee_basis_points")}
|
||||
|
||||
+1
-1
@@ -7,6 +7,7 @@ import { getTeams } from "@/api/team/get-team";
|
||||
import type { MinimalTeamsAndProjects } from "@/components/contracts/import-contract/types";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { resolveFunctionSelectors } from "@/lib/selectors";
|
||||
import { supportedERCs } from "@/utils/supportedERCs";
|
||||
import { shortenIfAddress } from "@/utils/usedapp-external";
|
||||
import type { ProjectMeta } from "../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import { TeamHeader } from "../../../../team/components/TeamHeader/team-header";
|
||||
@@ -14,7 +15,6 @@ import { ConfigureCustomChain } from "./_layout/ConfigureCustomChain";
|
||||
import { getContractMetadataHeaderData } from "./_layout/contract-metadata";
|
||||
import { ContractPageLayout } from "./_layout/contract-page-layout";
|
||||
import { ContractPageLayoutClient } from "./_layout/contract-page-layout.client";
|
||||
import { supportedERCs } from "./_utils/detectedFeatures/supportedERCs";
|
||||
import { getContractPageParamsInfo } from "./_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "./_utils/getContractPageMetadata";
|
||||
import { getContractPageSidebarLinks } from "./_utils/getContractPageSidebarLinks";
|
||||
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
import type { Meta, StoryObj } from "@storybook/nextjs";
|
||||
import type { Status } from "thirdweb/bridge";
|
||||
import { storybookThirdwebClient } from "@/storybook/utils";
|
||||
import { BridgeStatus } from "./bridge-status";
|
||||
|
||||
const meta = {
|
||||
component: BridgeStatus,
|
||||
title: "chain/tx/bridge-status",
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="container max-w-6xl py-10">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
} satisfies Meta<typeof BridgeStatus>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
const completedStatus: Status = {
|
||||
status: "COMPLETED",
|
||||
paymentId:
|
||||
"0x8e0a0d9c0254a75a3004a28e0fb69bae7f2d659938ddd461753627270930db60",
|
||||
originAmount: 129935n,
|
||||
destinationAmount: 100000n,
|
||||
transactions: [
|
||||
{
|
||||
chainId: 42161,
|
||||
transactionHash:
|
||||
"0x37daf698878e73590adb2d6f833e810304a9981829ad983fb355245af1183107",
|
||||
},
|
||||
{
|
||||
chainId: 8453,
|
||||
transactionHash:
|
||||
"0xc72abba2761bb48f605125dc12c17b15088303db960816fa12316f0c90796417",
|
||||
},
|
||||
],
|
||||
originChainId: 42161,
|
||||
originTokenAddress: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
|
||||
originToken: {
|
||||
chainId: 42161,
|
||||
address: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
|
||||
symbol: "USDC",
|
||||
name: "USD Coin",
|
||||
decimals: 6,
|
||||
iconUri: "https://ethereum-optimism.github.io/data/USDC/logo.png",
|
||||
},
|
||||
destinationChainId: 8453,
|
||||
destinationTokenAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
||||
destinationToken: {
|
||||
chainId: 8453,
|
||||
address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
||||
symbol: "USDC",
|
||||
name: "USD Coin",
|
||||
decimals: 6,
|
||||
iconUri:
|
||||
"https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694",
|
||||
},
|
||||
sender: "0x2a4f24F935Eb178e3e7BA9B53A5Ee6d8407C0709",
|
||||
receiver: "0x2a4f24F935Eb178e3e7BA9B53A5Ee6d8407C0709",
|
||||
};
|
||||
|
||||
const pendingStatus: Status = {
|
||||
...completedStatus,
|
||||
status: "PENDING",
|
||||
};
|
||||
|
||||
const failedStatus: Status = {
|
||||
...completedStatus,
|
||||
status: "FAILED",
|
||||
};
|
||||
|
||||
const notFoundStatus: Status = {
|
||||
...completedStatus,
|
||||
status: "NOT_FOUND",
|
||||
transactions: [],
|
||||
};
|
||||
|
||||
export const Completed: Story = {
|
||||
args: {
|
||||
bridgeStatus: completedStatus,
|
||||
client: storybookThirdwebClient,
|
||||
},
|
||||
};
|
||||
|
||||
export const Pending: Story = {
|
||||
args: {
|
||||
bridgeStatus: pendingStatus,
|
||||
client: storybookThirdwebClient,
|
||||
},
|
||||
};
|
||||
|
||||
export const Failed: Story = {
|
||||
args: {
|
||||
bridgeStatus: failedStatus,
|
||||
client: storybookThirdwebClient,
|
||||
},
|
||||
};
|
||||
|
||||
export const NotFound: Story = {
|
||||
args: {
|
||||
bridgeStatus: notFoundStatus,
|
||||
client: storybookThirdwebClient,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithPurchaseData: Story = {
|
||||
args: {
|
||||
bridgeStatus: {
|
||||
...completedStatus,
|
||||
purchaseData: {
|
||||
userId: "68d645b7ded999651272bf1e",
|
||||
credits: 32000,
|
||||
transactionId: "fd2606d1-90df-45c6-bd2c-b19a34764a31",
|
||||
},
|
||||
},
|
||||
client: storybookThirdwebClient,
|
||||
},
|
||||
};
|
||||
+419
@@ -0,0 +1,419 @@
|
||||
"use client";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { CodeClient } from "@workspace/ui/components/code/code.client";
|
||||
import { Img } from "@workspace/ui/components/img";
|
||||
import { Spinner } from "@workspace/ui/components/spinner";
|
||||
import {
|
||||
ArrowRightIcon,
|
||||
ArrowUpRightIcon,
|
||||
CircleCheckIcon,
|
||||
CircleXIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
getAddress,
|
||||
NATIVE_TOKEN_ADDRESS,
|
||||
type ThirdwebClient,
|
||||
} from "thirdweb";
|
||||
import type { Status, Token } from "thirdweb/bridge";
|
||||
import { status } from "thirdweb/bridge";
|
||||
import { toTokens } from "thirdweb/utils";
|
||||
import { WalletAddress } from "@/components/blocks/wallet-address";
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { LinkWithCopyButton } from "@/components/ui/link-with-copy-button";
|
||||
import { SkeletonContainer } from "@/components/ui/skeleton";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { fetchChain } from "@/utils/fetchChain";
|
||||
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
|
||||
|
||||
type PurchaseData = Exclude<Status, { status: "NOT_FOUND" }>["purchaseData"];
|
||||
|
||||
export function BridgeStatus(props: {
|
||||
bridgeStatus: Status;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
const { bridgeStatus } = props;
|
||||
const purchaseDataString =
|
||||
bridgeStatus.status !== "NOT_FOUND" && bridgeStatus.purchaseData
|
||||
? getPurchaseData(bridgeStatus.purchaseData)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div className="bg-card rounded-xl border relative">
|
||||
{bridgeStatus.status === "FAILED" &&
|
||||
bridgeStatus.transactions.length > 0 && (
|
||||
<FailedBridgeStatusContent
|
||||
transactions={bridgeStatus.transactions}
|
||||
client={props.client}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(bridgeStatus.status === "COMPLETED" ||
|
||||
bridgeStatus.status === "PENDING") && (
|
||||
<BridgeStatusContent
|
||||
bridgeStatus={bridgeStatus}
|
||||
client={props.client}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="px-6 lg:px-8 py-7 space-y-4">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-foreground "> Status </p>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 text-sm capitalize",
|
||||
bridgeStatus.status === "COMPLETED" && "text-green-500",
|
||||
bridgeStatus.status === "PENDING" && "text-blue-500",
|
||||
bridgeStatus.status === "FAILED" && "text-red-500",
|
||||
)}
|
||||
>
|
||||
{bridgeStatus.status === "COMPLETED" && (
|
||||
<CircleCheckIcon className="size-3.5" />
|
||||
)}
|
||||
{bridgeStatus.status === "PENDING" && (
|
||||
<Spinner className="size-3.5" />
|
||||
)}
|
||||
{bridgeStatus.status === "FAILED" && (
|
||||
<CircleXIcon className="size-3.5" />
|
||||
)}
|
||||
{bridgeStatus.status === "NOT_FOUND" && (
|
||||
<CircleXIcon className="size-3.5" />
|
||||
)}
|
||||
|
||||
{bridgeStatus.status.toLowerCase().replace("_", " ")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-foreground ">Payment ID</p>
|
||||
<CopyTextButton
|
||||
textToCopy={bridgeStatus.paymentId}
|
||||
textToShow={bridgeStatus.paymentId}
|
||||
tooltip="Payment ID"
|
||||
className="text-sm -translate-x-1.5 tabular-nums text-muted-foreground"
|
||||
copyIconPosition="left"
|
||||
iconClassName="text-muted-foreground/70"
|
||||
variant="ghost"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{purchaseDataString && (
|
||||
<div className="px-6 lg:px-8 py-7 space-y-2 border-t border-dashed">
|
||||
<p className="text-sm text-foreground ">Purchase Data</p>
|
||||
<CodeClient
|
||||
code={purchaseDataString}
|
||||
lang="json"
|
||||
className="[&_code]:text-xs"
|
||||
scrollableClassName="p-3"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TokenInfo(props: {
|
||||
label: string;
|
||||
addressLabel: string;
|
||||
token: Token;
|
||||
amountWei: bigint | undefined;
|
||||
walletAddress: string;
|
||||
client: ThirdwebClient;
|
||||
txHash: string | undefined;
|
||||
}) {
|
||||
const chainQuery = useChainQuery(props.token.chainId);
|
||||
const isNativeToken =
|
||||
props.token.address.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase();
|
||||
|
||||
const tokenAddress = getAddress(props.token.address);
|
||||
|
||||
return (
|
||||
<div className="flex-1 pt-10 pb-9 px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
||||
{props.label}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="h-6" />
|
||||
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative hover:ring-2 hover:ring-offset-2 hover:ring-offset-card hover:ring-foreground/30 rounded-full">
|
||||
<Link
|
||||
href={
|
||||
props.token.address.toLowerCase() ===
|
||||
NATIVE_TOKEN_ADDRESS.toLowerCase()
|
||||
? `/${chainQuery.data?.slug || props.token.chainId}`
|
||||
: `/${chainQuery.data?.slug || props.token.chainId}/${props.token.address}`
|
||||
}
|
||||
target="_blank"
|
||||
aria-label="View Token"
|
||||
className="absolute inset-0 z-10"
|
||||
/>
|
||||
|
||||
<Img
|
||||
src={props.token.iconUri}
|
||||
alt={props.token.name}
|
||||
className="size-10 rounded-full"
|
||||
fallback={
|
||||
<div className="size-10 from-blue-500 to-foreground rounded-full bg-gradient-to-b" />
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="absolute -bottom-0.5 -right-0.5 bg-background p-0.5 rounded-full">
|
||||
<Img
|
||||
src={resolveSchemeWithErrorHandler({
|
||||
client: props.client,
|
||||
uri: chainQuery.data?.icon?.url,
|
||||
})}
|
||||
alt={props.token.name}
|
||||
className="size-4 rounded-full"
|
||||
fallback={
|
||||
<div className="size-4 from-blue-500 to-foreground rounded-full bg-gradient-to-b" />
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<p className="text-lg text-foreground font-medium tracking-tight leading-none">
|
||||
{props.amountWei ? (
|
||||
<>
|
||||
{toTokens(props.amountWei, props.token.decimals)}{" "}
|
||||
{props.token.symbol}
|
||||
</>
|
||||
) : (
|
||||
"N/A"
|
||||
)}
|
||||
</p>
|
||||
<SkeletonContainer
|
||||
skeletonData={`Chain ${props.token.chainId}`}
|
||||
loadedData={chainQuery.data?.name}
|
||||
className="[&_*]:flex"
|
||||
render={(data) => (
|
||||
<Link
|
||||
href={`/${chainQuery.data?.slug || props.token.chainId}`}
|
||||
className="text-sm text-muted-foreground leading-none hover:underline flex items-center gap-1"
|
||||
target="_blank"
|
||||
>
|
||||
{data}{" "}
|
||||
<ArrowUpRightIcon className="size-3.5 text-muted-foreground/70 shrink-0" />
|
||||
</Link>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-8" />
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-foreground ">{props.addressLabel}</p>
|
||||
<WalletAddress
|
||||
address={getAddress(props.walletAddress)}
|
||||
client={props.client}
|
||||
className="py-0.5 h-auto text-sm [&>*]:!font-sans tabular-nums [&>*]:font-normal text-muted-foreground hover:text-foreground"
|
||||
iconClassName="size-3"
|
||||
shortenAddress={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-foreground ">Token Address</p>
|
||||
<LinkWithCopyButton
|
||||
href={
|
||||
isNativeToken
|
||||
? `/${chainQuery.data?.slug || props.token.chainId}`
|
||||
: `/${chainQuery.data?.slug || props.token.chainId}/${props.token.address}`
|
||||
}
|
||||
textToShow={tokenAddress}
|
||||
textToCopy={tokenAddress}
|
||||
copyTooltip="Copy Token Address"
|
||||
className="-translate-x-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-foreground ">Transaction Hash</p>
|
||||
{props.txHash ? (
|
||||
<LinkWithCopyButton
|
||||
className="-translate-x-1"
|
||||
href={`/${chainQuery.data?.slug || props.token.chainId}/tx/${props.txHash}`}
|
||||
textToShow={`${props.txHash.slice(0, 12)}...${props.txHash.slice(-4)}`}
|
||||
textToCopy={props.txHash}
|
||||
copyTooltip="Copy Transaction Hash"
|
||||
/>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground ">N/A</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BridgeStatusContent(props: {
|
||||
bridgeStatus: Exclude<Status, { status: "FAILED" } | { status: "NOT_FOUND" }>;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
const { bridgeStatus } = props;
|
||||
|
||||
const fromTxHash = bridgeStatus.transactions.find(
|
||||
(tx) => tx.chainId === bridgeStatus.originChainId,
|
||||
)?.transactionHash;
|
||||
const toTxHash = bridgeStatus.transactions.find(
|
||||
(tx) => tx.chainId === bridgeStatus.destinationChainId,
|
||||
)?.transactionHash;
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 border-b border-dashed">
|
||||
<TokenInfo
|
||||
label="From"
|
||||
token={bridgeStatus.originToken}
|
||||
amountWei={bridgeStatus.originAmount}
|
||||
walletAddress={bridgeStatus.sender}
|
||||
client={props.client}
|
||||
txHash={fromTxHash}
|
||||
addressLabel="Sender"
|
||||
/>
|
||||
|
||||
<div className="border-t lg:border-l lg:border-t-0 border-dashed relative">
|
||||
<div className="p-1.5 rounded-full bg-background absolute top-0 lg:top-1/2 left-1/2 lg:left-0 -translate-y-1/2 -translate-x-1/2 border">
|
||||
<ArrowRightIcon className="size-4 text-muted-foreground rotate-90 lg:rotate-0" />
|
||||
</div>
|
||||
<TokenInfo
|
||||
label="To"
|
||||
token={bridgeStatus.destinationToken}
|
||||
// TODO - destinationAmount is not available in PENDING status, fix it in API
|
||||
amountWei={
|
||||
bridgeStatus.status === "COMPLETED"
|
||||
? bridgeStatus.destinationAmount
|
||||
: undefined
|
||||
}
|
||||
walletAddress={bridgeStatus.receiver}
|
||||
client={props.client}
|
||||
txHash={toTxHash}
|
||||
addressLabel="Receiver"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FailedBridgeStatusContent(props: {
|
||||
transactions: { chainId: number; transactionHash: string }[];
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
return (
|
||||
<div className="px-6 lg:px-8 py-7 space-y-1.5 border-b border-dashed">
|
||||
<h3 className="text-lg font-medium tracking-tight mb-4">Transactions</h3>
|
||||
<div className="space-y-4">
|
||||
{props.transactions.map((tx) => {
|
||||
return (
|
||||
<TxHashRow
|
||||
key={tx.chainId}
|
||||
client={props.client}
|
||||
chainId={tx.chainId}
|
||||
txHash={tx.transactionHash}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useChainQuery(chainId: number | undefined) {
|
||||
return useQuery({
|
||||
enabled: !!chainId,
|
||||
queryKey: ["chain-metadata", chainId],
|
||||
queryFn: async () => {
|
||||
if (!chainId) {
|
||||
return null;
|
||||
}
|
||||
return fetchChain(chainId);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function TxHashRow(props: {
|
||||
client: ThirdwebClient;
|
||||
chainId: number;
|
||||
txHash: string;
|
||||
}) {
|
||||
const chainQuery = useChainQuery(props.chainId);
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Img
|
||||
src={resolveSchemeWithErrorHandler({
|
||||
client: props.client,
|
||||
uri: chainQuery.data?.icon?.url,
|
||||
})}
|
||||
alt={chainQuery.data?.name}
|
||||
className="size-3.5 rounded-full"
|
||||
fallback={
|
||||
<div className="size-3.5 from-blue-500 to-foreground rounded-full bg-gradient-to-b" />
|
||||
}
|
||||
/>
|
||||
<SkeletonContainer
|
||||
skeletonData={`Chain ${props.chainId}`}
|
||||
loadedData={chainQuery.data?.name}
|
||||
render={(data) => <p className="text-sm text-foreground">{data}</p>}
|
||||
/>
|
||||
</div>
|
||||
<LinkWithCopyButton
|
||||
href={`/${chainQuery.data?.slug || props.chainId}/tx/${props.txHash}`}
|
||||
textToShow={props.txHash}
|
||||
textToCopy={props.txHash}
|
||||
copyTooltip="Copy Transaction Hash"
|
||||
className="-translate-x-0.5"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function BridgeStatusWithPolling(props: {
|
||||
bridgeStatus: Status;
|
||||
client: ThirdwebClient;
|
||||
chainId: number;
|
||||
transactionHash: string;
|
||||
}) {
|
||||
const bridgeStatusQuery = useQuery({
|
||||
enabled:
|
||||
props.bridgeStatus.status === "PENDING" ||
|
||||
props.bridgeStatus.status === "NOT_FOUND",
|
||||
queryKey: ["bridge-status", props.transactionHash, props.chainId],
|
||||
queryFn: () =>
|
||||
status({
|
||||
chainId: props.chainId,
|
||||
transactionHash: props.transactionHash as `0x${string}`,
|
||||
client: props.client,
|
||||
}),
|
||||
refetchInterval(query) {
|
||||
const status = query.state.data?.status;
|
||||
if (status === "COMPLETED" || status === "FAILED") {
|
||||
return false;
|
||||
}
|
||||
return 5000;
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<BridgeStatus
|
||||
bridgeStatus={bridgeStatusQuery.data || props.bridgeStatus}
|
||||
client={props.client}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function getPurchaseData(purchaseData: PurchaseData) {
|
||||
try {
|
||||
return JSON.stringify(purchaseData, null, 2);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
+355
-126
@@ -1,16 +1,42 @@
|
||||
import { toTokens, ZERO_ADDRESS } from "thirdweb";
|
||||
import { Button } from "@workspace/ui/components/button";
|
||||
import { PlainTextCodeBlock } from "@workspace/ui/components/code/plaintext-code";
|
||||
import { ToolTipLabel } from "@workspace/ui/components/tooltip";
|
||||
import { formatDate, formatDistanceToNowStrict, fromUnixTime } from "date-fns";
|
||||
import {
|
||||
CircleAlertIcon,
|
||||
CircleCheckIcon,
|
||||
Clock4Icon,
|
||||
InfoIcon,
|
||||
} from "lucide-react";
|
||||
import { getAddress, toTokens } from "thirdweb";
|
||||
import { status } from "thirdweb/bridge";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import {
|
||||
eth_getBlockByHash,
|
||||
eth_getTransactionByHash,
|
||||
eth_getTransactionReceipt,
|
||||
getRpcClient,
|
||||
} from "thirdweb/rpc";
|
||||
import { hexToNumber, shortenAddress, toEther } from "thirdweb/utils";
|
||||
import type { TransactionReceipt } from "thirdweb/transaction";
|
||||
import { hexToNumber, toEther } from "thirdweb/utils";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
|
||||
import { mapV4ChainToV5Chain } from "@/utils/map-chains";
|
||||
import { getChain } from "../../../utils";
|
||||
import { BridgeStatusWithPolling } from "./bridge-status";
|
||||
import { BridgeAndOverviewTabs } from "./tabs";
|
||||
|
||||
type Transaction = Awaited<ReturnType<typeof eth_getTransactionByHash>>;
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{ chain_id: string; txHash: `0x${string}` }>;
|
||||
@@ -25,142 +51,345 @@ export default async function Page(props: {
|
||||
client: serverThirdwebClient,
|
||||
});
|
||||
|
||||
const [transaction, receipt] = await Promise.all([
|
||||
const [transaction, receipt, bridgeStatus] = await Promise.all([
|
||||
eth_getTransactionByHash(rpcRequest, {
|
||||
hash: params.txHash,
|
||||
}),
|
||||
}).catch(() => undefined),
|
||||
eth_getTransactionReceipt(rpcRequest, {
|
||||
hash: params.txHash,
|
||||
}),
|
||||
}).catch(() => undefined),
|
||||
status({
|
||||
chainId: chain.chainId,
|
||||
transactionHash: params.txHash,
|
||||
client: serverThirdwebClient,
|
||||
}).catch(() => undefined),
|
||||
]);
|
||||
if (!transaction.blockHash) {
|
||||
return <div>no tx found</div>;
|
||||
|
||||
if (!transaction?.blockHash || !receipt) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center grow">
|
||||
<div className="flex flex-col items-center justify-center gap-3">
|
||||
<div className="p-2 rounded-full bg-background border">
|
||||
<CircleAlertIcon className="size-5 text-muted-foreground" />
|
||||
</div>
|
||||
Transaction not found
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const block = await eth_getBlockByHash(rpcRequest, {
|
||||
blockHash: transaction.blockHash,
|
||||
});
|
||||
|
||||
const overviewContent = (
|
||||
<GeneericTxDetails
|
||||
transaction={transaction}
|
||||
receipt={receipt}
|
||||
block={block}
|
||||
chain={chain}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<main className="container flex flex-col gap-y-10 py-6">
|
||||
<header className="flex flex-col gap-y-2">
|
||||
<p className="font-bold text-3xl">Transaction Details</p>
|
||||
<div className="flex gap-x-2">
|
||||
{shortenAddress(transaction.from)} called on{" "}
|
||||
{shortenAddress(transaction.to || ZERO_ADDRESS)}
|
||||
</div>
|
||||
</header>
|
||||
<main className="pb-20">
|
||||
<div className="flex h-14 items-center border-border border-b">
|
||||
<Breadcrumb className="container max-w-6xl">
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="/chainlist">Chainlist</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href={`/${chain.slug}`}>
|
||||
{chain.name.replace("Mainnet", "")}
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>Transaction Details</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
|
||||
<Separator orientation="horizontal" />
|
||||
<div className="container max-w-6xl py-10">
|
||||
<h1 className="font-semibold text-3xl tracking-tight">
|
||||
Transaction Details
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<section className="flex flex-col gap-y-2">
|
||||
<div className="grid grid-cols-2">
|
||||
<div className="flex items-center gap-x-2">Transaction hash</div>
|
||||
<p>{transaction.hash}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2">
|
||||
<div className="flex items-center gap-x-2">
|
||||
Status {/* and method */}
|
||||
</div>
|
||||
<div className="flex gap-x-2">
|
||||
<Badge
|
||||
className={
|
||||
receipt.status === "success" ? "bg-green-500" : "bg-red-500"
|
||||
}
|
||||
>
|
||||
{receipt.status}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2">
|
||||
<div className="flex items-center gap-x-2">Block</div>
|
||||
<p>{Number(transaction.blockNumber)}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2">
|
||||
<div className="flex items-center gap-x-2">Timestamp</div>
|
||||
<p>
|
||||
{new Date(
|
||||
Number.parseInt(block.timestamp.toString()) * 1000,
|
||||
).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Separator orientation="horizontal" />
|
||||
|
||||
<section className="flex flex-col gap-y-2">
|
||||
<div className="grid grid-cols-2">
|
||||
<div className="flex items-center gap-x-2">From</div>
|
||||
<p>{transaction.from}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2">
|
||||
<div className="flex items-center gap-x-2">
|
||||
Interacted with contract
|
||||
</div>
|
||||
<p>{transaction.to}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Separator orientation="horizontal" />
|
||||
|
||||
<section className="flex flex-col gap-y-2">
|
||||
<div className="grid grid-cols-2">
|
||||
<div className="flex items-center gap-x-2">Value</div>
|
||||
<p>{toEther(transaction.value)} ETH</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2">
|
||||
<div className="flex items-center gap-x-2">Transaction fee</div>
|
||||
<p>{toEther((transaction.gasPrice || 0n) * receipt.gasUsed)} ETH</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2">
|
||||
<div className="flex items-center gap-x-2">Gas price</div>
|
||||
<p>{toEther(transaction.gasPrice || 0n)} ETH</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2">
|
||||
<div className="flex items-center gap-x-2">Gas usage</div>
|
||||
<p>{receipt.gasUsed.toString()}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2">
|
||||
<div className="flex items-center gap-x-2">Gas fees (Gwei)</div>
|
||||
<p>
|
||||
Base: {toTokens(block.baseFeePerGas || 0n, 9)} | Max:{" "}
|
||||
{toTokens(transaction.maxFeePerGas || 0n, 9)} | Max priority:{" "}
|
||||
{transaction.maxPriorityFeePerGas?.toString()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2">
|
||||
<div className="flex items-center gap-x-2">Burnt fees</div>
|
||||
<p>{toEther(block.baseFeePerGas || 0n * receipt.gasUsed)}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Separator orientation="horizontal" />
|
||||
|
||||
<section className="flex flex-col gap-y-2">
|
||||
<div className="grid grid-cols-2">
|
||||
<div className="flex items-center gap-x-2">Other</div>
|
||||
<p>
|
||||
Txn type: {hexToNumber(transaction.typeHex || "0x0")} (
|
||||
{transaction.type}) | Nonce: {transaction.nonce} | Position:{" "}
|
||||
{transaction.transactionIndex}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 items-start">
|
||||
<div className="flex items-center gap-x-2">Raw Input:</div>
|
||||
<div className="break-words rounded-lg bg-card p-4 text-sm">
|
||||
{transaction.input}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div className="container max-w-6xl">
|
||||
{bridgeStatus ? (
|
||||
<BridgeAndOverviewTabs
|
||||
bridgeStatus={
|
||||
<BridgeStatusWithPolling
|
||||
bridgeStatus={bridgeStatus}
|
||||
client={getClientThirdwebClient()}
|
||||
chainId={chain.chainId}
|
||||
transactionHash={params.txHash}
|
||||
/>
|
||||
}
|
||||
overview={overviewContent}
|
||||
/>
|
||||
) : (
|
||||
overviewContent
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function GeneericTxDetails(props: {
|
||||
transaction: Transaction;
|
||||
receipt: TransactionReceipt;
|
||||
block: {
|
||||
timestamp: bigint;
|
||||
baseFeePerGas: bigint | null;
|
||||
};
|
||||
chain: ChainMetadata;
|
||||
}) {
|
||||
const { transaction, receipt, chain, block } = props;
|
||||
|
||||
const timestamp = getDatefromTimestamp(block.timestamp);
|
||||
|
||||
const fromAddress = getAddress(transaction.from);
|
||||
const toAddress = transaction.to ? getAddress(transaction.to) : undefined;
|
||||
|
||||
return (
|
||||
<div className="border rounded-xl p-4 lg:p-6 bg-card">
|
||||
{/* section 1 */}
|
||||
<section className="border-b pb-6 space-y-5 lg:space-y-3">
|
||||
<GridItem
|
||||
label="Transaction hash"
|
||||
tooltip="A TxHash or transaction hash is a unique 66-character identifier that is generated whenever a transaction is executed."
|
||||
>
|
||||
<CopyTextButton
|
||||
textToCopy={transaction.hash}
|
||||
textToShow={transaction.hash}
|
||||
tooltip="Copy transaction hash"
|
||||
variant="ghost"
|
||||
copyIconPosition="right"
|
||||
className="truncate -translate-x-1.5 max-w-full"
|
||||
/>
|
||||
</GridItem>
|
||||
|
||||
<GridItem label="Status" tooltip="The status of the transaction.">
|
||||
<Badge
|
||||
className="py-0.5 gap-1.5 px-1.5 capitalize"
|
||||
variant={receipt.status === "success" ? "success" : "destructive"}
|
||||
>
|
||||
{receipt.status === "success" ? (
|
||||
<CircleCheckIcon className="size-3" />
|
||||
) : (
|
||||
<CircleAlertIcon className="size-3" />
|
||||
)}
|
||||
{receipt.status}
|
||||
</Badge>
|
||||
</GridItem>
|
||||
|
||||
{transaction.blockNumber && (
|
||||
<GridItem
|
||||
label="Block"
|
||||
tooltip="Number of the block in which the transaction is recorded. Block confirmations indicate how many blocks have been added since the transaction was produced."
|
||||
>
|
||||
<p>{Number(transaction.blockNumber)}</p>
|
||||
</GridItem>
|
||||
)}
|
||||
|
||||
{timestamp && (
|
||||
<GridItem
|
||||
label="Timestamp"
|
||||
tooltip="The date and time at which a transaction is validated."
|
||||
>
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
<Clock4Icon className="size-3" />
|
||||
<p>
|
||||
{formatDistanceToNowStrict(timestamp, {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</p>
|
||||
<p>({formatDate(timestamp, "PP pp z")})</p>
|
||||
</div>
|
||||
</GridItem>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* section 2 */}
|
||||
<section className="border-b py-6 space-y-5 lg:space-y-3">
|
||||
<GridItem label="From" tooltip="The sending party of the transaction.">
|
||||
<CopyTextButton
|
||||
textToCopy={fromAddress}
|
||||
textToShow={fromAddress}
|
||||
tooltip="Copy from address"
|
||||
variant="ghost"
|
||||
copyIconPosition="right"
|
||||
className="truncate -translate-x-1.5 max-w-full"
|
||||
/>
|
||||
</GridItem>
|
||||
|
||||
{toAddress && (
|
||||
<GridItem
|
||||
label="Interacted with (To)"
|
||||
tooltip="The receiving party of the transaction (could be a contract address)."
|
||||
>
|
||||
<CopyTextButton
|
||||
textToCopy={toAddress}
|
||||
textToShow={toAddress}
|
||||
tooltip="Copy to address"
|
||||
variant="ghost"
|
||||
copyIconPosition="right"
|
||||
className="truncate -translate-x-1.5 max-w-full"
|
||||
/>
|
||||
</GridItem>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* section 3 */}
|
||||
<section className="py-6 space-y-5 lg:space-y-3 border-b">
|
||||
<GridItem
|
||||
label="Value"
|
||||
tooltip="The value being transacted in ETH and fiat value. Note: You can click the fiat value (if available) to see historical value at the time of transaction."
|
||||
>
|
||||
<p>
|
||||
{toEther(transaction.value)} {chain.nativeCurrency?.symbol}
|
||||
</p>
|
||||
</GridItem>
|
||||
|
||||
<GridItem
|
||||
label="Transaction Fee"
|
||||
tooltip="Amount paid to the validator for processing the transaction."
|
||||
>
|
||||
<p>
|
||||
{toEther((transaction.gasPrice || 0n) * receipt.gasUsed)}{" "}
|
||||
{chain.nativeCurrency?.symbol}
|
||||
</p>
|
||||
</GridItem>
|
||||
|
||||
<GridItem
|
||||
label="Gas Price"
|
||||
tooltip="The price per gas unit the sender is willing to pay"
|
||||
>
|
||||
<p>
|
||||
{toEther(transaction.gasPrice || 0n)} {chain.nativeCurrency?.symbol}
|
||||
</p>
|
||||
</GridItem>
|
||||
|
||||
<GridItem
|
||||
label="Gas Usage"
|
||||
tooltip="The amount of gas used by the transaction."
|
||||
>
|
||||
<p>{receipt.gasUsed.toString()}</p>
|
||||
</GridItem>
|
||||
|
||||
<GridItem
|
||||
label="Gas Fees"
|
||||
tooltip="Base Fee refers to the network Base Fee at the time of the block, while Max Fee & Max Priority Fee refer to the max amount a user is willing to pay for their tx & to give to the miner respectively."
|
||||
>
|
||||
<p className="flex gap-2.5">
|
||||
<span>Base: {toTokens(block.baseFeePerGas || 0n, 9)} Gwei</span>
|
||||
|
||||
{transaction.maxFeePerGas && (
|
||||
<>
|
||||
<span className="text-muted-foreground/50">|</span>
|
||||
<span>
|
||||
Max: {toTokens(transaction.maxFeePerGas || 0n, 9)} Gwei
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
{transaction.maxPriorityFeePerGas && (
|
||||
<>
|
||||
<span className="text-muted-foreground/50">|</span>
|
||||
<span>
|
||||
Max priority: {transaction.maxPriorityFeePerGas?.toString()}{" "}
|
||||
Gwei
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</GridItem>
|
||||
|
||||
<GridItem
|
||||
label="Burnt fees"
|
||||
tooltip="The amount of fees that were burnt by the transaction."
|
||||
>
|
||||
<p>
|
||||
{toEther((block.baseFeePerGas || 0n) * receipt.gasUsed)}{" "}
|
||||
{chain.nativeCurrency?.symbol}
|
||||
</p>
|
||||
</GridItem>
|
||||
</section>
|
||||
|
||||
{/* section 4 */}
|
||||
<section className="pt-6 space-y-5 lg:space-y-3">
|
||||
<GridItem
|
||||
label="Other Attributes"
|
||||
tooltip="Other data related to this transaction."
|
||||
>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Badge variant="outline" className="gap-1 py-1">
|
||||
<span className="text-muted-foreground">Txn type:</span>
|
||||
<span className="capitalize">
|
||||
{hexToNumber(transaction.typeHex || "0x0")} ({transaction.type})
|
||||
</span>
|
||||
</Badge>
|
||||
<Badge variant="outline" className="gap-1 py-1">
|
||||
<span className="text-muted-foreground">Nonce: </span>
|
||||
<span>{transaction.nonce}</span>
|
||||
</Badge>
|
||||
<Badge variant="outline" className="gap-1 py-1">
|
||||
<span className="text-muted-foreground">Position: </span>
|
||||
<span>{transaction.transactionIndex}</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</GridItem>
|
||||
|
||||
<GridItem
|
||||
label="Input Data"
|
||||
tooltip="Additional data included for this transaction. Commonly used as part of contract interaction or as a message sent to the recipient."
|
||||
>
|
||||
<PlainTextCodeBlock
|
||||
code={transaction.input}
|
||||
className="[&_code]:!whitespace-pre-wrap break-all [&_code]:text-xs text-muted-foreground w-full"
|
||||
scrollableClassName="max-h-[400px] p-2 min-h-[80px]"
|
||||
/>
|
||||
</GridItem>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function GridItem(props: {
|
||||
label: string;
|
||||
children: React.ReactNode;
|
||||
tooltip?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-1 lg:grid grid-cols-[300px_1fr] items-start">
|
||||
<div className="flex items-center gap-0.5">
|
||||
<ToolTipLabel label={props.tooltip}>
|
||||
<Button
|
||||
asChild
|
||||
variant="ghost"
|
||||
className="h-auto w-auto p-1 hidden lg:block"
|
||||
>
|
||||
<div>
|
||||
<InfoIcon className="size-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
</Button>
|
||||
</ToolTipLabel>
|
||||
<p className="text-muted-foreground text-sm">{props.label}</p>
|
||||
</div>
|
||||
<div className="text-sm w-full">{props.children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getDatefromTimestamp(timestamp: bigint) {
|
||||
try {
|
||||
return fromUnixTime(Number(timestamp));
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import { TabButtons } from "@/components/ui/tabs";
|
||||
|
||||
export function BridgeAndOverviewTabs(props: {
|
||||
bridgeStatus: React.ReactNode;
|
||||
overview: React.ReactNode;
|
||||
}) {
|
||||
const [tab, setTab] = useState<"bridge" | "overview">("bridge");
|
||||
return (
|
||||
<div>
|
||||
<TabButtons
|
||||
tabs={[
|
||||
{
|
||||
name: "Bridge",
|
||||
onClick: () => setTab("bridge"),
|
||||
isActive: tab === "bridge",
|
||||
},
|
||||
{
|
||||
name: "Overview",
|
||||
onClick: () => setTab("overview"),
|
||||
isActive: tab === "overview",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<div className="h-3" />
|
||||
{tab === "bridge" && props.bridgeStatus}
|
||||
{tab === "overview" && props.overview}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+3
-3
@@ -15,7 +15,7 @@ import {
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { products } from "../../../components/server/products";
|
||||
import { services } from "../../../components/server/products";
|
||||
|
||||
function cleanUrl(url: string) {
|
||||
if (url.endsWith("?")) {
|
||||
@@ -345,7 +345,7 @@ export const ChainServiceFilter: React.FC<ChainServiceFilterProps> = ({
|
||||
|
||||
const section = (
|
||||
<FilterSection title="Services">
|
||||
{products.map((product) => (
|
||||
{services.map((product) => (
|
||||
<div className="group flex items-center gap-2" key={product.id}>
|
||||
<Checkbox
|
||||
checked={isServiceActive(mutableSearchParams, product.id)}
|
||||
@@ -393,7 +393,7 @@ export const ChainServiceFilter: React.FC<ChainServiceFilterProps> = ({
|
||||
}
|
||||
|
||||
const firstFilter = allFilters[0];
|
||||
const name = products.find((p) => p.id === firstFilter)?.name;
|
||||
const name = services.find((p) => p.id === firstFilter)?.name;
|
||||
const plus = allFilters.length > 1 ? ` +${allFilters.length - 1}` : "";
|
||||
|
||||
return [`${name}${plus}`, true];
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@ import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ChainSupportedService } from "@/types/chain";
|
||||
import { ChainIcon } from "../../../components/server/chain-icon";
|
||||
import { products } from "../../../components/server/products";
|
||||
import { services } from "../../../components/server/products";
|
||||
import { getCustomChainMetadata } from "../../../utils";
|
||||
|
||||
type ChainListRowProps = {
|
||||
@@ -83,7 +83,7 @@ export function ChainListRow({
|
||||
<TableCell>
|
||||
<div className="flex w-[520px] flex-row items-center gap-14 ">
|
||||
<div className="z-10 flex items-center gap-4">
|
||||
{products.map((p) => {
|
||||
{services.map((p) => {
|
||||
return (
|
||||
<ProductIcon
|
||||
href={p.link}
|
||||
|
||||
@@ -1,74 +1,69 @@
|
||||
import { ConnectSDKIcon } from "@/icons/ConnectSDKIcon";
|
||||
import { ContractIcon } from "@/icons/ContractIcon";
|
||||
import { EngineIcon } from "@/icons/EngineIcon";
|
||||
import { BotIcon, CoinsIcon, FuelIcon, HardDriveIcon } from "lucide-react";
|
||||
import { BridgeIcon } from "@/icons/BridgeIcon";
|
||||
import { InsightIcon } from "@/icons/InsightIcon";
|
||||
import { NebulaIcon } from "@/icons/NebulaIcon";
|
||||
import { PayIcon } from "@/icons/PayIcon";
|
||||
import { RPCIcon } from "@/icons/RPCIcon";
|
||||
import { SmartAccountIcon } from "@/icons/SmartAccountIcon";
|
||||
import { WalletProductIcon } from "@/icons/WalletProductIcon";
|
||||
import type { ChainSupportedService } from "@/types/chain";
|
||||
|
||||
export const products = [
|
||||
export const services = [
|
||||
{
|
||||
description: "Create, deploy and manage smart contracts",
|
||||
icon: ContractIcon,
|
||||
icon: CoinsIcon,
|
||||
id: "contracts",
|
||||
link: "https://thirdweb.com/explore",
|
||||
name: "Contracts",
|
||||
link: "https://thirdweb.com/token",
|
||||
name: "Tokens",
|
||||
},
|
||||
{
|
||||
description: "Create and manage crypto wallets",
|
||||
icon: ConnectSDKIcon,
|
||||
icon: WalletProductIcon,
|
||||
id: "connect-sdk",
|
||||
link: "https://thirdweb.com/connect",
|
||||
name: "Wallets",
|
||||
link: "https://thirdweb.com/wallets",
|
||||
name: "User Wallets",
|
||||
},
|
||||
{
|
||||
description: "Performant and scalable RPC service",
|
||||
icon: RPCIcon,
|
||||
id: "rpc-edge",
|
||||
link: "https://portal.thirdweb.com/infrastructure/rpc-edge/overview",
|
||||
name: "RPC Edge",
|
||||
link: "https://thirdweb.com/rpc",
|
||||
name: "RPC",
|
||||
},
|
||||
{
|
||||
description: "Backend server that reads, writes, and deploys contracts",
|
||||
icon: EngineIcon,
|
||||
icon: HardDriveIcon,
|
||||
id: "engine",
|
||||
link: "https://thirdweb.com/engine",
|
||||
name: "Transactions",
|
||||
link: "https://thirdweb.com/wallets",
|
||||
name: "Server Wallets",
|
||||
},
|
||||
{
|
||||
description: "Enable gas sponsorship for seamless transactions",
|
||||
icon: SmartAccountIcon,
|
||||
icon: FuelIcon,
|
||||
id: "account-abstraction",
|
||||
link: "https://portal.thirdweb.com/wallets/sponsor-gas",
|
||||
name: "Account Abstraction",
|
||||
link: "https://thirdweb.com/wallets",
|
||||
name: "Gas Sponsorship",
|
||||
},
|
||||
{
|
||||
description: "Enable payments in any token on any chain",
|
||||
icon: PayIcon,
|
||||
icon: BridgeIcon,
|
||||
id: "pay",
|
||||
link: "https://portal.thirdweb.com/payments",
|
||||
name: "Payments",
|
||||
link: "https://thirdweb.com/monetize/bridge",
|
||||
name: "Bridge",
|
||||
},
|
||||
{
|
||||
description: "Query and analyze blockchain data",
|
||||
icon: InsightIcon,
|
||||
id: "insight",
|
||||
link: "https://thirdweb.com/insight",
|
||||
name: "Insight",
|
||||
},
|
||||
{
|
||||
description: "The most powerful AI for interacting with the blockchain",
|
||||
icon: NebulaIcon,
|
||||
icon: BotIcon,
|
||||
id: "nebula",
|
||||
link: "https://thirdweb.com/ai",
|
||||
name: "thirdweb AI",
|
||||
name: "AI",
|
||||
},
|
||||
{
|
||||
icon: PayIcon,
|
||||
id: "account-abstraction",
|
||||
link: "https://thirdweb.com/x402",
|
||||
name: "x402",
|
||||
},
|
||||
{
|
||||
icon: InsightIcon,
|
||||
id: "insight",
|
||||
link: "https://thirdweb.com/gateway",
|
||||
name: "Indexer",
|
||||
},
|
||||
] satisfies Array<{
|
||||
name: string;
|
||||
id: ChainSupportedService;
|
||||
icon: React.FC<{ className?: string }>;
|
||||
description: string;
|
||||
link: string;
|
||||
}>;
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 MiB |
@@ -1,130 +0,0 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { Suspense } from "react";
|
||||
import {
|
||||
ContractCard,
|
||||
ContractCardSkeleton,
|
||||
} from "@/components/contracts/contract-card/contract-card";
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
import { DeployUpsellCard } from "../components/upsells/deploy-your-own";
|
||||
import { getCategory } from "../data";
|
||||
|
||||
type ExploreCategoryPageProps = {
|
||||
params: Promise<{
|
||||
category: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
export async function generateMetadata(
|
||||
props: ExploreCategoryPageProps,
|
||||
): Promise<Metadata> {
|
||||
const params = await props.params;
|
||||
const category = getCategory(params.category);
|
||||
if (!category) {
|
||||
notFound();
|
||||
}
|
||||
return {
|
||||
description: `${category.description} Deploy with one click to Ethereum, Polygon, Optimism, and other EVM blockchains with thirdweb.`,
|
||||
title: `${category.name} Smart Contracts | Explore`,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function ExploreCategoryPage(
|
||||
props: ExploreCategoryPageProps,
|
||||
) {
|
||||
const params = await props.params;
|
||||
const category = getCategory(params.category);
|
||||
if (!category) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<Breadcrumb className="border-border border-b px-6 py-4">
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink asChild>
|
||||
<Link href="/explore">Explore</Link>
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>
|
||||
{category.displayName || category.name}
|
||||
</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<div className="container flex flex-col gap-4 py-8">
|
||||
<h1 className="mb-3 font-bold text-3xl tracking-tighter lg:text-5xl">
|
||||
{category.displayName || category.name}
|
||||
</h1>
|
||||
<p className="max-w-screen-md text-base text-muted-foreground lg:text-lg">
|
||||
{category.description}{" "}
|
||||
{category.learnMore && (
|
||||
<Link href={category.learnMore}>Learn more</Link>
|
||||
)}
|
||||
</p>
|
||||
<div className="h-10" />
|
||||
<div className="grid grid-cols-1 gap-5 md:grid-cols-3">
|
||||
{category.contracts.map((publishedContractId) => {
|
||||
const publisher: string | undefined = Array.isArray(
|
||||
publishedContractId,
|
||||
)
|
||||
? publishedContractId[0].split("/")[0]
|
||||
: publishedContractId.split("/")[0];
|
||||
const contractId: string | undefined = Array.isArray(
|
||||
publishedContractId,
|
||||
)
|
||||
? publishedContractId[0].split("/")[1]
|
||||
: publishedContractId.split("/")[1];
|
||||
const modules = Array.isArray(publishedContractId)
|
||||
? publishedContractId[1]
|
||||
: undefined;
|
||||
const overrides = Array.isArray(publishedContractId)
|
||||
? publishedContractId[2]
|
||||
: undefined;
|
||||
|
||||
if (!publisher || !contractId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense
|
||||
fallback={<ContractCardSkeleton />}
|
||||
key={publisher + contractId + overrides?.title}
|
||||
>
|
||||
<ContractCard
|
||||
contractId={contractId}
|
||||
descriptionOverride={overrides?.description}
|
||||
isBeta={category.isBeta}
|
||||
modules={
|
||||
modules?.length
|
||||
? modules.map((m) => ({
|
||||
moduleId: m.split("/")[1] || "",
|
||||
publisher: m.split("/")[0] || "",
|
||||
}))
|
||||
: undefined
|
||||
}
|
||||
publisher={publisher}
|
||||
titleOverride={overrides?.title}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="h-16" />
|
||||
<DeployUpsellCard />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+28
-32
@@ -1,4 +1,3 @@
|
||||
import { ArrowRightIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { Suspense } from "react";
|
||||
import {
|
||||
@@ -7,48 +6,45 @@ import {
|
||||
} from "@/components/contracts/contract-card/contract-card";
|
||||
import type { ExploreCategory } from "../../data";
|
||||
|
||||
export function ContractRow({ category }: { category: ExploreCategory }) {
|
||||
export function ContractRow({
|
||||
category,
|
||||
prefixCard,
|
||||
}: {
|
||||
category: ExploreCategory;
|
||||
prefixCard?: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<section>
|
||||
{/* Title, Description + View all link */}
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<header className="flex flex-col gap-1.5">
|
||||
<Link href={`/explore/${category.id}`}>
|
||||
<h2 className="font-semibold text-2xl tracking-tight">
|
||||
{category.displayName || category.name}
|
||||
</h2>
|
||||
</Link>
|
||||
<header className="flex flex-col gap-1">
|
||||
<h2 className="font-semibold text-2xl tracking-tight">
|
||||
{category.displayName || category.name}
|
||||
</h2>
|
||||
|
||||
<p className="text-muted-foreground">
|
||||
{category.description}{" "}
|
||||
{category.learnMore && (
|
||||
<Link
|
||||
className="inline text-link-foreground"
|
||||
href={category.learnMore}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
</Link>
|
||||
)}
|
||||
</p>
|
||||
{(category.description || category.learnMore) && (
|
||||
<p className="text-muted-foreground">
|
||||
{category.description}{" "}
|
||||
{category.learnMore && (
|
||||
<Link
|
||||
className="inline text-link-foreground"
|
||||
href={category.learnMore}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
</Link>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</header>
|
||||
|
||||
{category.contracts.length > 6 && (
|
||||
<Link
|
||||
className="flex shrink-0 items-center gap-1 text-base text-link-foreground hover:text-foreground"
|
||||
href={`/explore/${category.id}`}
|
||||
>
|
||||
View all
|
||||
<ArrowRightIcon className="size-4" />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="h-5" />
|
||||
|
||||
<div className="relative z-0 grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
{category.contracts.slice(0, 6).map((publishedContractId) => {
|
||||
{prefixCard}
|
||||
{category.contracts.map((publishedContractId) => {
|
||||
const publisher: string | undefined = Array.isArray(
|
||||
publishedContractId,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
"use client";
|
||||
|
||||
import { RocketIcon, ShieldCheckIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { reportTokenUpsellClicked } from "@/analytics/report";
|
||||
import { Img } from "@/components/blocks/Img";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
const thirdwebEns = {
|
||||
name: "thirdweb.eth",
|
||||
image: "https://euc.li/thirdweb.eth",
|
||||
};
|
||||
|
||||
function TokenContractCard(props: {
|
||||
name: string;
|
||||
description: string;
|
||||
link: string;
|
||||
trackingType: "nft" | "coin";
|
||||
}) {
|
||||
return (
|
||||
<article className="relative flex min-h-[220px] flex-col rounded-lg border border-border bg-card p-4 hover:border-active-border">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="flex items-center gap-1 font-medium text-sm text-success-text">
|
||||
<ShieldCheckIcon className="size-4" />
|
||||
Audited
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-3.5" />
|
||||
|
||||
{/* Title */}
|
||||
<h3 className="font-semibold text-lg tracking-tight">
|
||||
<Link
|
||||
className="cursor-pointer before:absolute before:inset-0 before:z-0"
|
||||
href={props.link}
|
||||
onClick={() => {
|
||||
reportTokenUpsellClicked({
|
||||
assetType: props.trackingType,
|
||||
pageType: "explore",
|
||||
});
|
||||
}}
|
||||
>
|
||||
{props.name}
|
||||
</Link>
|
||||
</h3>
|
||||
|
||||
<p className="mt-1 text-muted-foreground text-sm leading-5">
|
||||
{props.description}
|
||||
</p>
|
||||
|
||||
<div className="mt-auto relative z-1 flex justify-between gap-2 pt-3">
|
||||
<Link
|
||||
className="flex shrink-0 items-center gap-1.5 hover:underline"
|
||||
href={`/${thirdwebEns.name}`}
|
||||
target="_blank"
|
||||
>
|
||||
<Img
|
||||
className="size-5 rounded-full object-cover"
|
||||
src={thirdwebEns.image}
|
||||
/>
|
||||
|
||||
<span className="text-xs"> {thirdwebEns.name}</span>
|
||||
</Link>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Button
|
||||
asChild
|
||||
className="relative z-10 h-auto gap-1.5 px-2.5 py-1.5 text-xs hover:bg-inverted hover:text-inverted-foreground"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
<Link href={props.link}>
|
||||
<RocketIcon className="size-3" />
|
||||
Deploy
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
export function NFTCollectionCard() {
|
||||
return (
|
||||
<TokenContractCard
|
||||
name="NFT Collection"
|
||||
description="Launch an ERC-721 or ERC-1155 NFT collection"
|
||||
link="/team/~/~project/tokens/create/nft"
|
||||
trackingType="nft"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function CoinCard() {
|
||||
return (
|
||||
<TokenContractCard
|
||||
name="Coin"
|
||||
description="Launch an ERC-20 token to create a cryptocurrency"
|
||||
link="/team/~/~project/tokens/create/token"
|
||||
trackingType="coin"
|
||||
/>
|
||||
);
|
||||
}
|
||||
+1
-20
@@ -5,31 +5,12 @@ import { PlainTextCodeBlock } from "@/components/ui/code/plaintext-code";
|
||||
export const DeployUpsellCard: React.FC = () => {
|
||||
return (
|
||||
<section className="rounded-xl border border-border px-4 py-6 shadow-lg md:px-8 md:py-14">
|
||||
<h3 className="font-bold text-2xl tracking-tight">
|
||||
<h3 className="font-semibold text-2xl tracking-tight">
|
||||
Didn't find what you're looking for? Build your own!
|
||||
</h3>
|
||||
|
||||
<div className="h-8" />
|
||||
|
||||
<p className="text-muted-foreground ">
|
||||
Create a contract with a single command.{" "}
|
||||
<Link
|
||||
className="inline-flex items-center gap-1.5 text-link-foreground"
|
||||
href="https://portal.thirdweb.com/contracts/build/overview"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more about the Solidity SDK
|
||||
<ExternalLinkIcon className="size-4" />
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
<div className="h-2" />
|
||||
|
||||
<PlainTextCodeBlock code="npx thirdweb create contract" />
|
||||
|
||||
<div className="h-8" />
|
||||
|
||||
<p className="text-muted-foreground ">
|
||||
Deploy a contract with a single command.{" "}
|
||||
<Link
|
||||
|
||||
-70
@@ -1,70 +0,0 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import exploreFeatureImage from "../../../../../../../public/assets/landingpage/explore-featured.png";
|
||||
import heroIcon1 from "../../../../../../../public/assets/product-pages/publish/hero-icon-1.png";
|
||||
import heroIcon2 from "../../../../../../../public/assets/product-pages/publish/hero-icon-2.png";
|
||||
|
||||
export const PublishUpsellCard: React.FC = () => {
|
||||
return (
|
||||
<div className="flex gap-10 rounded-xl border border-border bg-card p-8 shadow-lg md:p-10">
|
||||
<div className="flex flex-col gap-6">
|
||||
<h2 className="font-bold text-3xl tracking-tighter">
|
||||
Accelerate your protocol's growth
|
||||
</h2>
|
||||
|
||||
<p className="text-muted-foreground">
|
||||
Publishing your contract is the best way to get your contracts in
|
||||
front of our 70k+ community of web3 developers.
|
||||
</p>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Image alt="" className="hidden size-6 md:block" src={heroIcon1} />
|
||||
|
||||
<p className="text-muted-foreground">
|
||||
<span className="font-semibold text-foreground">
|
||||
Save development time.{" "}
|
||||
</span>
|
||||
Focus on protocol development and save time by not having to build
|
||||
middleware layer yourself.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Image alt="" className="hidden size-6 md:block" src={heroIcon2} />
|
||||
<p className="text-muted-foreground">
|
||||
<span className="font-semibold text-foreground">
|
||||
Shareable landing page.{" "}
|
||||
</span>
|
||||
By publishing your contract, your contracts become easily shareable
|
||||
with a landing page for your contract.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-auto flex gap-2 pt-4">
|
||||
<Button asChild>
|
||||
<Link href="/contact-us" rel="noopener noreferrer" target="_blank">
|
||||
Get In Touch
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<Button asChild variant="outline">
|
||||
<Link
|
||||
href="https://portal.thirdweb.com/contracts/publish/overview"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Learn More
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Image
|
||||
alt=""
|
||||
className="hidden w-[40%] grayscale invert md:block dark:filter-none"
|
||||
draggable={false}
|
||||
src={exploreFeatureImage}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,151 +1,55 @@
|
||||
type PublishedContractID = `${string}/${string}`;
|
||||
|
||||
export interface ExploreCategory {
|
||||
id: string;
|
||||
name: string;
|
||||
displayName?: string;
|
||||
description: string;
|
||||
description: string | undefined;
|
||||
learnMore?: string;
|
||||
contracts:
|
||||
| Array<PublishedContractID>
|
||||
| Array<
|
||||
[
|
||||
PublishedContractID,
|
||||
Array<PublishedContractID>,
|
||||
{ title: string; description: string },
|
||||
]
|
||||
>;
|
||||
showInExplore?: boolean;
|
||||
contracts: Array<
|
||||
| PublishedContractID
|
||||
| [
|
||||
PublishedContractID,
|
||||
Array<PublishedContractID>,
|
||||
{ title: string; description: string },
|
||||
]
|
||||
>;
|
||||
isBeta?: boolean;
|
||||
}
|
||||
|
||||
const POPULAR = {
|
||||
export const NFTS = {
|
||||
contracts: [
|
||||
"thirdweb.eth/DropERC721",
|
||||
"thirdweb.eth/MarketplaceV3",
|
||||
"thirdweb.eth/AccountFactory",
|
||||
"0xaF3202F6bAEbA50d37e0d4B0b870455EDF198D7c/BurnMintERC677",
|
||||
"thirdweb.eth/DropERC1155",
|
||||
"thirdweb.eth/TokenERC20",
|
||||
"thirdweb.eth/NFTStake",
|
||||
"unlock-protocol.eth/PublicLock",
|
||||
],
|
||||
description: "A collection of our most deployed contracts.",
|
||||
id: "popular",
|
||||
name: "Popular",
|
||||
} satisfies ExploreCategory;
|
||||
|
||||
const NFTS = {
|
||||
contracts: [
|
||||
"thirdweb.eth/DropERC721",
|
||||
"thirdweb.eth/OpenEditionERC721",
|
||||
"thirdweb.eth/TokenERC721",
|
||||
"thirdweb.eth/DropERC1155",
|
||||
"thirdweb.eth/TokenERC1155",
|
||||
"flairsdk.eth/ERC721CommunityStream",
|
||||
"thirdweb.eth/LoyaltyCard",
|
||||
"doubledev.eth/ERC4907",
|
||||
"thirdweb.eth/Multiwrap",
|
||||
"kronickatz.eth/ERC721NESDrop",
|
||||
],
|
||||
description:
|
||||
"NFT Collections, Editions, Drops and everything else NFT-related.",
|
||||
displayName: "NFTs",
|
||||
id: "nft",
|
||||
name: "NFT",
|
||||
} satisfies ExploreCategory;
|
||||
|
||||
const GOVERNANCE = {
|
||||
contracts: [
|
||||
"thirdweb.eth/VoteERC20",
|
||||
"thirdweb.eth/TokenERC20",
|
||||
"thirdweb.eth/Split",
|
||||
],
|
||||
export const GOVERNANCE = {
|
||||
contracts: ["thirdweb.eth/VoteERC20", "thirdweb.eth/Split"],
|
||||
description: "Create your own DAO, vote on proposals, and manage a treasury.",
|
||||
id: "daos-governance",
|
||||
name: "DAOs & Governance",
|
||||
} satisfies ExploreCategory;
|
||||
|
||||
const DROPS = {
|
||||
contracts: [
|
||||
"thirdweb.eth/DropERC721",
|
||||
"thirdweb.eth/DropERC1155",
|
||||
"thirdweb.eth/DropERC20",
|
||||
],
|
||||
description: "Release NFTs and Tokens based on preset Claim Conditions.",
|
||||
displayName: "Drops",
|
||||
id: "drops",
|
||||
name: "Drop",
|
||||
} satisfies ExploreCategory;
|
||||
|
||||
const MARKETS = {
|
||||
contracts: [
|
||||
"thirdweb.eth/MarketplaceV3",
|
||||
"thirdweb.eth/TokenERC20",
|
||||
"thirdweb.eth/Split",
|
||||
],
|
||||
export const MARKETS = {
|
||||
contracts: ["thirdweb.eth/MarketplaceV3", "thirdweb.eth/Split"],
|
||||
description: "Quickly spin up your own on-chain marketplace for NFTs.",
|
||||
displayName: "Marketplaces",
|
||||
id: "marketplace",
|
||||
name: "Marketplace",
|
||||
} satisfies ExploreCategory;
|
||||
|
||||
const AIRDROP = {
|
||||
export const AIRDROP = {
|
||||
contracts: ["thirdweb.eth/Airdrop"],
|
||||
description:
|
||||
"Efficiently transfer large numbers of on-chain assets to a large number of recipients.",
|
||||
displayName: "Airdrops",
|
||||
id: "airdrop",
|
||||
name: "Airdrop",
|
||||
} satisfies ExploreCategory;
|
||||
|
||||
const GAMING = {
|
||||
contracts: [
|
||||
"thirdweb.eth/MarketplaceV3",
|
||||
"thirdweb.eth/DropERC721",
|
||||
"thirdweb.eth/TokenERC20",
|
||||
"thirdweb.eth/TokenERC1155",
|
||||
"thirdweb.eth/Multiwrap",
|
||||
"thirdweb.eth/NFTStake",
|
||||
],
|
||||
description:
|
||||
"A collection of contracts that are popular for building play-to-earn and free-to-own web3 games.",
|
||||
displayName: "Gaming",
|
||||
id: "gaming",
|
||||
name: "Gaming",
|
||||
showInExplore: false,
|
||||
} satisfies ExploreCategory;
|
||||
|
||||
const LOYALTY = {
|
||||
contracts: [
|
||||
"thirdweb.eth/LoyaltyCard",
|
||||
"thirdweb.eth/MarketplaceV3",
|
||||
"thirdweb.eth/TokenERC20",
|
||||
],
|
||||
description:
|
||||
"A collection of contracts that are popular for building loyalty programs.",
|
||||
displayName: "Loyalty",
|
||||
id: "loyalty",
|
||||
name: "Loyalty",
|
||||
showInExplore: false,
|
||||
} satisfies ExploreCategory;
|
||||
|
||||
const COMMERCE = {
|
||||
contracts: [
|
||||
"thirdweb.eth/TokenERC721",
|
||||
"thirdweb.eth/TokenERC1155",
|
||||
"thirdweb.eth/TokenERC20",
|
||||
"thirdweb.eth/Split",
|
||||
],
|
||||
description:
|
||||
"Most popular contracts for building web3 commerce apps. Reward loyal customers and sell NFTs through your storefront.",
|
||||
displayName: "Commerce",
|
||||
id: "commerce",
|
||||
name: "Commerce",
|
||||
showInExplore: false,
|
||||
} satisfies ExploreCategory;
|
||||
|
||||
const STAKING = {
|
||||
export const STAKING = {
|
||||
contracts: [
|
||||
"thirdweb.eth/NFTStake",
|
||||
"thirdweb.eth/EditionStake",
|
||||
@@ -153,12 +57,10 @@ const STAKING = {
|
||||
],
|
||||
description: "Stake your NFTs or tokens to earn ERC20 tokens in return.",
|
||||
displayName: "Staking",
|
||||
id: "staking",
|
||||
name: "Staking",
|
||||
showInExplore: true,
|
||||
} satisfies ExploreCategory;
|
||||
|
||||
const SMART_WALLET = {
|
||||
export const SMART_WALLET = {
|
||||
contracts: [
|
||||
"thirdweb.eth/AccountFactory",
|
||||
"thirdweb.eth/ManagedAccountFactory",
|
||||
@@ -168,56 +70,58 @@ const SMART_WALLET = {
|
||||
description:
|
||||
"Account factories that let you spin up Account Abstraction (ERC-4337) wallets for your users. Not sure which factory is right for you?",
|
||||
displayName: "Account Abstraction",
|
||||
id: "smart-wallet",
|
||||
learnMore:
|
||||
"https://portal.thirdweb.com/wallets/smart-wallet/get-started#1-deploy-a-smart-wallet-factory-contract",
|
||||
name: "Account Abstraction",
|
||||
showInExplore: true,
|
||||
} satisfies ExploreCategory;
|
||||
|
||||
const STYLUS = {
|
||||
export const STYLUS = {
|
||||
contracts: [
|
||||
"0x6453a486d52e0EB6E79Ec4491038E2522a926936/StylusAirdropERC1155",
|
||||
"0x6453a486d52e0EB6E79Ec4491038E2522a926936/StylusAirdropERC721",
|
||||
"0x6453a486d52e0EB6E79Ec4491038E2522a926936/StylusAirdropERC20",
|
||||
// erc721 token
|
||||
[
|
||||
"thirdweb.eth/ERC721CoreInitializable",
|
||||
[
|
||||
"0x6453a486d52e0eb6e79ec4491038e2522a926936/StylusMintableERC721",
|
||||
"deployer.thirdweb.eth/BatchMetadataERC721",
|
||||
"0x6453a486d52e0eb6e79ec4491038e2522a926936/StylusTransferableERC721",
|
||||
],
|
||||
{
|
||||
description: "ERC721 NFTs that only owners can mint.",
|
||||
title: "Modular NFT Collection",
|
||||
},
|
||||
],
|
||||
// erc1155 token
|
||||
[
|
||||
"thirdweb.eth/ERC1155CoreInitializable",
|
||||
[
|
||||
"0x6453a486d52e0eb6e79ec4491038e2522a926936/StylusMintableERC1155",
|
||||
"deployer.thirdweb.eth/BatchMetadataERC1155",
|
||||
"0x6453a486d52e0eb6e79ec4491038e2522a926936/StylusTransferableERC1155",
|
||||
"deployer.thirdweb.eth/SequentialTokenIdERC1155",
|
||||
],
|
||||
{
|
||||
description: "ERC1155 NFTs that only owners can mint.",
|
||||
title: "Modular Edition",
|
||||
},
|
||||
],
|
||||
// erc20 token
|
||||
[
|
||||
"thirdweb.eth/ERC20CoreInitializable",
|
||||
[
|
||||
"0x6453a486d52e0eb6e79ec4491038e2522a926936/StylusMintableERC20",
|
||||
"0x6453a486d52e0eb6e79ec4491038e2522a926936/StylusTransferableERC20",
|
||||
],
|
||||
{
|
||||
description: "ERC20 Tokens that only owners can mint.",
|
||||
title: "Modular Token",
|
||||
},
|
||||
],
|
||||
],
|
||||
description:
|
||||
"Airdrop your NFTs or tokens to a large number of recipients. Built with Arbitrum Stylus.",
|
||||
displayName: "Arbitrum Stylus Contracts",
|
||||
id: "stylus",
|
||||
description: undefined,
|
||||
displayName: "Arbitrum Stylus",
|
||||
name: "Stylus",
|
||||
showInExplore: true,
|
||||
isBeta: true,
|
||||
} satisfies ExploreCategory;
|
||||
|
||||
const CATEGORIES: Record<string, ExploreCategory> = {
|
||||
[POPULAR.id]: POPULAR,
|
||||
[NFTS.id]: NFTS,
|
||||
[MARKETS.id]: MARKETS,
|
||||
[STYLUS.id]: STYLUS,
|
||||
[DROPS.id]: DROPS,
|
||||
[SMART_WALLET.id]: SMART_WALLET,
|
||||
[AIRDROP.id]: AIRDROP,
|
||||
[GAMING.id]: GAMING,
|
||||
[LOYALTY.id]: LOYALTY,
|
||||
[COMMERCE.id]: COMMERCE,
|
||||
[STAKING.id]: STAKING,
|
||||
[GOVERNANCE.id]: GOVERNANCE,
|
||||
};
|
||||
|
||||
export function getCategory(id: string) {
|
||||
if (isExploreCategory(id)) {
|
||||
return CATEGORIES[id];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
type ExploreCategoryName = keyof typeof CATEGORIES;
|
||||
|
||||
function isExploreCategory(category: string): category is ExploreCategoryName {
|
||||
return category in CATEGORIES;
|
||||
}
|
||||
|
||||
export const EXPLORE_PAGE_DATA = Object.values(CATEGORIES).filter((v) =>
|
||||
"showInExplore" in v ? v.showInExplore !== false : true,
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user