Compare commits
93
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5bbecd182 | ||
|
|
bafa0e83c3 | ||
|
|
5dc1899541 | ||
|
|
4b183518af | ||
|
|
e8f952a191 | ||
|
|
132ccd3535 | ||
|
|
f23911e1da | ||
|
|
cd22bca3d8 | ||
|
|
e4b6d87dd8 | ||
|
|
158c2b6e8e | ||
|
|
52229be96a | ||
|
|
012f3defda | ||
|
|
8e43e340a7 | ||
|
|
74e52c000e | ||
|
|
120f965941 | ||
|
|
cf36a33c3f | ||
|
|
2fdb69dd44 | ||
|
|
06c0cf3898 | ||
|
|
47b1bbbd7c | ||
|
|
53e9d8bc1d | ||
|
|
9fadbcc17a | ||
|
|
c04b93e705 | ||
|
|
e6e994479f | ||
|
|
cf61115064 | ||
|
|
60d2190e95 | ||
|
|
3cf0039c58 | ||
|
|
4841ff382a | ||
|
|
d580fe05e3 | ||
|
|
7a1099b601 | ||
|
|
4e5fdcf415 | ||
|
|
eaeb53218c | ||
|
|
09b7404985 | ||
|
|
3c77c6b2c8 | ||
|
|
71aa2578c3 | ||
|
|
c32576fb47 | ||
|
|
d522343e04 | ||
|
|
4f22e2b190 | ||
|
|
319a203a4e | ||
|
|
cdb907f175 | ||
|
|
364d97e97e | ||
|
|
795e9c13a4 | ||
|
|
0b510f881b | ||
|
|
5351d4e407 | ||
|
|
0cafa33a18 | ||
|
|
dd77c7f57c | ||
|
|
9c3ff94f29 | ||
|
|
45fcfb1dae | ||
|
|
788eefd1d5 | ||
|
|
f8c981c0ca | ||
|
|
1628fa2a79 | ||
|
|
0e26a88e7f | ||
|
|
50f98d7486 | ||
|
|
40aeb529ac | ||
|
|
e7bbc321c2 | ||
|
|
8f157aa43f | ||
|
|
a437baa57a | ||
|
|
4331ed1f04 | ||
|
|
5c896b0fd5 | ||
|
|
16aa7b7918 | ||
|
|
341d312679 | ||
|
|
3aff548a10 | ||
|
|
002c545d0e | ||
|
|
bc5b543269 | ||
|
|
1a45f21abb | ||
|
|
d59ab13ce8 | ||
|
|
221f95b4f8 | ||
|
|
223c497f49 | ||
|
|
541bee5b34 | ||
|
|
df128bfc17 | ||
|
|
f2d10853f1 | ||
|
|
a68830ca67 | ||
|
|
5da500edca | ||
|
|
4542a48933 | ||
|
|
17127d7763 | ||
|
|
0dbb174e50 | ||
|
|
0a1d59d789 | ||
|
|
a04cda1e5a | ||
|
|
67a363f624 | ||
|
|
1cd11a86f7 | ||
|
|
9da5cd7d49 | ||
|
|
0dd1438012 | ||
|
|
0f4e6622e0 | ||
|
|
e67a300410 | ||
|
|
00a6a56158 | ||
|
|
7855c3ea8a | ||
|
|
6597cb6525 | ||
|
|
fbd9853697 | ||
|
|
b01faacd99 | ||
|
|
ef6b4f48ef | ||
|
|
c5a2cdb3b6 | ||
|
|
5dd0c91d3e | ||
|
|
f8f90fd3ab | ||
|
|
63037f1bf1 |
@@ -29,3 +29,5 @@ packages/*/typedoc/*
|
||||
*storybook.log
|
||||
storybook-static
|
||||
.aider*
|
||||
|
||||
tsconfig.tsbuildinfo
|
||||
@@ -96,4 +96,8 @@ UNTHREAD_PRO_TIER_ID=""
|
||||
NEXT_PUBLIC_DEMO_ENGINE_URL=""
|
||||
|
||||
# API server secret (required for thirdweb.com SIWE login). Copy from Vercel.
|
||||
API_SERVER_SECRET=""
|
||||
API_SERVER_SECRET=""
|
||||
|
||||
# Used for the Faucet page (/<chain_id>)
|
||||
NEXT_PUBLIC_TURNSTILE_SITE_KEY=""
|
||||
TURNSTILE_SECRET_KEY=""
|
||||
@@ -34,6 +34,11 @@ module.exports = {
|
||||
message:
|
||||
"Use useV5DashboardChain instead if you are using it inside a component",
|
||||
},
|
||||
{
|
||||
selector: "CallExpression[callee.name='resolveScheme']",
|
||||
message:
|
||||
"resolveScheme can throw error if resolution fails. Either catch the error and ignore the lint warning or Use `resolveSchemeWithErrorHandler` / `replaceIpfsUrl` utility in dashboard instead",
|
||||
},
|
||||
],
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// add framer paths here
|
||||
module.exports = [
|
||||
"/",
|
||||
"/connect/sign-in",
|
||||
"/contracts/modular-contracts",
|
||||
"/unlimited-wallets",
|
||||
];
|
||||
@@ -1,4 +1,5 @@
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@@ -77,8 +78,16 @@ module.exports = {
|
||||
};
|
||||
},
|
||||
additionalPaths: async (config) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const FRAMER_PATHS = require("./framer-rewrites");
|
||||
const allChains = await fetchChainsFromApi();
|
||||
return [
|
||||
...FRAMER_PATHS.map((path) => ({
|
||||
loc: path,
|
||||
changefreq: config.changefreq,
|
||||
priority: config.priority,
|
||||
lastmod: config.autoLastmod ? new Date().toISOString() : undefined,
|
||||
})),
|
||||
...allChains.map((chain) => {
|
||||
return {
|
||||
loc: `/${chain.slug}`,
|
||||
|
||||
@@ -6,9 +6,9 @@ const ContentSecurityPolicy = `
|
||||
media-src * data: blob:;
|
||||
object-src 'none';
|
||||
style-src 'self' 'unsafe-inline' vercel.live;
|
||||
font-src 'self' vercel.live assets.vercel.com framerusercontent.com;
|
||||
font-src 'self' vercel.live assets.vercel.com framerusercontent.com fonts.gstatic.com;
|
||||
frame-src * data:;
|
||||
script-src 'self' 'unsafe-eval' 'unsafe-inline' 'wasm-unsafe-eval' 'inline-speculation-rules' *.thirdweb.com *.thirdweb-dev.com vercel.live js.stripe.com framerusercontent.com events.framer.com;
|
||||
script-src 'self' 'unsafe-eval' 'unsafe-inline' 'wasm-unsafe-eval' 'inline-speculation-rules' *.thirdweb.com *.thirdweb-dev.com vercel.live js.stripe.com framerusercontent.com events.framer.com challenges.cloudflare.com;
|
||||
connect-src * data: blob:;
|
||||
worker-src 'self' blob:;
|
||||
block-all-mixed-content;
|
||||
@@ -38,13 +38,7 @@ const securityHeaders = [
|
||||
];
|
||||
|
||||
const redirects = require("./redirects");
|
||||
|
||||
// add framer paths here
|
||||
const FRAMER_PATHS = [
|
||||
"/connect/sign-in",
|
||||
"/contracts/modular-contracts",
|
||||
"/unlimited-wallets",
|
||||
];
|
||||
const FRAMER_PATHS = require("./framer-rewrites");
|
||||
|
||||
/**
|
||||
* @returns {import('next').RemotePattern[]}
|
||||
@@ -135,6 +129,11 @@ const moduleExports = {
|
||||
source: "/thirdweb.eth/:path*",
|
||||
destination: "/deployer.thirdweb.eth/:path*",
|
||||
},
|
||||
// re-write /home to / (this is so that logged in users will be able to go to /home and NOT be redirected to the logged in app)
|
||||
{
|
||||
source: "/home",
|
||||
destination: "/",
|
||||
},
|
||||
...FRAMER_PATHS.map((path) => ({
|
||||
source: path,
|
||||
destination: `https://landing.thirdweb.com${path}`,
|
||||
@@ -191,7 +190,7 @@ module.exports = withBundleAnalyzer(
|
||||
// An auth token is required for uploading source maps.
|
||||
authToken: process.env.SENTRY_AUTH_TOKEN,
|
||||
// Suppresses source map uploading logs during build
|
||||
silent: false,
|
||||
silent: true,
|
||||
// For all available options, see:
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
|
||||
|
||||
|
||||
+18
-17
@@ -29,6 +29,7 @@
|
||||
"@emotion/react": "11.13.3",
|
||||
"@emotion/styled": "11.13.0",
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@marsidev/react-turnstile": "^1.0.2",
|
||||
"@n8tb1t/use-scroll-position": "^2.0.3",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.2",
|
||||
"@radix-ui/react-avatar": "^1.1.1",
|
||||
@@ -46,10 +47,10 @@
|
||||
"@radix-ui/react-switch": "^1.1.1",
|
||||
"@radix-ui/react-tooltip": "1.1.3",
|
||||
"@sentry/nextjs": "8.34.0",
|
||||
"@shazow/whatsabi": "^0.15.3",
|
||||
"@shazow/whatsabi": "^0.15.4",
|
||||
"@stripe/react-stripe-js": "^2.8.1",
|
||||
"@stripe/stripe-js": "^3.5.0",
|
||||
"@tanstack/react-query": "5.59.13",
|
||||
"@tanstack/react-query": "5.59.16",
|
||||
"@tanstack/react-table": "^8.17.3",
|
||||
"@thirdweb-dev/service-utils": "workspace:*",
|
||||
"@vercel/og": "^0.6.2",
|
||||
@@ -61,13 +62,13 @@
|
||||
"compare-versions": "^6.1.0",
|
||||
"date-fns": "4.1.0",
|
||||
"flat": "^6.0.1",
|
||||
"framer-motion": "11.11.8",
|
||||
"framer-motion": "11.11.9",
|
||||
"fuse.js": "7.0.0",
|
||||
"input-otp": "^1.2.4",
|
||||
"ioredis": "^5.4.1",
|
||||
"ipaddr.js": "^2.2.0",
|
||||
"lottie-react": "^2.4.0",
|
||||
"lucide-react": "0.452.0",
|
||||
"lucide-react": "0.453.0",
|
||||
"next": "14.2.15",
|
||||
"next-plausible": "^3.12.0",
|
||||
"next-seo": "^6.5.0",
|
||||
@@ -84,8 +85,8 @@
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-dropzone": "^14.2.9",
|
||||
"react-error-boundary": "^4.1.2",
|
||||
"react-hook-form": "7.52.0",
|
||||
"react-intersection-observer": "^9.10.3",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-responsive-carousel": "^3.2.23",
|
||||
"react-table": "^7.8.0",
|
||||
@@ -108,18 +109,18 @@
|
||||
"@next/bundle-analyzer": "14.2.15",
|
||||
"@next/eslint-plugin-next": "14.2.15",
|
||||
"@playwright/test": "1.47.2",
|
||||
"@storybook/addon-essentials": "8.3.5",
|
||||
"@storybook/addon-interactions": "8.3.5",
|
||||
"@storybook/addon-links": "8.3.5",
|
||||
"@storybook/addon-onboarding": "8.3.5",
|
||||
"@storybook/addon-viewport": "8.3.5",
|
||||
"@storybook/blocks": "8.3.5",
|
||||
"@storybook/nextjs": "8.3.5",
|
||||
"@storybook/react": "8.3.5",
|
||||
"@storybook/test": "8.3.5",
|
||||
"@storybook/addon-essentials": "8.3.6",
|
||||
"@storybook/addon-interactions": "8.3.6",
|
||||
"@storybook/addon-links": "8.3.6",
|
||||
"@storybook/addon-onboarding": "8.3.6",
|
||||
"@storybook/addon-viewport": "8.3.6",
|
||||
"@storybook/blocks": "8.3.6",
|
||||
"@storybook/nextjs": "8.3.6",
|
||||
"@storybook/react": "8.3.6",
|
||||
"@storybook/test": "8.3.6",
|
||||
"@types/color": "^3.0.6",
|
||||
"@types/node": "20.14.9",
|
||||
"@types/papaparse": "^5.3.14",
|
||||
"@types/papaparse": "^5.3.15",
|
||||
"@types/pluralize": "^0.0.33",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/react": "^18.3.11",
|
||||
@@ -138,8 +139,8 @@
|
||||
"knip": "^5.33.3",
|
||||
"next-sitemap": "^4.2.3",
|
||||
"postcss": "8.4.47",
|
||||
"storybook": "8.3.5",
|
||||
"tailwindcss": "3.4.13",
|
||||
"storybook": "8.3.6",
|
||||
"tailwindcss": "3.4.14",
|
||||
"typescript": "5.6.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,13 +119,7 @@ export function AppFooter(props: AppFooterProps) {
|
||||
>
|
||||
Terms of Service
|
||||
</Link>
|
||||
<Link
|
||||
className="px-[10px] py-[6px] text-muted-foreground text-sm hover:underline"
|
||||
href="https://thirdweb.com/gas"
|
||||
target="_blank"
|
||||
>
|
||||
Gas Estimator
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
className="px-[10px] py-[6px] text-muted-foreground text-sm hover:underline"
|
||||
href="https://thirdweb.com/chainlist"
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export function UnexpectedValueErrorMessage(props: {
|
||||
value: unknown;
|
||||
title: string;
|
||||
description: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const stringifiedValue = useMemo(() => {
|
||||
try {
|
||||
return JSON.stringify(props.value, null, 2);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}, [props.value]);
|
||||
|
||||
return (
|
||||
<div className={props.className}>
|
||||
<p className="text-base text-destructive-text"> {props.title} </p>
|
||||
<p className="text-foreground text-sm">{props.description}</p>
|
||||
{stringifiedValue && (
|
||||
<div className="mt-3">
|
||||
<p className="mb-0.5 text-muted-foreground text-sm">Value Received</p>
|
||||
<ScrollShadow className="rounded-lg bg-muted/50">
|
||||
<code className="block whitespace-pre p-4 font-mono text-xs">
|
||||
{stringifiedValue}
|
||||
</code>
|
||||
</ScrollShadow>
|
||||
|
||||
<CopyTextButton
|
||||
copyIconPosition="left"
|
||||
textToShow="Copy value"
|
||||
textToCopy={stringifiedValue}
|
||||
tooltip="Copy value"
|
||||
className="-translate-x-2 mt-1 text-muted-foreground"
|
||||
variant="ghost"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -119,13 +119,18 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
|
||||
if (filteredOptions.length >= itemsToShow) {
|
||||
break;
|
||||
}
|
||||
const option = options[i];
|
||||
if (!option) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (overrideSearchFn) {
|
||||
if (overrideSearchFn(options[i], searchValLowercase)) {
|
||||
filteredOptions.push(options[i]);
|
||||
if (overrideSearchFn(option, searchValLowercase)) {
|
||||
filteredOptions.push(option);
|
||||
}
|
||||
} else {
|
||||
if (options[i].label.toLowerCase().includes(searchValLowercase)) {
|
||||
filteredOptions.push(options[i]);
|
||||
if (option.label.toLowerCase().includes(searchValLowercase)) {
|
||||
filteredOptions.push(option);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,14 +63,18 @@ export const SelectWithSearch = React.forwardRef<
|
||||
if (filteredOptions.length >= itemsToShow) {
|
||||
break;
|
||||
}
|
||||
const option = options[i];
|
||||
if (!option) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (overrideSearchFn) {
|
||||
if (overrideSearchFn(options[i], searchValLowercase)) {
|
||||
filteredOptions.push(options[i]);
|
||||
if (overrideSearchFn(option, searchValLowercase)) {
|
||||
filteredOptions.push(option);
|
||||
}
|
||||
} else {
|
||||
if (options[i].label.toLowerCase().includes(searchValLowercase)) {
|
||||
filteredOptions.push(options[i]);
|
||||
if (option.label.toLowerCase().includes(searchValLowercase)) {
|
||||
filteredOptions.push(option);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"use client";
|
||||
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import {
|
||||
HoverCard,
|
||||
@@ -7,6 +6,7 @@ import {
|
||||
HoverCardTrigger,
|
||||
} from "@/components/ui/hover-card";
|
||||
import { useThirdwebClient } from "@/constants/thirdweb.client";
|
||||
import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler";
|
||||
import { useClipboard } from "hooks/useClipboard";
|
||||
import { Check, Copy, ExternalLinkIcon } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
type SocialProfile,
|
||||
useSocialProfiles,
|
||||
} from "thirdweb/react";
|
||||
import { resolveScheme } from "thirdweb/storage";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { Badge } from "../ui/badge";
|
||||
import { Button } from "../ui/button";
|
||||
@@ -113,22 +112,20 @@ export function WalletAddress(props: {
|
||||
) : !profiles.data?.length ? (
|
||||
<p className="text-muted-foreground text-sm">No profiles found</p>
|
||||
) : (
|
||||
profiles.data?.map((profile) => (
|
||||
<div
|
||||
className="flex flex-row items-center gap-2"
|
||||
key={profile.type + profile.name}
|
||||
>
|
||||
{profile.avatar &&
|
||||
(profile.avatar.startsWith("http") ||
|
||||
profile.avatar?.startsWith("ipfs")) && (
|
||||
profiles.data?.map((profile) => {
|
||||
const walletAvatarLink = resolveSchemeWithErrorHandler({
|
||||
client: thirdwebClient,
|
||||
uri: profile.avatar,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-row items-center gap-2"
|
||||
key={profile.type + profile.name}
|
||||
>
|
||||
{walletAvatarLink && (
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
src={resolveScheme({
|
||||
client: thirdwebClient,
|
||||
uri: profile.avatar,
|
||||
})}
|
||||
alt={profile.name}
|
||||
/>
|
||||
<AvatarImage src={walletAvatarLink} alt={profile.name} />
|
||||
{profile.name && (
|
||||
<AvatarFallback>
|
||||
{profile.name.slice(0, 2)}
|
||||
@@ -136,19 +133,20 @@ export function WalletAddress(props: {
|
||||
)}
|
||||
</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>
|
||||
<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>
|
||||
</div>
|
||||
{profile.bio && (
|
||||
<p className="line-clamp-1 whitespace-normal text-muted-foreground text-sm">
|
||||
{profile.bio}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{profile.bio && (
|
||||
<p className="line-clamp-1 whitespace-normal text-muted-foreground text-sm">
|
||||
{profile.bio}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
);
|
||||
})
|
||||
)}
|
||||
<Button
|
||||
asChild
|
||||
|
||||
@@ -143,7 +143,7 @@ const ChartTooltipContent = React.forwardRef<
|
||||
}
|
||||
|
||||
const [item] = payload;
|
||||
const key = `${labelKey || item.dataKey || item.name || "value"}`;
|
||||
const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
const value =
|
||||
!labelKey && typeof label === "string"
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
"use client";
|
||||
|
||||
import { useClipboard } from "hooks/useClipboard";
|
||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { Button } from "./button";
|
||||
|
||||
export function PlainTextCodeBlock(props: {
|
||||
code: string;
|
||||
copyButtonClassName?: string;
|
||||
}) {
|
||||
const { hasCopied, onCopy } = useClipboard(props.code);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<code className="block whitespace-pre rounded-lg border border-border bg-muted/50 p-4">
|
||||
{props.code}
|
||||
</code>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={onCopy}
|
||||
className={cn(
|
||||
"absolute top-3.5 right-3.5 h-auto p-2",
|
||||
props.copyButtonClassName,
|
||||
)}
|
||||
>
|
||||
{hasCopied ? (
|
||||
<CheckIcon className="size-3 text-green-500" />
|
||||
) : (
|
||||
<CopyIcon className="size-3 text-muted-foreground" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -16,7 +16,9 @@ const ImageUpload = React.forwardRef<HTMLInputElement, ImageUploadProps>(
|
||||
const [activeFile, setActiveFile] = React.useState<File | null>(null);
|
||||
const onDrop = React.useCallback(
|
||||
(acceptedFiles: File[]) => {
|
||||
setActiveFile(acceptedFiles[0]);
|
||||
if (acceptedFiles[0]) {
|
||||
setActiveFile(acceptedFiles[0]);
|
||||
}
|
||||
onUpload?.(acceptedFiles);
|
||||
},
|
||||
[onUpload],
|
||||
|
||||
@@ -35,7 +35,8 @@ const InputOTPSlot = React.forwardRef<
|
||||
React.ComponentPropsWithoutRef<"div"> & { index: number }
|
||||
>(({ index, className, ...props }, ref) => {
|
||||
const inputOTPContext = React.useContext(OTPInputContext);
|
||||
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
|
||||
// biome-ignore lint/style/noNonNullAssertion: pure shadcn component - it works
|
||||
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]!;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -62,7 +63,7 @@ const InputOTPSeparator = React.forwardRef<
|
||||
React.ElementRef<"div">,
|
||||
React.ComponentPropsWithoutRef<"div">
|
||||
>(({ ...props }, ref) => (
|
||||
// biome-ignore lint/a11y/useFocusableInteractive: <explanation>
|
||||
// biome-ignore lint/a11y/useFocusableInteractive: pure shadcn component - it works
|
||||
<div ref={ref} role="separator" {...props}>
|
||||
<Dot />
|
||||
</div>
|
||||
|
||||
@@ -51,12 +51,12 @@ const RadioGroupItemButton = React.forwardRef<
|
||||
<RadioGroupPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"group flex min-w-32 cursor-pointer items-center space-x-3 space-y-0 rounded-md border-2 px-3 py-4 font-medium transition-all hover:border-foreground/25 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 peer-hover:border-nonce data-[state=checked]:border-inverted",
|
||||
"group flex min-w-32 cursor-pointer items-center space-x-3 space-y-0 rounded-lg border px-4 py-3 font-medium transition-all hover:border-foreground/25 hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 peer-hover:border-nonce data-[state=checked]:border-muted-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex aspect-square h-4 w-4 items-center justify-center rounded-full border-2 text-inverted ring-offset-background transition-all focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 group-hover:border-foreground/25 group-data-[state=checked]:border-inverted">
|
||||
<div className="flex aspect-square h-4 w-4 items-center justify-center rounded-full border text-inverted ring-offset-background transition-all focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 group-hover:border-foreground/25 group-data-[state=checked]:border-inverted">
|
||||
{/* Show on checked */}
|
||||
<RadioGroupPrimitive.Indicator className="flex items-center justify-center rounded-full transition-all">
|
||||
<Circle className="h-2 w-2 fill-current text-current" />
|
||||
|
||||
@@ -5,7 +5,6 @@ const LOGGED_IN_ONLY_PATHS = [
|
||||
"/team",
|
||||
// anything that _starts_ with /cli is logged in only
|
||||
"/cli",
|
||||
"/support",
|
||||
// publish page
|
||||
"/contracts/publish",
|
||||
];
|
||||
|
||||
@@ -23,3 +23,6 @@ export const DASHBOARD_STORAGE_URL =
|
||||
|
||||
export const API_SERVER_URL =
|
||||
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com";
|
||||
|
||||
export const TURNSTILE_SITE_KEY =
|
||||
process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY || "";
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { resolveScheme } from "thirdweb/storage";
|
||||
|
||||
export function resolveSchemeWithErrorHandler(options: {
|
||||
uri: string | undefined;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
if (!options.uri) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
return resolveScheme({
|
||||
uri: options.uri,
|
||||
client: options.client,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("error resolving ipfs url", options.uri, err);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -44,13 +44,12 @@ export function useActivity(contract: ThirdwebContract, autoUpdate?: boolean) {
|
||||
}
|
||||
const obj = eventsQuery.data.slice(0, 100).reduce(
|
||||
(acc, curr) => {
|
||||
if (acc[curr.transactionHash]) {
|
||||
acc[curr.transactionHash].events.push(curr);
|
||||
acc[curr.transactionHash].events.sort(
|
||||
(a, b) => b.logIndex - a.logIndex,
|
||||
);
|
||||
if (acc[curr.transactionHash].blockNumber > curr.blockNumber) {
|
||||
acc[curr.transactionHash].blockNumber = curr.blockNumber;
|
||||
const internalTx = acc[curr.transactionHash];
|
||||
if (internalTx) {
|
||||
internalTx.events.push(curr);
|
||||
internalTx.events.sort((a, b) => b.logIndex - a.logIndex);
|
||||
if (internalTx.blockNumber > curr.blockNumber) {
|
||||
internalTx.blockNumber = curr.blockNumber;
|
||||
}
|
||||
} else {
|
||||
acc[curr.transactionHash] = {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import { THIRDWEB_ANALYTICS_API_HOST, THIRDWEB_API_HOST } from "constants/urls";
|
||||
import { useAllChainsData } from "hooks/chains/allChains";
|
||||
import invariant from "tiny-invariant";
|
||||
import { accountKeys, apiKeys, authorizedWallets } from "../cache-keys";
|
||||
import { useLoggedInUser } from "./useLoggedInUser";
|
||||
@@ -241,6 +242,14 @@ export interface UserOpStats {
|
||||
sponsoredUsd: number;
|
||||
}
|
||||
|
||||
export interface UserOpStatsByChain {
|
||||
date: string;
|
||||
successful: number;
|
||||
failed: number;
|
||||
sponsoredUsd: number;
|
||||
chainId?: string;
|
||||
}
|
||||
|
||||
interface BillingProduct {
|
||||
name: string;
|
||||
id: string;
|
||||
@@ -434,8 +443,9 @@ export function useUserOpUsageAggregate(args: {
|
||||
}) {
|
||||
const { clientId, from, to } = args;
|
||||
const { user, isLoggedIn } = useLoggedInUser();
|
||||
const chainStore = useAllChainsData();
|
||||
|
||||
return useQuery({
|
||||
return useQuery<UserOpStats>({
|
||||
queryKey: accountKeys.userOpStats(
|
||||
user?.address as string,
|
||||
clientId as string,
|
||||
@@ -444,12 +454,37 @@ export function useUserOpUsageAggregate(args: {
|
||||
"all",
|
||||
),
|
||||
queryFn: async () => {
|
||||
return getUserOpUsage({
|
||||
clientId,
|
||||
from,
|
||||
to,
|
||||
period: "all",
|
||||
});
|
||||
const userOpStats: (UserOpStats & { chainId?: string })[] =
|
||||
await getUserOpUsage({
|
||||
clientId,
|
||||
from,
|
||||
to,
|
||||
period: "all",
|
||||
});
|
||||
|
||||
// Aggregate stats across wallet types
|
||||
return userOpStats.reduce(
|
||||
(acc, curr) => {
|
||||
// Skip testnets from the aggregated stats
|
||||
if (curr.chainId) {
|
||||
const chain = chainStore.idToChain.get(Number(curr.chainId));
|
||||
if (chain?.testnet) {
|
||||
return acc;
|
||||
}
|
||||
}
|
||||
|
||||
acc.successful += curr.successful;
|
||||
acc.failed += curr.failed;
|
||||
acc.sponsoredUsd += curr.sponsoredUsd;
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
date: (from || new Date()).toISOString(),
|
||||
successful: 0,
|
||||
failed: 0,
|
||||
sponsoredUsd: 0,
|
||||
},
|
||||
);
|
||||
},
|
||||
enabled: !!clientId && !!user?.address && isLoggedIn,
|
||||
});
|
||||
|
||||
@@ -48,7 +48,7 @@ export async function fetchDashboardContractMetadata(
|
||||
if (compilerMetadata?.name.includes(":")) {
|
||||
const _name = compilerMetadata.name.split(":")[1];
|
||||
if (_name) {
|
||||
compilerMetadata.name = compilerMetadata.name.split(":")[1];
|
||||
compilerMetadata.name = _name;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+56
-22
@@ -2,17 +2,24 @@
|
||||
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { THIRDWEB_ENGINE_FAUCET_WALLET } from "@/constants/env";
|
||||
import { Form, FormControl, FormField, FormItem } from "@/components/ui/form";
|
||||
import {
|
||||
THIRDWEB_ENGINE_FAUCET_WALLET,
|
||||
TURNSTILE_SITE_KEY,
|
||||
} from "@/constants/env";
|
||||
import { useThirdwebClient } from "@/constants/thirdweb.client";
|
||||
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
|
||||
import { Turnstile } from "@marsidev/react-turnstile";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { CanClaimResponseType } from "app/api/testnet-faucet/can-claim/CanClaimResponseType";
|
||||
import { mapV4ChainToV5Chain } from "contexts/map-chains";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { toUnits } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { useActiveAccount, useWalletBalance } from "thirdweb/react";
|
||||
import { z } from "zod";
|
||||
|
||||
function formatTime(seconds: number) {
|
||||
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
|
||||
@@ -29,12 +36,18 @@ function formatTime(seconds: number) {
|
||||
return rtf.format(+seconds, "second");
|
||||
}
|
||||
|
||||
const claimFaucetSchema = z.object({
|
||||
turnstileToken: z.string().min(1, {
|
||||
message: "Captcha validation is required.",
|
||||
}),
|
||||
});
|
||||
|
||||
export function FaucetButton({
|
||||
chain,
|
||||
amount,
|
||||
}: {
|
||||
chain: ChainMetadata;
|
||||
amount: string;
|
||||
amount: number;
|
||||
}) {
|
||||
const client = useThirdwebClient();
|
||||
const address = useActiveAccount()?.address;
|
||||
@@ -52,7 +65,7 @@ export function FaucetButton({
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const claimMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
mutationFn: async (turnstileToken: string) => {
|
||||
trackEvent({
|
||||
category: "faucet",
|
||||
action: "claim",
|
||||
@@ -67,6 +80,7 @@ export function FaucetButton({
|
||||
body: JSON.stringify({
|
||||
chainId: chainId,
|
||||
toAddress: address,
|
||||
turnstileToken,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -117,6 +131,8 @@ export function FaucetButton({
|
||||
faucetWalletBalanceQuery.data !== undefined &&
|
||||
faucetWalletBalanceQuery.data.value < toUnits("1", 17);
|
||||
|
||||
const form = useForm<z.infer<typeof claimFaucetSchema>>();
|
||||
|
||||
// loading state
|
||||
if (faucetWalletBalanceQuery.isPending || canClaimFaucetQuery.isPending) {
|
||||
return (
|
||||
@@ -161,28 +177,46 @@ export function FaucetButton({
|
||||
);
|
||||
}
|
||||
|
||||
const claimFunds = (values: z.infer<typeof claimFaucetSchema>) => {
|
||||
// Instead of having a dedicated endpoint (/api/verify-token),
|
||||
// we can just attach the token in the payload and send it to the claim-faucet endpoint, to avoid a round-trip request
|
||||
const claimPromise = claimMutation.mutateAsync(values.turnstileToken);
|
||||
toast.promise(claimPromise, {
|
||||
success: `${amount} ${chain.nativeCurrency.symbol} sent successfully`,
|
||||
error: `Failed to claim ${amount} ${chain.nativeCurrency.symbol}`,
|
||||
});
|
||||
};
|
||||
|
||||
// eligible to claim and faucet has balance
|
||||
return (
|
||||
<div className="flex w-full flex-col text-center">
|
||||
<Button
|
||||
variant="primary"
|
||||
className="w-full gap-2"
|
||||
onClick={() => {
|
||||
const claimPromise = claimMutation.mutateAsync();
|
||||
toast.promise(claimPromise, {
|
||||
success: `${amount} ${chain.nativeCurrency.symbol} sent successfully`,
|
||||
error: `Failed to claim ${amount} ${chain.nativeCurrency.symbol}`,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{claimMutation.isPending ? (
|
||||
<>
|
||||
Claiming <Spinner className="size-3" />
|
||||
</>
|
||||
) : (
|
||||
`Get ${amount} ${chain.nativeCurrency.symbol}`
|
||||
)}
|
||||
</Button>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(claimFunds)}>
|
||||
<Button variant="primary" className="w-full gap-2" type="submit">
|
||||
{claimMutation.isPending ? (
|
||||
<>
|
||||
Claiming <Spinner className="size-3" />
|
||||
</>
|
||||
) : (
|
||||
`Get ${amount} ${chain.nativeCurrency.symbol}`
|
||||
)}
|
||||
</Button>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="turnstileToken"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Turnstile
|
||||
siteKey={TURNSTILE_SITE_KEY}
|
||||
onSuccess={(token) => field.onChange(token)}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
{faucetWalletBalanceQuery.data && (
|
||||
<p className="mt-3 text-muted-foreground text-xs">
|
||||
|
||||
+2
-2
@@ -1,16 +1,16 @@
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { getFaucetClaimAmount } from "../../../../../../api/testnet-faucet/claim/claim-amount";
|
||||
import { ChainIcon } from "../../../../components/server/chain-icon";
|
||||
import { FaucetButton } from "../client/FaucetButton";
|
||||
import { GiftIcon } from "../icons/GiftIcon";
|
||||
import { SectionTitle } from "./SectionTitle";
|
||||
|
||||
const amountToGive = "0.01";
|
||||
|
||||
export async function FaucetSection(props: { chain: ChainMetadata }) {
|
||||
const { chain } = props;
|
||||
|
||||
// Check eligibilty.
|
||||
const sanitizedChainName = chain.name.replace("Mainnet", "").trim();
|
||||
const amountToGive = getFaucetClaimAmount(props.chain.chainId);
|
||||
|
||||
return (
|
||||
<section>
|
||||
|
||||
@@ -46,6 +46,10 @@ export async function generateMetadata({
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -122,6 +122,7 @@ export default async function Image({
|
||||
right: -2,
|
||||
height: 150,
|
||||
width: 150,
|
||||
objectFit: "contain",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
+10
-11
@@ -2,11 +2,11 @@
|
||||
|
||||
import { TransactionButton } from "components/buttons/TransactionButton";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
import { useAllChainsData } from "hooks/chains/allChains";
|
||||
import { toast } from "sonner";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { cancelAuction, cancelListing } from "thirdweb/extensions/marketplace";
|
||||
import { useSendAndConfirmTransaction } from "thirdweb/react";
|
||||
import { useAllChainsData } from "../../../hooks/chains/allChains";
|
||||
|
||||
interface CancelTabProps {
|
||||
id: string;
|
||||
@@ -25,14 +25,9 @@ export const CancelTab: React.FC<CancelTabProps> = ({
|
||||
const transaction = isAuction
|
||||
? cancelAuction({ contract, auctionId: BigInt(id) })
|
||||
: cancelListing({ contract, listingId: BigInt(id) });
|
||||
const { onSuccess, onError } = useTxNotifications(
|
||||
"Listing cancelled",
|
||||
"Error cancelling listing",
|
||||
);
|
||||
const cancelQuery = useSendAndConfirmTransaction();
|
||||
return (
|
||||
<div className="flex flex-col gap-3 pt-3">
|
||||
{/* maybe some text? */}
|
||||
<TransactionButton
|
||||
txChainID={contract.chain.id}
|
||||
transactionCount={1}
|
||||
@@ -43,7 +38,7 @@ export const CancelTab: React.FC<CancelTabProps> = ({
|
||||
action: "cancel-listing",
|
||||
label: "attempt",
|
||||
});
|
||||
cancelQuery.mutate(transaction, {
|
||||
const promise = cancelQuery.mutateAsync(transaction, {
|
||||
onSuccess: () => {
|
||||
trackEvent({
|
||||
category: "marketplace",
|
||||
@@ -51,7 +46,6 @@ export const CancelTab: React.FC<CancelTabProps> = ({
|
||||
label: "success",
|
||||
network,
|
||||
});
|
||||
onSuccess();
|
||||
},
|
||||
onError: (error) => {
|
||||
trackEvent({
|
||||
@@ -61,14 +55,19 @@ export const CancelTab: React.FC<CancelTabProps> = ({
|
||||
network,
|
||||
error,
|
||||
});
|
||||
onError(error);
|
||||
console.error(error);
|
||||
},
|
||||
});
|
||||
toast.promise(promise, {
|
||||
loading: `Cancelling ${isAuction ? "auction" : "listing"}`,
|
||||
success: "Item cancelled successfully",
|
||||
error: "Failed to cancel",
|
||||
});
|
||||
}}
|
||||
colorScheme="primary"
|
||||
alignSelf="flex-end"
|
||||
>
|
||||
Cancel Listing
|
||||
Cancel {isAuction ? "Auction" : "Listing"}
|
||||
</TransactionButton>
|
||||
</div>
|
||||
);
|
||||
+1
-1
@@ -8,7 +8,7 @@ import { useState } from "react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import { Button, Drawer } from "tw-components";
|
||||
import { CreateListingsForm } from "../listings/components/list-form";
|
||||
import { CreateListingsForm } from "./list-form";
|
||||
|
||||
const LIST_FORM_ID = "marketplace-list-form";
|
||||
|
||||
+1
-2
@@ -15,6 +15,7 @@ import {
|
||||
import { CurrencySelector } from "components/shared/CurrencySelector";
|
||||
import { SolidityInput } from "contract-ui/components/solidity-inputs";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useAllChainsData } from "hooks/chains/allChains";
|
||||
import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
import { isAlchemySupported } from "lib/wallet/nfts/alchemy";
|
||||
import { isMoralisSupported } from "lib/wallet/nfts/moralis";
|
||||
@@ -57,8 +58,6 @@ import {
|
||||
import { NFTMediaWithEmptyState } from "tw-components/nft-media";
|
||||
import { shortenIfAddress } from "utils/usedapp-external";
|
||||
|
||||
import { useAllChainsData } from "../../../../hooks/chains/allChains";
|
||||
|
||||
type ListForm =
|
||||
| (Omit<CreateListingParams, "quantity" | "currencyContractAddress"> & {
|
||||
currencyContractAddress: string;
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
import { WalletAddress } from "@/components/blocks/wallet-address";
|
||||
import { CopyAddressButton } from "@/components/ui/CopyAddressButton";
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { Sheet, SheetContent } from "@/components/ui/sheet";
|
||||
import { Flex, GridItem, SimpleGrid, usePrevious } from "@chakra-ui/react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type {
|
||||
DirectListing,
|
||||
EnglishAuction,
|
||||
} from "thirdweb/extensions/marketplace";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import { Badge, Card, CodeBlock, Heading, Text } from "tw-components";
|
||||
import { NFTMediaWithEmptyState } from "tw-components/nft-media";
|
||||
import { CancelTab } from "./cancel-tab";
|
||||
import { LISTING_STATUS } from "./types";
|
||||
|
||||
interface NFTDrawerProps {
|
||||
contract: ThirdwebContract;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
data: DirectListing | EnglishAuction | null;
|
||||
}
|
||||
|
||||
export const ListingDrawer: React.FC<NFTDrawerProps> = ({
|
||||
contract,
|
||||
isOpen,
|
||||
onClose,
|
||||
data,
|
||||
}) => {
|
||||
const address = useActiveAccount()?.address;
|
||||
const prevData = usePrevious(data);
|
||||
|
||||
const renderData = data || prevData;
|
||||
const isOwner =
|
||||
address?.toLowerCase() === renderData?.creatorAddress.toLowerCase();
|
||||
|
||||
const tokenId = renderData?.asset.id.toString() || "";
|
||||
|
||||
if (!renderData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet open={isOpen} onOpenChange={onClose}>
|
||||
<SheetContent className="flex w-full flex-col gap-6 py-6 md:min-w-[600px]">
|
||||
<div className="flex flex-row gap-6">
|
||||
<NFTMediaWithEmptyState
|
||||
metadata={renderData.asset.metadata}
|
||||
requireInteraction
|
||||
width="150px"
|
||||
height="150px"
|
||||
/>
|
||||
<Flex flexDir="column" gap={2} w="70%">
|
||||
<Heading size="title.lg">{renderData.asset.metadata.name}</Heading>
|
||||
<Text size="label.md" noOfLines={6}>
|
||||
{renderData.asset.metadata.name}
|
||||
</Text>
|
||||
</Flex>
|
||||
</div>
|
||||
|
||||
<Flex flexDir="column" gap={4}>
|
||||
<Card as={Flex} flexDir="column" gap={3}>
|
||||
<SimpleGrid rowGap={3} columns={12} placeItems="center left">
|
||||
<GridItem colSpan={3}>
|
||||
<Heading size="label.md">Asset contract address</Heading>
|
||||
</GridItem>
|
||||
<GridItem colSpan={9}>
|
||||
<CopyAddressButton
|
||||
address={renderData.assetContractAddress}
|
||||
copyIconPosition="right"
|
||||
/>
|
||||
</GridItem>
|
||||
<GridItem colSpan={3}>
|
||||
<Heading size="label.md">Token ID</Heading>
|
||||
</GridItem>
|
||||
<GridItem colSpan={9}>
|
||||
<CopyTextButton
|
||||
textToCopy={tokenId}
|
||||
textToShow={tokenId}
|
||||
copyIconPosition="right"
|
||||
tooltip="Token ID"
|
||||
/>
|
||||
</GridItem>
|
||||
<GridItem colSpan={3}>
|
||||
<Heading size="label.md">Seller</Heading>
|
||||
</GridItem>
|
||||
<GridItem colSpan={9}>
|
||||
<WalletAddress address={renderData.creatorAddress} />
|
||||
</GridItem>
|
||||
<GridItem colSpan={3}>
|
||||
<Heading size="label.md">Listing ID</Heading>
|
||||
</GridItem>
|
||||
<GridItem colSpan={9}>
|
||||
<CopyTextButton
|
||||
textToCopy={renderData.id.toString()}
|
||||
textToShow={renderData.id.toString()}
|
||||
copyIconPosition="right"
|
||||
tooltip="Listing ID"
|
||||
/>
|
||||
</GridItem>
|
||||
<GridItem colSpan={3}>
|
||||
<Heading size="label.md">Type</Heading>
|
||||
</GridItem>
|
||||
<GridItem colSpan={9}>
|
||||
<Text fontFamily="mono" size="body.md">
|
||||
{renderData.asset.type}
|
||||
</Text>
|
||||
</GridItem>
|
||||
<GridItem colSpan={3}>
|
||||
<Heading size="label.md">Status</Heading>
|
||||
</GridItem>
|
||||
<GridItem colSpan={9}>
|
||||
<Badge
|
||||
size="label.sm"
|
||||
variant="subtle"
|
||||
textTransform="capitalize"
|
||||
>
|
||||
{LISTING_STATUS[renderData.status]}
|
||||
</Badge>
|
||||
</GridItem>
|
||||
<GridItem colSpan={3}>
|
||||
<Heading size="label.md">Quantity</Heading>
|
||||
</GridItem>
|
||||
<GridItem colSpan={9}>
|
||||
<Text fontFamily="mono" size="body.md">
|
||||
{(renderData.quantity || 0n).toString()}{" "}
|
||||
{/* For listings that are completed, the `quantity` would be `0`
|
||||
So we show this text to make it clear */}
|
||||
{LISTING_STATUS[renderData.status] === "Completed"
|
||||
? "(Sold out)"
|
||||
: ""}
|
||||
</Text>
|
||||
</GridItem>
|
||||
|
||||
{renderData.type === "direct-listing" && (
|
||||
<>
|
||||
<GridItem colSpan={3}>
|
||||
<Heading size="label.md">Price</Heading>
|
||||
</GridItem>
|
||||
<GridItem colSpan={9}>
|
||||
<Text fontFamily="mono" size="body.md">
|
||||
{renderData.currencyValuePerToken.displayValue}{" "}
|
||||
{renderData.currencyValuePerToken.symbol}
|
||||
</Text>
|
||||
</GridItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/*
|
||||
Todo: Add a Buy button somewhere in this section once the Dashboard is fully migrated to v5 (?)
|
||||
Kien already shipped a prebuilt component for the Marketplace Buy Button in SDK v5
|
||||
*/}
|
||||
</SimpleGrid>
|
||||
</Card>
|
||||
{data?.asset.metadata.properties ? (
|
||||
<Card as={Flex} flexDir="column" gap={4}>
|
||||
<Heading size="label.md">Attributes</Heading>
|
||||
<CodeBlock
|
||||
code={
|
||||
JSON.stringify(data.asset.metadata.properties, null, 2) || ""
|
||||
}
|
||||
language="json"
|
||||
canCopy={false}
|
||||
wrap={false}
|
||||
overflow="auto"
|
||||
/>
|
||||
</Card>
|
||||
) : null}
|
||||
</Flex>
|
||||
{isOwner && (
|
||||
<CancelTab
|
||||
contract={contract}
|
||||
id={renderData.id.toString()}
|
||||
isAuction={renderData.type === "english-auction"}
|
||||
/>
|
||||
)}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
};
|
||||
+1
-4
@@ -17,7 +17,6 @@ import {
|
||||
} from "@chakra-ui/react";
|
||||
import type { UseQueryResult } from "@tanstack/react-query";
|
||||
import { MediaCell } from "components/contract-pages/table/table-columns/cells/media-cell";
|
||||
import { ListingDrawer } from "contract-ui/tabs/shared-components/listing-drawer";
|
||||
import {
|
||||
ChevronFirstIcon,
|
||||
ChevronLastIcon,
|
||||
@@ -40,6 +39,7 @@ import type {
|
||||
} from "thirdweb/extensions/marketplace";
|
||||
import { min } from "thirdweb/utils";
|
||||
import { Button, Text } from "tw-components";
|
||||
import { ListingDrawer } from "./listing-drawer";
|
||||
import { LISTING_STATUS } from "./types";
|
||||
|
||||
const tableColumns: Column<DirectListing | EnglishAuction>[] = [
|
||||
@@ -100,7 +100,6 @@ interface MarketplaceTableProps {
|
||||
start: number;
|
||||
}>
|
||||
>;
|
||||
type: "direct-listings" | "english-auctions";
|
||||
}
|
||||
|
||||
const DEFAULT_QUERY_STATE = { count: 50, start: 0 };
|
||||
@@ -112,7 +111,6 @@ export const MarketplaceTable: React.FC<MarketplaceTableProps> = ({
|
||||
totalCountQuery,
|
||||
queryParams,
|
||||
setQueryParams,
|
||||
type,
|
||||
}) => {
|
||||
const [listingsToShow, setListingsToShow_] = useState<"all" | "valid">("all");
|
||||
|
||||
@@ -215,7 +213,6 @@ export const MarketplaceTable: React.FC<MarketplaceTableProps> = ({
|
||||
data={tokenRow}
|
||||
isOpen={!!tokenRow}
|
||||
onClose={() => setTokenRow(null)}
|
||||
type={type}
|
||||
/>
|
||||
<Table {...getTableProps()}>
|
||||
<Thead>
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { Heading } from "tw-components";
|
||||
import { CreateListingButton } from "../shared-components/list-button";
|
||||
import { CreateListingButton } from "../components/list-button";
|
||||
import { DirectListingsTable } from "./components/table";
|
||||
|
||||
interface ContractDirectListingsPageProps {
|
||||
+1
-2
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { MarketplaceTable } from "contract-ui/tabs/shared-components/marketplace-table";
|
||||
import { useState } from "react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import {
|
||||
@@ -9,6 +8,7 @@ import {
|
||||
totalListings,
|
||||
} from "thirdweb/extensions/marketplace";
|
||||
import { useReadContract } from "thirdweb/react";
|
||||
import { MarketplaceTable } from "../../components/marketplace-table";
|
||||
|
||||
interface DirectListingsTableProps {
|
||||
contract: ThirdwebContract;
|
||||
@@ -40,7 +40,6 @@ export const DirectListingsTable: React.FC<DirectListingsTableProps> = ({
|
||||
totalCountQuery={totalCountQuery}
|
||||
queryParams={queryParams}
|
||||
setQueryParams={setQueryParams}
|
||||
type="direct-listings"
|
||||
/>
|
||||
);
|
||||
};
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { ContractDirectListingsPage } from "../../../../../../contract-ui/tabs/direct-listings/page";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { getContractPageParamsInfo } from "../../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../../_utils/getContractPageMetadata";
|
||||
import { ContractDirectListingsPage } from "./ContractDirectListingsPage";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { Heading } from "tw-components";
|
||||
import { CreateListingButton } from "../shared-components/list-button";
|
||||
import { CreateListingButton } from "../components/list-button";
|
||||
import { EnglishAuctionsTable } from "./components/table";
|
||||
|
||||
interface ContractEnglishAuctionsProps {
|
||||
+1
-2
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { MarketplaceTable } from "contract-ui/tabs/shared-components/marketplace-table";
|
||||
import { useState } from "react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import {
|
||||
@@ -9,6 +8,7 @@ import {
|
||||
totalAuctions,
|
||||
} from "thirdweb/extensions/marketplace";
|
||||
import { useReadContract } from "thirdweb/react";
|
||||
import { MarketplaceTable } from "../../components/marketplace-table";
|
||||
|
||||
interface EnglishAuctionsTableProps {
|
||||
contract: ThirdwebContract;
|
||||
@@ -40,7 +40,6 @@ export const EnglishAuctionsTable: React.FC<EnglishAuctionsTableProps> = ({
|
||||
totalCountQuery={totalCountQuery}
|
||||
queryParams={queryParams}
|
||||
setQueryParams={setQueryParams}
|
||||
type="english-auctions"
|
||||
/>
|
||||
);
|
||||
};
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { ContractEnglishAuctionsPage } from "../../../../../../contract-ui/tabs/english-auctions/page";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { getContractPageParamsInfo } from "../../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../../_utils/getContractPageMetadata";
|
||||
import { ContractEnglishAuctionsPage } from "./ContractEnglishAuctionsPage";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
+10
-13
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { AdminOnly } from "@3rdweb-sdk/react/components/roles/admin-only";
|
||||
import { useIsAdmin } from "@3rdweb-sdk/react/hooks/useContractRoles";
|
||||
@@ -13,7 +14,6 @@ import {
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuList,
|
||||
Spinner,
|
||||
} from "@chakra-ui/react";
|
||||
import { TransactionButton } from "components/buttons/TransactionButton";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
@@ -445,19 +445,16 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
|
||||
};
|
||||
}, [claimConditionsQuery.data, controlledFields, isMultiPhase]);
|
||||
|
||||
if (isFetchingData) {
|
||||
return (
|
||||
<div className="flex h-[400px] w-full items-center justify-center rounded-lg border border-border">
|
||||
<Spinner className="size-10" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* spinner */}
|
||||
{isFetchingData && (
|
||||
<Spinner
|
||||
color="primary"
|
||||
size="xs"
|
||||
position="absolute"
|
||||
top={2}
|
||||
right={4}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Flex onSubmit={handleFormSubmit} direction="column" as="form" gap={10}>
|
||||
<Flex direction="column" gap={6}>
|
||||
{/* Show the reason why the form is disabled */}
|
||||
@@ -553,7 +550,7 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
|
||||
|
||||
<Flex
|
||||
justifyContent="space-between"
|
||||
flexDir={{ base: "column", md: isColumn ? "column" : "row" }}
|
||||
flexDir={{ base: "column", md: "row" }}
|
||||
gap={2}
|
||||
>
|
||||
<div className="flex flex-row gap-2">
|
||||
|
||||
+1
-6
@@ -19,12 +19,7 @@ export const ClaimConditions: React.FC<ClaimConditionsProps> = ({
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-8">
|
||||
<Flex p={0} position="relative">
|
||||
<Flex
|
||||
pt={{ base: isColumn ? 0 : 6, md: 6 }}
|
||||
direction="column"
|
||||
gap={8}
|
||||
w="full"
|
||||
>
|
||||
<Flex direction="column" gap={6} w="full">
|
||||
{/* Info */}
|
||||
<section>
|
||||
<h2 className="mb-1 font-semibold text-xl tracking-tight">
|
||||
|
||||
+5
-5
@@ -3,11 +3,13 @@
|
||||
import { useApiKeys, useCreateApiKey } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { Flex, FormControl, Input, Link, Select } from "@chakra-ui/react";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useAllChainsData } from "hooks/chains/allChains";
|
||||
import { useClipboard } from "hooks/useClipboard";
|
||||
import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import type { StoredChain } from "stores/chainStores";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import {
|
||||
@@ -19,8 +21,6 @@ import {
|
||||
Heading,
|
||||
Text,
|
||||
} from "tw-components";
|
||||
import { useAllChainsData } from "../../../../hooks/chains/allChains";
|
||||
import type { StoredChain } from "../../../../stores/chainStores";
|
||||
|
||||
interface EmbedSetupProps {
|
||||
contract: ThirdwebContract;
|
||||
@@ -171,7 +171,7 @@ function minimizeChain(
|
||||
return {
|
||||
name: chain.name,
|
||||
chain: chain.chain,
|
||||
rpc: [firstRpc],
|
||||
rpc: [firstRpc || ""],
|
||||
nativeCurrency: chain.nativeCurrency,
|
||||
shortName: chain.shortName,
|
||||
chainId: chain.chainId,
|
||||
@@ -509,7 +509,7 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
|
||||
)}
|
||||
{colorOptions.map((color) => (
|
||||
<option key={color} value={color}>
|
||||
{color[0].toUpperCase() + color.substring(1)}
|
||||
{color[0]?.toUpperCase() + color.substring(1)}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
@@ -524,7 +524,7 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
|
||||
<Select {...register("secondaryColor")}>
|
||||
{colorOptions.map((color) => (
|
||||
<option key={color} value={color}>
|
||||
{color[0].toUpperCase() + color.substring(1)}
|
||||
{color[0]?.toUpperCase() + color.substring(1)}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { EmbedSetup } from "../../../../../../contract-ui/tabs/embed/components/embed-setup";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { EmbedSetup } from "./embed-setup";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
|
||||
+1
-1
@@ -24,6 +24,7 @@ import {
|
||||
Tooltip,
|
||||
} from "@chakra-ui/react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { useChainSlug } from "hooks/chains/chainSlug";
|
||||
import { useClipboard } from "hooks/useClipboard";
|
||||
import { ChevronDownIcon, CircleHelpIcon, CopyIcon } from "lucide-react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
@@ -39,7 +40,6 @@ import {
|
||||
Heading,
|
||||
Text,
|
||||
} from "tw-components";
|
||||
import { useChainSlug } from "../../../../hooks/chains/chainSlug";
|
||||
|
||||
interface EventsFeedProps {
|
||||
contract: ThirdwebContract;
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { EventsFeed } from "../../../../../../contract-ui/tabs/events/components/events-feed";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { EventsFeed } from "./events-feed";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
|
||||
+1
-1
@@ -2,8 +2,8 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { CircleAlertIcon } from "lucide-react";
|
||||
import { notFound } from "next/navigation";
|
||||
import { resolveContractAbi } from "thirdweb/contract";
|
||||
import { ContractExplorerPage } from "../../../../../../contract-ui/tabs/explorer/page";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { ContractExplorerPage } from "./ContractExplorerPage";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
|
||||
@@ -2,12 +2,14 @@ import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
|
||||
import { SidebarLayout } from "@/components/blocks/SidebarLayout";
|
||||
import { ContractMetadata } from "components/custom-contract/contract-header/contract-metadata";
|
||||
import { DeprecatedAlert } from "components/shared/DeprecatedAlert";
|
||||
import { PrimaryDashboardButton } from "contract-ui/components/primary-dashboard-button";
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getContractMetadata } from "thirdweb/extensions/common";
|
||||
import { isAddress, isContractDeployed } from "thirdweb/utils";
|
||||
import { resolveFunctionSelectors } from "../../../../../lib/selectors";
|
||||
import { shortenIfAddress } from "../../../../../utils/usedapp-external";
|
||||
import { ConfigureCustomChain } from "./ConfigureCustomChain";
|
||||
import { PrimaryDashboardButton } from "./_components/primary-dashboard-button";
|
||||
import { supportedERCs } from "./_utils/detectedFeatures/supportedERCs";
|
||||
import { getContractPageParamsInfo } from "./_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "./_utils/getContractPageMetadata";
|
||||
@@ -20,6 +22,10 @@ export default async function Layout(props: {
|
||||
};
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
if (!isAddress(props.params.contractAddress)) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const info = await getContractPageParamsInfo(props.params);
|
||||
|
||||
if (!info) {
|
||||
@@ -27,6 +33,18 @@ export default async function Layout(props: {
|
||||
}
|
||||
|
||||
const { contract, chainMetadata } = info;
|
||||
|
||||
if (chainMetadata.status === "deprecated") {
|
||||
notFound();
|
||||
}
|
||||
|
||||
// check if the contract exists
|
||||
const isValidContract = await isContractDeployed(contract).catch(() => false);
|
||||
if (!isValidContract) {
|
||||
// TODO - replace 404 with a better page to upsale deploy or other thirdweb products
|
||||
notFound();
|
||||
}
|
||||
|
||||
const contractPageMetadata = await getContractPageMetadata(contract);
|
||||
const sidebarLinks = getContractPageSidebarLinks({
|
||||
chainSlug: chainMetadata.slug,
|
||||
|
||||
+1
-1
@@ -1,8 +1,8 @@
|
||||
import { FormControl } from "@chakra-ui/react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { SolidityInput } from "contract-ui/components/solidity-inputs";
|
||||
import invariant from "tiny-invariant";
|
||||
import { FormErrorMessage, FormLabel } from "tw-components";
|
||||
import { SolidityInput } from "../../../components/solidity-inputs";
|
||||
import type { InstallModuleForm } from "./ModuleForm";
|
||||
import { getModuleInstalledParams } from "./getModuleInstalledParams";
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { ContractEditModulesPage } from "../../../../../../contract-ui/tabs/manage/page";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { ContractEditModulesPage } from "./ContractEditModulesPage";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
|
||||
+103
-88
@@ -1,17 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import { Flex, useDisclosure } from "@chakra-ui/react";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from "@/components/ui/sheet";
|
||||
import { Flex } from "@chakra-ui/react";
|
||||
import { TransactionButton } from "components/buttons/TransactionButton";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
import { UploadIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { multicall } from "thirdweb/extensions/common";
|
||||
import { balanceOf, encodeSafeTransferFrom } from "thirdweb/extensions/erc1155";
|
||||
import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react";
|
||||
import { Button, Text } from "tw-components";
|
||||
import { type AirdropAddressInput, AirdropUpload } from "./airdrop-upload";
|
||||
import {
|
||||
type AirdropAddressInput,
|
||||
AirdropUpload,
|
||||
} from "../../../tokens/components/airdrop-upload";
|
||||
|
||||
interface AirdropTabProps {
|
||||
contract: ThirdwebContract;
|
||||
@@ -22,8 +33,6 @@ interface AirdropTabProps {
|
||||
* This component must only take in ERC1155 contracts
|
||||
*/
|
||||
const AirdropTab: React.FC<AirdropTabProps> = ({ contract, tokenId }) => {
|
||||
const account = useActiveAccount();
|
||||
|
||||
const address = useActiveAccount()?.address;
|
||||
const { handleSubmit, setValue, watch, reset, formState } = useForm<{
|
||||
addresses: AirdropAddressInput[];
|
||||
@@ -31,100 +40,106 @@ const AirdropTab: React.FC<AirdropTabProps> = ({ contract, tokenId }) => {
|
||||
defaultValues: { addresses: [] },
|
||||
});
|
||||
const trackEvent = useTrack();
|
||||
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
const { mutate, isPending } = useSendAndConfirmTransaction();
|
||||
|
||||
const { onSuccess, onError } = useTxNotifications(
|
||||
"Airdrop successful",
|
||||
"Error transferring",
|
||||
contract,
|
||||
);
|
||||
|
||||
const sendAndConfirmTx = useSendAndConfirmTransaction();
|
||||
const addresses = watch("addresses");
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
<form
|
||||
onSubmit={handleSubmit(async (_data) => {
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action: "airdrop",
|
||||
label: "attempt",
|
||||
contract_address: contract.address,
|
||||
token_id: tokenId,
|
||||
});
|
||||
const totalOwned = await balanceOf({
|
||||
contract,
|
||||
tokenId: BigInt(tokenId),
|
||||
owner: account?.address ?? "",
|
||||
});
|
||||
// todo: make a batch-transfer extension for erc1155?
|
||||
const totalToAirdrop = _data.addresses.reduce((prev, curr) => {
|
||||
return BigInt(prev) + BigInt(curr?.quantity || 1);
|
||||
}, 0n);
|
||||
if (totalOwned < totalToAirdrop) {
|
||||
return onError(
|
||||
new Error(
|
||||
`The caller owns ${totalOwned.toString()} NFTs, but wants to airdrop ${totalToAirdrop.toString()} NFTs.`,
|
||||
),
|
||||
);
|
||||
}
|
||||
const data = _data.addresses.map(({ address: to, quantity }) =>
|
||||
encodeSafeTransferFrom({
|
||||
from: account?.address ?? "",
|
||||
to,
|
||||
value: BigInt(quantity),
|
||||
data: "0x",
|
||||
try {
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action: "airdrop",
|
||||
label: "attempt",
|
||||
contract_address: contract.address,
|
||||
token_id: tokenId,
|
||||
});
|
||||
const totalOwned = await balanceOf({
|
||||
contract,
|
||||
tokenId: BigInt(tokenId),
|
||||
}),
|
||||
);
|
||||
const transaction = multicall({ contract, data });
|
||||
mutate(transaction, {
|
||||
onSuccess: () => {
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action: "airdrop",
|
||||
label: "success",
|
||||
contract_address: contract.address,
|
||||
token_id: tokenId,
|
||||
});
|
||||
onSuccess();
|
||||
reset();
|
||||
},
|
||||
onError: (error) => {
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action: "airdrop",
|
||||
label: "success",
|
||||
contract_address: contract.address,
|
||||
token_id: tokenId,
|
||||
error,
|
||||
});
|
||||
onError(error);
|
||||
},
|
||||
});
|
||||
owner: address ?? "",
|
||||
});
|
||||
// todo: make a batch-transfer extension for erc1155?
|
||||
const totalToAirdrop = _data.addresses.reduce((prev, curr) => {
|
||||
return BigInt(prev) + BigInt(curr?.quantity || 1);
|
||||
}, 0n);
|
||||
if (totalOwned < totalToAirdrop) {
|
||||
return toast.error(
|
||||
`The caller owns ${totalOwned.toString()} NFTs, but wants to airdrop ${totalToAirdrop.toString()} NFTs.`,
|
||||
);
|
||||
}
|
||||
const data = _data.addresses.map(({ address: to, quantity }) =>
|
||||
encodeSafeTransferFrom({
|
||||
from: address ?? "",
|
||||
to,
|
||||
value: BigInt(quantity),
|
||||
data: "0x",
|
||||
tokenId: BigInt(tokenId),
|
||||
}),
|
||||
);
|
||||
const transaction = multicall({ contract, data });
|
||||
const promise = sendAndConfirmTx.mutateAsync(transaction, {
|
||||
onSuccess: () => {
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action: "airdrop",
|
||||
label: "success",
|
||||
contract_address: contract.address,
|
||||
token_id: tokenId,
|
||||
});
|
||||
reset();
|
||||
},
|
||||
onError: (error) => {
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action: "airdrop",
|
||||
label: "success",
|
||||
contract_address: contract.address,
|
||||
token_id: tokenId,
|
||||
error,
|
||||
});
|
||||
},
|
||||
});
|
||||
toast.promise(promise, {
|
||||
loading: "Airdropping NFTs",
|
||||
success: "Airdropped successfully",
|
||||
error: "Failed to airdrop",
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast.error("Failed to airdrop NFTs");
|
||||
}
|
||||
})}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="mb-3 flex w-full flex-col gap-6 md:flex-row">
|
||||
<AirdropUpload
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
setAirdrop={(value) =>
|
||||
setValue("addresses", value, { shouldDirty: true })
|
||||
}
|
||||
/>
|
||||
<Flex direction={{ base: "column", md: "row" }} gap={4}>
|
||||
<Button
|
||||
colorScheme="primary"
|
||||
borderRadius="md"
|
||||
onClick={onOpen}
|
||||
rightIcon={<UploadIcon className="size-5" />}
|
||||
>
|
||||
Upload addresses
|
||||
</Button>
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
colorScheme="primary"
|
||||
borderRadius="md"
|
||||
rightIcon={<UploadIcon className="size-5" />}
|
||||
>
|
||||
Upload addresses
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className="w-full overflow-y-auto sm:min-w-[540px] lg:min-w-[700px]">
|
||||
<SheetHeader>
|
||||
<SheetTitle className="mb-5 text-left">
|
||||
Aidrop NFTs
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
<AirdropUpload
|
||||
onClose={() => setOpen(false)}
|
||||
setAirdrop={(value) =>
|
||||
setValue("addresses", value, { shouldDirty: true })
|
||||
}
|
||||
/>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
<Flex
|
||||
gap={2}
|
||||
@@ -149,7 +164,7 @@ const AirdropTab: React.FC<AirdropTabProps> = ({ contract, tokenId }) => {
|
||||
<TransactionButton
|
||||
txChainID={contract.chain.id}
|
||||
transactionCount={1}
|
||||
isLoading={isPending}
|
||||
isLoading={sendAndConfirmTx.isPending}
|
||||
type="submit"
|
||||
colorScheme="primary"
|
||||
disabled={!!address && addresses.length === 0}
|
||||
|
||||
-494
@@ -1,494 +0,0 @@
|
||||
import { UnorderedList } from "@/components/ui/List/List";
|
||||
import { useThirdwebClient } from "@/constants/thirdweb.client";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
Box,
|
||||
Container,
|
||||
Flex,
|
||||
IconButton,
|
||||
Link,
|
||||
Portal,
|
||||
Select,
|
||||
Table,
|
||||
TableContainer,
|
||||
Tbody,
|
||||
Td,
|
||||
Th,
|
||||
Thead,
|
||||
Tooltip,
|
||||
Tr,
|
||||
} from "@chakra-ui/react";
|
||||
import { Logo } from "components/logo";
|
||||
import {
|
||||
ChevronFirstIcon,
|
||||
ChevronLastIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
CircleAlertIcon,
|
||||
UploadIcon,
|
||||
} from "lucide-react";
|
||||
import Papa from "papaparse";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { type DropzoneOptions, useDropzone } from "react-dropzone";
|
||||
import { type Column, usePagination, useTable } from "react-table";
|
||||
import { resolveAddress } from "thirdweb/extensions/ens";
|
||||
import { isAddress } from "thirdweb/utils";
|
||||
import { Button, Drawer, Heading, Text } from "tw-components";
|
||||
import { csvMimeTypes } from "utils/batch";
|
||||
|
||||
export interface AirdropAddressInput {
|
||||
address: string;
|
||||
quantity: string;
|
||||
isValid?: boolean;
|
||||
}
|
||||
interface AirdropUploadProps {
|
||||
setAirdrop: (airdrop: AirdropAddressInput[]) => void;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const AirdropUpload: React.FC<AirdropUploadProps> = ({
|
||||
setAirdrop,
|
||||
isOpen,
|
||||
onClose,
|
||||
}) => {
|
||||
const client = useThirdwebClient();
|
||||
const [validAirdrop, setValidAirdrop] = useState<AirdropAddressInput[]>([]);
|
||||
const [airdropData, setAirdropData] = useState<AirdropAddressInput[]>([]);
|
||||
const [noCsv, setNoCsv] = useState(false);
|
||||
const [invalidFound, setInvalidFound] = useState(false);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setValidAirdrop([]);
|
||||
setNoCsv(false);
|
||||
}, []);
|
||||
|
||||
const _onClose = useCallback(() => {
|
||||
onClose();
|
||||
}, [onClose]);
|
||||
|
||||
const onDrop = useCallback<Required<DropzoneOptions>["onDrop"]>(
|
||||
(acceptedFiles) => {
|
||||
setNoCsv(false);
|
||||
|
||||
const csv = acceptedFiles.find(
|
||||
(f) => csvMimeTypes.includes(f.type) || f.name?.endsWith(".csv"),
|
||||
);
|
||||
if (!csv) {
|
||||
console.error(
|
||||
"No valid CSV file found, make sure you have an address column.",
|
||||
);
|
||||
setNoCsv(true);
|
||||
return;
|
||||
}
|
||||
|
||||
Papa.parse(csv, {
|
||||
header: true,
|
||||
complete: (results) => {
|
||||
const data: AirdropAddressInput[] = (
|
||||
results.data as AirdropAddressInput[]
|
||||
)
|
||||
.map(({ address, quantity }) => ({
|
||||
address: (address || "").trim(),
|
||||
quantity: (quantity || "1").trim(),
|
||||
}))
|
||||
.filter(({ address }) => address !== "");
|
||||
|
||||
if (!data[0]?.address) {
|
||||
setNoCsv(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setValidAirdrop(data);
|
||||
},
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// FIXME: this can be a mutation or query instead!
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
if (validAirdrop.length === 0) {
|
||||
return setAirdropData([]);
|
||||
}
|
||||
|
||||
const normalizeAddresses = async (snapshot: AirdropAddressInput[]) => {
|
||||
const normalized = await Promise.all(
|
||||
snapshot.map(async ({ address, ...rest }) => {
|
||||
let isValid = true;
|
||||
let resolvedAddress = address;
|
||||
|
||||
try {
|
||||
resolvedAddress = isAddress(address)
|
||||
? address
|
||||
: await resolveAddress({ name: address, client });
|
||||
isValid = !!resolvedAddress;
|
||||
} catch {
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
return {
|
||||
address,
|
||||
resolvedAddress,
|
||||
isValid,
|
||||
...rest,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const seen = new Set();
|
||||
const filteredData = normalized.filter((el) => {
|
||||
const duplicate = seen.has(el.resolvedAddress);
|
||||
seen.add(el.resolvedAddress);
|
||||
return !duplicate;
|
||||
});
|
||||
|
||||
const valid = filteredData.filter(({ isValid }) => isValid);
|
||||
const invalid = filteredData.filter(({ isValid }) => !isValid);
|
||||
|
||||
if (invalid?.length > 0) {
|
||||
setInvalidFound(true);
|
||||
}
|
||||
const ordered = [...invalid, ...valid];
|
||||
setAirdropData(ordered);
|
||||
};
|
||||
normalizeAddresses(validAirdrop);
|
||||
}, [validAirdrop, client]);
|
||||
|
||||
const removeInvalid = useCallback(() => {
|
||||
const filteredData = airdropData.filter(({ isValid }) => isValid);
|
||||
setValidAirdrop(filteredData);
|
||||
setInvalidFound(false);
|
||||
}, [airdropData]);
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
onDrop,
|
||||
});
|
||||
|
||||
const paginationPortalRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const onSave = () => {
|
||||
setAirdrop(airdropData);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
allowPinchZoom
|
||||
preserveScrollBarGap
|
||||
size="xl"
|
||||
onClose={_onClose}
|
||||
isOpen={isOpen}
|
||||
>
|
||||
<Flex direction="column" gap={6} h="100%">
|
||||
<Flex shadow="sm">
|
||||
<Container maxW="container.page">
|
||||
<Flex align="center" justify="space-between" p={4}>
|
||||
<div className="flex flex-row gap-2">
|
||||
<Logo hideWordmark />
|
||||
<Heading size="title.md">
|
||||
{validAirdrop.length ? "Edit" : "Upload"} Airdrop
|
||||
</Heading>
|
||||
</div>
|
||||
</Flex>
|
||||
</Container>
|
||||
</Flex>
|
||||
|
||||
{validAirdrop.length > 0 ? (
|
||||
<AirdropTable portalRef={paginationPortalRef} data={airdropData} />
|
||||
) : (
|
||||
<Flex flexGrow={1} align="center" overflow="auto">
|
||||
<Container maxW="container.page">
|
||||
<Flex gap={8} flexDir="column">
|
||||
<div className="relative aspect-[21/9] w-full">
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full cursor-pointer rounded-md border border-border hover:border-primary",
|
||||
noCsv ? "bg-red-200" : "bg-card",
|
||||
)}
|
||||
{...getRootProps()}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
<div className="m-auto flex flex-col p-6">
|
||||
<UploadIcon
|
||||
size={16}
|
||||
className={cn("mx-auto mb-2 text-gray-500", {
|
||||
"text-red-500": noCsv,
|
||||
})}
|
||||
/>
|
||||
{isDragActive ? (
|
||||
<Heading
|
||||
as={Text}
|
||||
size="label.md"
|
||||
className="text-center"
|
||||
>
|
||||
Drop the files here
|
||||
</Heading>
|
||||
) : (
|
||||
<Heading
|
||||
as={Text}
|
||||
size="label.md"
|
||||
color={noCsv ? "red.500" : "gray.600"}
|
||||
className="text-center"
|
||||
>
|
||||
{noCsv
|
||||
? `No valid CSV file found, make sure your CSV includes the "address" column.`
|
||||
: "Drag & Drop a CSV file here"}
|
||||
</Heading>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row gap-2">
|
||||
<Heading size="subtitle.sm">Requirements</Heading>
|
||||
<UnorderedList>
|
||||
<li>
|
||||
Files <em>must</em> contain one .csv file with an address
|
||||
and quantity column, if the quantity column is not
|
||||
provided, it will default to 1 NFT per wallet. -{" "}
|
||||
<Link download color="primary.500" href="/airdrop.csv">
|
||||
Download an example CSV
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
Repeated addresses will be removed and only the first
|
||||
found will be kept.
|
||||
</li>
|
||||
</UnorderedList>
|
||||
</div>
|
||||
</Flex>
|
||||
</Container>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
<Flex borderTop="1px solid" borderTopColor="borderColor">
|
||||
<Container maxW="container.page">
|
||||
<Flex
|
||||
align="center"
|
||||
justify="space-between"
|
||||
p={{ base: 0, md: 4 }}
|
||||
flexDir={{ base: "column", md: "row" }}
|
||||
mt={{ base: 4, md: 0 }}
|
||||
>
|
||||
<Box ref={paginationPortalRef} />
|
||||
<Flex
|
||||
gap={2}
|
||||
align="center"
|
||||
mt={{ base: 4, md: 0 }}
|
||||
w={{ base: "100%", md: "auto" }}
|
||||
>
|
||||
<Button
|
||||
borderRadius="md"
|
||||
disabled={validAirdrop.length === 0}
|
||||
onClick={() => {
|
||||
reset();
|
||||
}}
|
||||
w={{ base: "100%", md: "auto" }}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
{invalidFound ? (
|
||||
<Button
|
||||
borderRadius="md"
|
||||
colorScheme="primary"
|
||||
disabled={validAirdrop.length === 0}
|
||||
onClick={() => {
|
||||
removeInvalid();
|
||||
}}
|
||||
w={{ base: "100%", md: "auto" }}
|
||||
>
|
||||
Remove invalid
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
borderRadius="md"
|
||||
colorScheme="primary"
|
||||
onClick={onSave}
|
||||
w={{ base: "100%", md: "auto" }}
|
||||
isDisabled={validAirdrop.length === 0}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Container>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
interface AirdropTableProps {
|
||||
data: AirdropAddressInput[];
|
||||
portalRef: React.RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
const AirdropTable: React.FC<AirdropTableProps> = ({ data, portalRef }) => {
|
||||
const columns = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
Header: "Address",
|
||||
accessor: ({ address, isValid }) => {
|
||||
if (isValid) {
|
||||
return address;
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Tooltip
|
||||
label={
|
||||
address.startsWith("0x")
|
||||
? "Address is not valid"
|
||||
: "Address couldn't be resolved"
|
||||
}
|
||||
>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<CircleAlertIcon className="size-5 text-red-500" />
|
||||
<Text fontWeight="bold" color="red.500" cursor="default">
|
||||
{address}
|
||||
</Text>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: "Quantity",
|
||||
accessor: ({ quantity }: { quantity: string }) => {
|
||||
return quantity || "1";
|
||||
},
|
||||
},
|
||||
] as Column<AirdropAddressInput>[];
|
||||
}, []);
|
||||
|
||||
const {
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
prepareRow,
|
||||
// Instead of using 'rows', we'll use page,
|
||||
page,
|
||||
// which has only the rows for the active page
|
||||
|
||||
// The rest of these things are super handy, too ;)
|
||||
canPreviousPage,
|
||||
canNextPage,
|
||||
pageOptions,
|
||||
pageCount,
|
||||
gotoPage,
|
||||
nextPage,
|
||||
previousPage,
|
||||
setPageSize,
|
||||
state: { pageIndex, pageSize },
|
||||
} = useTable(
|
||||
{
|
||||
columns,
|
||||
data,
|
||||
initialState: {
|
||||
pageSize: 50,
|
||||
pageIndex: 0,
|
||||
},
|
||||
},
|
||||
// old package: this will be removed
|
||||
// eslint-disable-next-line react-compiler/react-compiler
|
||||
usePagination,
|
||||
);
|
||||
|
||||
// Render the UI for your table
|
||||
return (
|
||||
<Flex flexGrow={1}>
|
||||
<TableContainer>
|
||||
<Table {...getTableProps()}>
|
||||
<Thead>
|
||||
{headerGroups.map((headerGroup, headerGroupIndex) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
|
||||
<Tr {...headerGroup.getHeaderGroupProps()} key={headerGroupIndex}>
|
||||
{headerGroup.headers.map((column, columnIndex) => (
|
||||
<Th
|
||||
{...column.getHeaderProps()}
|
||||
border="none"
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
|
||||
key={columnIndex}
|
||||
>
|
||||
<Text as="label" size="label.sm" color="faded">
|
||||
{column.render("Header")}
|
||||
</Text>
|
||||
</Th>
|
||||
))}
|
||||
</Tr>
|
||||
))}
|
||||
</Thead>
|
||||
<Tbody {...getTableBodyProps()}>
|
||||
{page.map((row, rowIndex) => {
|
||||
prepareRow(row);
|
||||
return (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
|
||||
<Tr {...row.getRowProps()} key={rowIndex}>
|
||||
{row.cells.map((cell, cellIndex) => (
|
||||
<Td
|
||||
{...cell.getCellProps()}
|
||||
borderColor="borderColor"
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
|
||||
key={cellIndex}
|
||||
>
|
||||
{cell.render("Cell")}
|
||||
</Td>
|
||||
))}
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Portal containerRef={portalRef}>
|
||||
<div className="flex w-full items-center justify-center">
|
||||
<div className="flex flex-row gap-1">
|
||||
<IconButton
|
||||
isDisabled={!canPreviousPage}
|
||||
aria-label="first page"
|
||||
icon={<ChevronFirstIcon className="size-4" />}
|
||||
onClick={() => gotoPage(0)}
|
||||
/>
|
||||
<IconButton
|
||||
isDisabled={!canPreviousPage}
|
||||
aria-label="previous page"
|
||||
icon={<ChevronLeftIcon className="size-4" />}
|
||||
onClick={() => previousPage()}
|
||||
/>
|
||||
<p className="my-auto whitespace-nowrap">
|
||||
Page <strong>{pageIndex + 1}</strong> of{" "}
|
||||
<strong>{pageOptions.length}</strong>
|
||||
</p>
|
||||
<IconButton
|
||||
isDisabled={!canNextPage}
|
||||
aria-label="next page"
|
||||
icon={<ChevronRightIcon className="size-4" />}
|
||||
onClick={() => nextPage()}
|
||||
/>
|
||||
<IconButton
|
||||
isDisabled={!canNextPage}
|
||||
aria-label="last page"
|
||||
icon={<ChevronLastIcon className="size-4" />}
|
||||
onClick={() => gotoPage(pageCount - 1)}
|
||||
/>
|
||||
|
||||
<Select
|
||||
onChange={(e) => {
|
||||
setPageSize(Number.parseInt(e.target.value as string, 10));
|
||||
}}
|
||||
value={pageSize}
|
||||
>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
<option value="250">250</option>
|
||||
<option value="500">500</option>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</Portal>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
+110
-74
@@ -1,101 +1,137 @@
|
||||
"use client";
|
||||
|
||||
import { FormControl, Input } from "@chakra-ui/react";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { TransactionButton } from "components/buttons/TransactionButton";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
import { useForm } from "react-hook-form";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { toast } from "sonner";
|
||||
import { type ThirdwebContract, isAddress } from "thirdweb";
|
||||
import { mintAdditionalSupplyTo } from "thirdweb/extensions/erc1155";
|
||||
import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react";
|
||||
import { FormErrorMessage, FormHelperText, FormLabel } from "tw-components";
|
||||
import { z } from "zod";
|
||||
|
||||
interface MintSupplyTabProps {
|
||||
contract: ThirdwebContract;
|
||||
tokenId: string;
|
||||
}
|
||||
|
||||
const mintAdditionalSupplyFormSchema = z.object({
|
||||
to: z.string().refine((value) => isAddress(value), {
|
||||
message: "Invalid Ethereum address",
|
||||
}),
|
||||
amount: z.number().int().min(1, "Amount must be at least 1"),
|
||||
});
|
||||
|
||||
const MintSupplyTab: React.FC<MintSupplyTabProps> = ({ contract, tokenId }) => {
|
||||
const trackEvent = useTrack();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
reset,
|
||||
} = useForm<{ to: string; amount: string }>({
|
||||
defaultValues: { amount: "1" },
|
||||
const address = useActiveAccount()?.address;
|
||||
|
||||
const form = useForm<z.input<typeof mintAdditionalSupplyFormSchema>>({
|
||||
resolver: zodResolver(mintAdditionalSupplyFormSchema),
|
||||
defaultValues: {
|
||||
amount: 1,
|
||||
to: address || "",
|
||||
},
|
||||
});
|
||||
|
||||
const address = useActiveAccount()?.address;
|
||||
const { mutate, isPending } = useSendAndConfirmTransaction();
|
||||
const { onSuccess, onError } = useTxNotifications(
|
||||
"Mint successful",
|
||||
"Error minting additional supply",
|
||||
contract,
|
||||
);
|
||||
const sendAndConfirmTx = useSendAndConfirmTransaction();
|
||||
|
||||
function onSubmit(values: z.input<typeof mintAdditionalSupplyFormSchema>) {
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action: "mint-supply",
|
||||
label: "attempt",
|
||||
});
|
||||
const transaction = mintAdditionalSupplyTo({
|
||||
contract,
|
||||
to: values.to,
|
||||
tokenId: BigInt(tokenId),
|
||||
supply: BigInt(values.amount),
|
||||
});
|
||||
const promise = sendAndConfirmTx.mutateAsync(transaction, {
|
||||
onSuccess: () => {
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action: "mint-supply",
|
||||
label: "success",
|
||||
});
|
||||
form.reset();
|
||||
},
|
||||
onError: (error) => {
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action: "mint-supply",
|
||||
label: "error",
|
||||
error,
|
||||
});
|
||||
},
|
||||
});
|
||||
toast.promise(promise, {
|
||||
loading: "Minting NFT",
|
||||
success: "Minted successfully",
|
||||
error: "Failed to mint",
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={handleSubmit((data) => {
|
||||
if (address) {
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action: "mint-supply",
|
||||
label: "attempt",
|
||||
});
|
||||
const transaction = mintAdditionalSupplyTo({
|
||||
contract,
|
||||
to: address,
|
||||
tokenId: BigInt(tokenId),
|
||||
supply: BigInt(data.amount),
|
||||
});
|
||||
mutate(transaction, {
|
||||
onSuccess: () => {
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action: "mint-supply",
|
||||
label: "success",
|
||||
});
|
||||
onSuccess();
|
||||
reset();
|
||||
},
|
||||
onError: (error) => {
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action: "mint-supply",
|
||||
label: "error",
|
||||
error,
|
||||
});
|
||||
onError(error);
|
||||
},
|
||||
});
|
||||
}
|
||||
})}
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="flex w-full flex-col gap-2"
|
||||
>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex w-full flex-col gap-6 md:flex-row">
|
||||
<FormControl isRequired isInvalid={!!errors.to}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="amount"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Amount</FormLabel>
|
||||
<Input placeholder="1" {...register("amount")} />
|
||||
<FormHelperText>How many would you like to mint?</FormHelperText>
|
||||
<FormErrorMessage>{errors.to?.message}</FormErrorMessage>
|
||||
</FormControl>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
How many would you like to mint?
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<TransactionButton
|
||||
txChainID={contract.chain.id}
|
||||
transactionCount={1}
|
||||
isLoading={isPending}
|
||||
type="submit"
|
||||
colorScheme="primary"
|
||||
alignSelf="flex-end"
|
||||
>
|
||||
Mint
|
||||
</TransactionButton>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="to"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Recipient</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<TransactionButton
|
||||
txChainID={contract.chain.id}
|
||||
transactionCount={1}
|
||||
isLoading={sendAndConfirmTx.isPending}
|
||||
type="submit"
|
||||
colorScheme="primary"
|
||||
alignSelf="flex-end"
|
||||
>
|
||||
Mint
|
||||
</TransactionButton>
|
||||
</form>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+112
-11
@@ -1,8 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { UnexpectedValueErrorMessage } from "@/components/blocks/error-fallbacks/unexpect-value-error-message";
|
||||
import { WalletAddress } from "@/components/blocks/wallet-address";
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { useThirdwebClient } from "@/constants/thirdweb.client";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler";
|
||||
import {
|
||||
Box,
|
||||
ButtonGroup,
|
||||
@@ -15,15 +19,16 @@ import {
|
||||
useBreakpointValue,
|
||||
} from "@chakra-ui/react";
|
||||
import { useChainSlug } from "hooks/chains/chainSlug";
|
||||
import { ChevronLeftIcon } from "lucide-react";
|
||||
import { ChevronLeftIcon, ExternalLinkIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { getNFT as getErc721NFT } from "thirdweb/extensions/erc721";
|
||||
import { getNFT as getErc1155NFT } from "thirdweb/extensions/erc1155";
|
||||
import { useReadContract } from "thirdweb/react";
|
||||
import { Badge, Button, Card, CodeBlock, Heading, Text } from "tw-components";
|
||||
import { AddressCopyButton } from "tw-components/AddressCopyButton";
|
||||
import { NFTMediaWithEmptyState } from "tw-components/nft-media";
|
||||
import { shortenString } from "utils/usedapp-external";
|
||||
import { NftProperty } from "../components/nft-property";
|
||||
import { useNFTDrawerTabs } from "./useNftDrawerTabs";
|
||||
|
||||
@@ -49,6 +54,8 @@ interface TokenIdPageProps {
|
||||
isErc721: boolean;
|
||||
}
|
||||
|
||||
// TODO: verify the entire nft object with zod schema and display an error message
|
||||
|
||||
export const TokenIdPage: React.FC<TokenIdPageProps> = ({
|
||||
contract,
|
||||
tokenId,
|
||||
@@ -65,6 +72,8 @@ export const TokenIdPage: React.FC<TokenIdPageProps> = ({
|
||||
tokenId,
|
||||
});
|
||||
|
||||
const client = useThirdwebClient();
|
||||
|
||||
const { data: nft, isPending } = useReadContract(
|
||||
isErc721 ? getErc721NFT : getErc1155NFT,
|
||||
{
|
||||
@@ -74,6 +83,15 @@ export const TokenIdPage: React.FC<TokenIdPageProps> = ({
|
||||
},
|
||||
);
|
||||
|
||||
const tokenURIHttpLink = resolveSchemeWithErrorHandler({
|
||||
client,
|
||||
uri: nft?.tokenURI,
|
||||
});
|
||||
const nftImageLink = resolveSchemeWithErrorHandler({
|
||||
client,
|
||||
uri: nft?.metadata.image,
|
||||
});
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="flex h-[400px] items-center justify-center">
|
||||
@@ -126,15 +144,15 @@ export const TokenIdPage: React.FC<TokenIdPageProps> = ({
|
||||
height={isMobile ? "100%" : "300px"}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Flex flexDir="column" gap={6} w="full" px={2}>
|
||||
<Flex flexDir="column" gap={2}>
|
||||
<Heading size="title.lg">{nft.metadata.name}</Heading>
|
||||
<Flex flexDir="column" gap={1.5}>
|
||||
<NFTName value={nft.metadata.name} />
|
||||
{nft.metadata?.description && (
|
||||
<Text size="label.md" noOfLines={50} whiteSpace="pre-wrap">
|
||||
{nft.metadata.description}
|
||||
</Text>
|
||||
<NFTDescription value={nft.metadata.description} />
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<Flex flexDir="column" gap={{ base: 0, md: 4 }}>
|
||||
<Box
|
||||
w="full"
|
||||
@@ -203,10 +221,11 @@ export const TokenIdPage: React.FC<TokenIdPageProps> = ({
|
||||
<Heading size="label.md">Token ID</Heading>
|
||||
</GridItem>
|
||||
<GridItem colSpan={8}>
|
||||
<AddressCopyButton
|
||||
size="xs"
|
||||
address={nft.id?.toString()}
|
||||
title="Token ID"
|
||||
<CopyTextButton
|
||||
textToCopy={nft.id?.toString()}
|
||||
textToShow={nft.id?.toString()}
|
||||
tooltip="Token ID"
|
||||
copyIconPosition="right"
|
||||
/>
|
||||
</GridItem>
|
||||
|
||||
@@ -240,6 +259,52 @@ export const TokenIdPage: React.FC<TokenIdPageProps> = ({
|
||||
</GridItem>
|
||||
</>
|
||||
)}
|
||||
<GridItem colSpan={4}>
|
||||
<Heading size="label.md">Token URI</Heading>
|
||||
</GridItem>
|
||||
<GridItem
|
||||
colSpan={8}
|
||||
className="flex flex-row items-center gap-1"
|
||||
>
|
||||
<CopyTextButton
|
||||
textToCopy={nft.tokenURI}
|
||||
textToShow={shortenString(nft.tokenURI)}
|
||||
tooltip="The URI of this NFT"
|
||||
copyIconPosition="right"
|
||||
/>
|
||||
{tokenURIHttpLink && (
|
||||
<Button variant="ghost" size="sm">
|
||||
<Link href={tokenURIHttpLink} target="_blank">
|
||||
<ExternalLinkIcon className="size-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</GridItem>
|
||||
{nft.metadata.image && (
|
||||
<>
|
||||
<GridItem colSpan={4}>
|
||||
<Heading size="label.md">Media URI</Heading>
|
||||
</GridItem>
|
||||
<GridItem
|
||||
colSpan={8}
|
||||
className="flex flex-row items-center gap-1"
|
||||
>
|
||||
<CopyTextButton
|
||||
textToCopy={nft.metadata.image}
|
||||
textToShow={shortenString(nft.metadata.image)}
|
||||
tooltip="The media URI of this NFT"
|
||||
copyIconPosition="right"
|
||||
/>
|
||||
{nftImageLink && (
|
||||
<Button variant="ghost" size="sm">
|
||||
<Link href={nftImageLink} target="_blank">
|
||||
<ExternalLinkIcon className="size-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</GridItem>
|
||||
</>
|
||||
)}
|
||||
</SimpleGrid>
|
||||
</Card>
|
||||
{properties ? (
|
||||
@@ -280,3 +345,39 @@ export const TokenIdPage: React.FC<TokenIdPageProps> = ({
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
function NFTName(props: {
|
||||
value: unknown;
|
||||
}) {
|
||||
if (typeof props.value === "string") {
|
||||
return (
|
||||
<h2 className="font-semibold text-lg tracking-tight"> {props.value}</h2>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<UnexpectedValueErrorMessage
|
||||
title="Invalid Name"
|
||||
description="Name is not a string"
|
||||
value={props.value}
|
||||
className="mb-3 rounded-lg border border-border p-4"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function NFTDescription(props: {
|
||||
value: unknown;
|
||||
}) {
|
||||
if (typeof props.value === "string") {
|
||||
return <p className="text-muted-foreground"> {props.value}</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<UnexpectedValueErrorMessage
|
||||
title="Invalid Description"
|
||||
description="Description is not a string"
|
||||
value={props.value}
|
||||
className="mb-3 rounded-lg border border-border p-4"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+84
-26
@@ -1,7 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { UnexpectedValueErrorMessage } from "@/components/blocks/error-fallbacks/unexpect-value-error-message";
|
||||
import { WalletAddress } from "@/components/blocks/wallet-address";
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
Flex,
|
||||
IconButton,
|
||||
@@ -16,6 +19,7 @@ import {
|
||||
Thead,
|
||||
Tr,
|
||||
} from "@chakra-ui/react";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { MediaCell } from "components/contract-pages/table/table-columns/cells/media-cell";
|
||||
import { useChainSlug } from "hooks/chains/chainSlug";
|
||||
import {
|
||||
@@ -26,6 +30,7 @@ import {
|
||||
ChevronRightIcon,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { ErrorBoundary, type FallbackProps } from "react-error-boundary";
|
||||
import {
|
||||
type CellProps,
|
||||
type Column,
|
||||
@@ -72,24 +77,51 @@ export const NFTGetAllTable: React.FC<ContractOverviewNFTGetAllProps> = ({
|
||||
{
|
||||
Header: "Name",
|
||||
accessor: (row) => row.metadata.name,
|
||||
Cell: (cell: CellProps<NFT, string>) => (
|
||||
<Text noOfLines={1} size="label.md">
|
||||
{cell.value}
|
||||
</Text>
|
||||
),
|
||||
Cell: (cell: CellProps<NFT, string>) => {
|
||||
if (typeof cell.value !== "string") {
|
||||
return (
|
||||
<UnexpectedValueErrorMessage
|
||||
title="Invalid Name"
|
||||
description="Name is not a string"
|
||||
value={cell.value}
|
||||
className="w-[300px] py-3"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<p className="line-clamp-1 text-muted-foreground text-sm">
|
||||
{cell.value}
|
||||
</p>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: "Description",
|
||||
accessor: (row) => row.metadata.description,
|
||||
Cell: (cell: CellProps<NFT, string>) => (
|
||||
<Text
|
||||
noOfLines={4}
|
||||
size="body.md"
|
||||
fontStyle={!cell.value ? "italic" : "normal"}
|
||||
>
|
||||
{cell.value || "No description"}
|
||||
</Text>
|
||||
),
|
||||
Cell: (cell: CellProps<NFT, string>) => {
|
||||
if (typeof cell.value !== "string") {
|
||||
return (
|
||||
<UnexpectedValueErrorMessage
|
||||
title="Invalid description"
|
||||
description="Description is not a string"
|
||||
value={cell.value}
|
||||
className="w-[300px] py-3"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
className={cn(
|
||||
"line-clamp-4 max-w-[200px] whitespace-normal text-muted-foreground text-sm",
|
||||
{ italic: !cell.value },
|
||||
)}
|
||||
>
|
||||
{cell.value || "No description"}
|
||||
</p>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
if (isErc721) {
|
||||
@@ -280,18 +312,22 @@ export const NFTGetAllTable: React.FC<ContractOverviewNFTGetAllProps> = ({
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
|
||||
key={rowIndex}
|
||||
>
|
||||
{row.cells.map((cell, cellIndex) => (
|
||||
<Td
|
||||
{...cell.getCellProps()}
|
||||
borderBottomWidth="inherit"
|
||||
borderColor="borderColor"
|
||||
maxW="sm"
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
|
||||
key={cellIndex}
|
||||
>
|
||||
{cell.render("Cell")}
|
||||
</Td>
|
||||
))}
|
||||
{row.cells.map((cell, cellIndex) => {
|
||||
return (
|
||||
<Td
|
||||
{...cell.getCellProps()}
|
||||
borderBottomWidth="inherit"
|
||||
borderColor="borderColor"
|
||||
maxW="sm"
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
|
||||
key={cellIndex}
|
||||
>
|
||||
<ErrorBoundary FallbackComponent={NFTCellErrorBoundary}>
|
||||
{cell.render("Cell")}
|
||||
</ErrorBoundary>
|
||||
</Td>
|
||||
);
|
||||
})}
|
||||
<Td borderBottomWidth="inherit" borderColor="borderColor">
|
||||
{!failedToLoad && <ArrowRightIcon className="size-4" />}
|
||||
</Td>
|
||||
@@ -374,3 +410,25 @@ export const NFTGetAllTable: React.FC<ContractOverviewNFTGetAllProps> = ({
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
function NFTCellErrorBoundary(errorProps: FallbackProps) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
Sentry.withScope((scope) => {
|
||||
scope.setTag("component-crashed", "true");
|
||||
scope.setTag("component-crashed-boundary", "NFTCellErrorBoundary");
|
||||
scope.setLevel("fatal");
|
||||
Sentry.captureException(errorProps.error);
|
||||
});
|
||||
}, [errorProps.error]);
|
||||
|
||||
return (
|
||||
<CopyTextButton
|
||||
textToShow={errorProps.error.message}
|
||||
textToCopy={errorProps.error.message}
|
||||
copyIconPosition="left"
|
||||
tooltip={errorProps.error.message}
|
||||
className="max-w-[250px] truncate text-destructive-text"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+7
-3
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { PublishedBy } from "components/contract-components/shared/published-by";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { AnalyticsOverview } from "./components/Analytics";
|
||||
import { BuildYourApp } from "./components/BuildYourApp";
|
||||
@@ -21,6 +20,7 @@ interface ContractOverviewPageProps {
|
||||
isPermissionsEnumerable: boolean;
|
||||
chainSlug: string;
|
||||
isAnalyticsSupported: boolean;
|
||||
functionSelectors: string[];
|
||||
}
|
||||
|
||||
const TRACKING_CATEGORY = "contract_overview";
|
||||
@@ -35,6 +35,7 @@ export const ContractOverviewPage: React.FC<ContractOverviewPageProps> = ({
|
||||
isPermissionsEnumerable,
|
||||
chainSlug,
|
||||
isAnalyticsSupported,
|
||||
functionSelectors,
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-8 lg:flex-row">
|
||||
@@ -45,6 +46,7 @@ export const ContractOverviewPage: React.FC<ContractOverviewPageProps> = ({
|
||||
isErc20={isErc20}
|
||||
contract={contract}
|
||||
chainSlug={chainSlug}
|
||||
functionSelectors={functionSelectors}
|
||||
/>
|
||||
|
||||
{isAnalyticsSupported && (
|
||||
@@ -98,7 +100,9 @@ export const ContractOverviewPage: React.FC<ContractOverviewPageProps> = ({
|
||||
/>
|
||||
</div>
|
||||
<div className="shrink-0 lg:w-[300px]">
|
||||
<PublishedBy contract={contract} />
|
||||
<ErrorBoundary fallback={null}>
|
||||
<PublishedBy contract={contract} />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
+2
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { ThirdwebAreaChart } from "@/components/blocks/charts/area-chart";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
|
||||
+5
-1
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Flex,
|
||||
GridItem,
|
||||
@@ -7,7 +9,9 @@ import {
|
||||
} from "@chakra-ui/react";
|
||||
import { ChakraNextImage as Image } from "components/Image";
|
||||
import { PRODUCTS } from "components/product-pages/common/nav/data";
|
||||
import { Card, Text, TrackedLink, type TrackedLinkProps } from "tw-components";
|
||||
import { Card } from "tw-components/card";
|
||||
import { TrackedLink, type TrackedLinkProps } from "tw-components/link";
|
||||
import { Text } from "tw-components/text";
|
||||
|
||||
const RENDERED_PRODUCTS = ["sdk", "storage", "ui-components", "auth"];
|
||||
|
||||
|
||||
+4
-5
@@ -1,7 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { AdminOnly } from "@3rdweb-sdk/react/components/roles/admin-only";
|
||||
import { useIsMinter } from "@3rdweb-sdk/react/hooks/useContractRoles";
|
||||
import { StepsCard } from "components/dashboard/StepsCard";
|
||||
import { useContractFunctionSelectors } from "contract-ui/hooks/useContractFunctionSelectors";
|
||||
import Link from "next/link";
|
||||
import { useMemo } from "react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
@@ -18,6 +19,7 @@ interface ContractChecklistProps {
|
||||
isErc1155: boolean;
|
||||
isErc20: boolean;
|
||||
chainSlug: string;
|
||||
functionSelectors: string[];
|
||||
}
|
||||
|
||||
type Step = {
|
||||
@@ -27,13 +29,10 @@ type Step = {
|
||||
};
|
||||
|
||||
export const ContractChecklist: React.FC<ContractChecklistProps> = (props) => {
|
||||
const functionSelectorQuery = useContractFunctionSelectors(props.contract);
|
||||
return (
|
||||
// if no permissions, simply return null (do not fail open)
|
||||
<AdminOnly contract={props.contract} failOpen={false}>
|
||||
{!!functionSelectorQuery.data?.length && (
|
||||
<Inner functionSelectors={functionSelectorQuery.data} {...props} />
|
||||
)}
|
||||
<Inner {...props} />
|
||||
</AdminOnly>
|
||||
);
|
||||
};
|
||||
|
||||
+2
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
type InternalTransaction,
|
||||
useActivity,
|
||||
|
||||
+3
-1
@@ -1,8 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { WalletAddress } from "@/components/blocks/wallet-address";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { SkeletonContainer } from "@/components/ui/skeleton";
|
||||
import { TrackedLinkTW } from "@/components/ui/tracked-link";
|
||||
import { ListingStatsV3 } from "contract-ui/tabs/listings/components/listing-stats";
|
||||
import { useMemo } from "react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import {
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
import { useReadContract } from "thirdweb/react";
|
||||
import { min } from "thirdweb/utils";
|
||||
import { NFTMediaWithEmptyState } from "tw-components/nft-media";
|
||||
import { ListingStatsV3 } from "./listing-stats";
|
||||
|
||||
type ListingData =
|
||||
| (Pick<
|
||||
|
||||
+3
-1
@@ -1,9 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { Flex, useBreakpointValue } from "@chakra-ui/react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import * as ERC721 from "thirdweb/extensions/erc721";
|
||||
import * as ERC1155 from "thirdweb/extensions/erc1155";
|
||||
import { useReadContract } from "thirdweb/react";
|
||||
import { TrackedLink, type TrackedLinkProps } from "tw-components";
|
||||
import { TrackedLink, type TrackedLinkProps } from "tw-components/link";
|
||||
import { NFTCards } from "../../_components/NFTCards";
|
||||
|
||||
interface NFTDetailsProps {
|
||||
|
||||
+10
-9
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { Box, Flex, List, SimpleGrid, Tag } from "@chakra-ui/react";
|
||||
import { getAllRoleMembers } from "contract-ui/hooks/permissions";
|
||||
@@ -8,14 +10,11 @@ import { useMemo } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { type ThirdwebContract, ZERO_ADDRESS } from "thirdweb";
|
||||
import { useReadContract } from "thirdweb/react";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Heading,
|
||||
Text,
|
||||
TrackedLink,
|
||||
type TrackedLinkProps,
|
||||
} from "tw-components";
|
||||
import { Button } from "tw-components/button";
|
||||
import { Card } from "tw-components/card";
|
||||
import { Heading } from "tw-components/heading";
|
||||
import { TrackedLink, type TrackedLinkProps } from "tw-components/link";
|
||||
import { Text } from "tw-components/text";
|
||||
import { shortenIfAddress } from "utils/usedapp-external";
|
||||
|
||||
interface PermissionsTableProps {
|
||||
@@ -41,7 +40,9 @@ export const PermissionsTable: React.FC<PermissionsTableProps> = ({
|
||||
roleMembers.forEach((member) => {
|
||||
return !acc.find((m) => m.member === member)
|
||||
? acc.push({ member, roles: [role] })
|
||||
: acc[acc.findIndex((m) => m.member === member)].roles.push(role);
|
||||
: acc[acc.findIndex((m) => m.member === member)]?.roles.push(
|
||||
role,
|
||||
);
|
||||
});
|
||||
|
||||
return acc;
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { TokenSupply } from "contract-ui/tabs/tokens/components/supply";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { TokenSupply } from "../../tokens/components/supply";
|
||||
|
||||
interface TokenDetailsProps {
|
||||
contract: ThirdwebContract;
|
||||
|
||||
@@ -31,6 +31,7 @@ export default async function Page(props: {
|
||||
}
|
||||
chainSlug={chainMetadata.slug}
|
||||
isAnalyticsSupported={contractPageMetadata.isAnalyticsSupported}
|
||||
functionSelectors={contractPageMetadata.functionSelectors}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+4
-4
@@ -3,6 +3,10 @@
|
||||
import { ButtonGroup, Flex } from "@chakra-ui/react";
|
||||
import { TransactionButton } from "components/buttons/TransactionButton";
|
||||
import { ROLE_DESCRIPTION_MAP } from "constants/mappings";
|
||||
import {
|
||||
createSetAllRoleMembersTx,
|
||||
getAllRoleMembers,
|
||||
} from "contract-ui/hooks/permissions";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
import { useMemo } from "react";
|
||||
@@ -14,10 +18,6 @@ import {
|
||||
useSendAndConfirmTransaction,
|
||||
} from "thirdweb/react";
|
||||
import { Button } from "tw-components";
|
||||
import {
|
||||
createSetAllRoleMembersTx,
|
||||
getAllRoleMembers,
|
||||
} from "../../../hooks/permissions";
|
||||
import { ContractPermission } from "./contract-permission";
|
||||
|
||||
type PermissionFormContext = {
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { ContractPermissionsPage } from "../../../../../../contract-ui/tabs/permissions/page";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { ContractPermissionsPage } from "./ContractPermissionsPage";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { ContractProposalsPage } from "../../../../../../contract-ui/tabs/proposals/page";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { ContractProposalsPage } from "./ContractProposalsPage";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
|
||||
+2
-1
@@ -89,7 +89,8 @@ export const SettingsMetadata = ({
|
||||
let image: string | undefined = metadata.data?.image;
|
||||
try {
|
||||
image = image
|
||||
? resolveScheme({
|
||||
? // eslint-disable-next-line no-restricted-syntax
|
||||
resolveScheme({
|
||||
client: contract.client,
|
||||
uri: image,
|
||||
})
|
||||
+1
-1
@@ -1,8 +1,8 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import * as CommonExt from "thirdweb/extensions/common";
|
||||
import { ContractSettingsPage } from "../../../../../../contract-ui/tabs/settings/page";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { ContractSettingsPage } from "./ContractSettingsPage";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ interface VerificationResult {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
async function verifyContract(contract: ThirdwebContract) {
|
||||
export async function verifyContract(contract: ThirdwebContract) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
"https://contract.thirdweb.com/verify/contract",
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { ContractSourcesPage } from "../../../../../../contract-ui/tabs/sources/page";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { ContractSourcesPage } from "./ContractSourcesPage";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
|
||||
+1
-1
@@ -9,6 +9,7 @@ import {
|
||||
StatLabel,
|
||||
StatNumber,
|
||||
} from "@chakra-ui/react";
|
||||
import { useAllChainsData } from "hooks/chains/allChains";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
type ThirdwebContract,
|
||||
@@ -24,7 +25,6 @@ import {
|
||||
} from "thirdweb/react";
|
||||
import { Card, Heading, Text } from "tw-components";
|
||||
import { shortenIfAddress } from "utils/usedapp-external";
|
||||
import { useAllChainsData } from "../../../hooks/chains/allChains";
|
||||
import { DistributeButton } from "./components/distribute-button";
|
||||
|
||||
export type Balance = {
|
||||
+2
-2
@@ -8,7 +8,7 @@ import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
import { useMemo } from "react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { Button } from "tw-components";
|
||||
import type { Balance } from "../page";
|
||||
import type { Balance } from "../ContractSplitPage";
|
||||
|
||||
interface DistributeButtonProps {
|
||||
contract: ThirdwebContract;
|
||||
@@ -31,7 +31,7 @@ export const DistributeButton: React.FC<DistributeButtonProps> = ({
|
||||
const numTransactions = useMemo(() => {
|
||||
if (
|
||||
validBalances.length === 1 &&
|
||||
validBalances[0].name === "Native Token"
|
||||
validBalances[0]?.name === "Native Token"
|
||||
) {
|
||||
return 1;
|
||||
}
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { ContractSplitPage } from "../../../../../../contract-ui/tabs/split/page";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { ContractSplitPage } from "./ContractSplitPage";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ export const TokenAirdropButton: React.FC<TokenAirdropButtonProps> = ({
|
||||
Airdrop
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className="z-[10000] overflow-y-auto sm:w-[540px] sm:max-w-[90%] lg:w-[700px]">
|
||||
<SheetContent className="w-full overflow-y-auto sm:min-w-[540px] lg:min-w-[700px]">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Aidrop tokens</SheetTitle>
|
||||
</SheetHeader>
|
||||
+52
-50
@@ -10,10 +10,7 @@ import type { ThirdwebContract } from "thirdweb";
|
||||
import { transferBatch } from "thirdweb/extensions/erc20";
|
||||
import { useSendAndConfirmTransaction } from "thirdweb/react";
|
||||
import { Button, Text } from "tw-components";
|
||||
import {
|
||||
AirdropUploadERC20,
|
||||
type ERC20AirdropAddressInput,
|
||||
} from "./airdrop-upload-erc20";
|
||||
import { type AirdropAddressInput, AirdropUpload } from "./airdrop-upload";
|
||||
interface TokenAirdropFormProps {
|
||||
contract: ThirdwebContract;
|
||||
toggle?: Dispatch<SetStateAction<boolean>>;
|
||||
@@ -25,7 +22,7 @@ export const TokenAirdropForm: React.FC<TokenAirdropFormProps> = ({
|
||||
toggle,
|
||||
}) => {
|
||||
const { handleSubmit, setValue, watch } = useForm<{
|
||||
addresses: ERC20AirdropAddressInput[];
|
||||
addresses: AirdropAddressInput[];
|
||||
}>({
|
||||
defaultValues: { addresses: [] },
|
||||
});
|
||||
@@ -41,55 +38,60 @@ export const TokenAirdropForm: React.FC<TokenAirdropFormProps> = ({
|
||||
<div className="pt-3">
|
||||
<form
|
||||
onSubmit={handleSubmit((data) => {
|
||||
trackEvent({
|
||||
category: "token",
|
||||
action: "airdrop",
|
||||
label: "attempt",
|
||||
contractAddress: contract.address,
|
||||
});
|
||||
const tx = transferBatch({
|
||||
contract,
|
||||
batch: data.addresses
|
||||
.filter((address) => address.quantity !== undefined)
|
||||
.map((address) => ({
|
||||
to: address.address,
|
||||
amount: address.quantity,
|
||||
})),
|
||||
});
|
||||
const promise = sendTransaction.mutateAsync(tx, {
|
||||
onSuccess: () => {
|
||||
trackEvent({
|
||||
category: "token",
|
||||
action: "airdrop",
|
||||
label: "success",
|
||||
contract_address: contract.address,
|
||||
});
|
||||
// Close the sheet/modal on success
|
||||
if (toggle) {
|
||||
toggle(false);
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
trackEvent({
|
||||
category: "token",
|
||||
action: "airdrop",
|
||||
label: "success",
|
||||
contract_address: contract.address,
|
||||
error,
|
||||
});
|
||||
console.error(error);
|
||||
},
|
||||
});
|
||||
toast.promise(promise, {
|
||||
loading: "Airdropping tokens",
|
||||
success: "Tokens airdropped successfully",
|
||||
error: "Failed to airdrop tokens",
|
||||
});
|
||||
try {
|
||||
trackEvent({
|
||||
category: "token",
|
||||
action: "airdrop",
|
||||
label: "attempt",
|
||||
contractAddress: contract.address,
|
||||
});
|
||||
const tx = transferBatch({
|
||||
contract,
|
||||
batch: data.addresses
|
||||
.filter((address) => address.quantity !== undefined)
|
||||
.map((address) => ({
|
||||
to: address.address,
|
||||
amount: address.quantity,
|
||||
})),
|
||||
});
|
||||
const promise = sendTransaction.mutateAsync(tx, {
|
||||
onSuccess: () => {
|
||||
trackEvent({
|
||||
category: "token",
|
||||
action: "airdrop",
|
||||
label: "success",
|
||||
contract_address: contract.address,
|
||||
});
|
||||
// Close the sheet/modal on success
|
||||
if (toggle) {
|
||||
toggle(false);
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
trackEvent({
|
||||
category: "token",
|
||||
action: "airdrop",
|
||||
label: "success",
|
||||
contract_address: contract.address,
|
||||
error,
|
||||
});
|
||||
console.error(error);
|
||||
},
|
||||
});
|
||||
toast.promise(promise, {
|
||||
loading: "Airdropping tokens",
|
||||
success: "Tokens airdropped successfully",
|
||||
error: "Failed to airdrop tokens",
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast.error("Failed to airdrop tokens");
|
||||
}
|
||||
})}
|
||||
>
|
||||
<div className="mb-3 flex w-full flex-col gap-6 md:flex-row">
|
||||
{airdropFormOpen ? (
|
||||
<AirdropUploadERC20
|
||||
<AirdropUpload
|
||||
onClose={() => setAirdropFormOpen(false)}
|
||||
setAirdrop={(value) =>
|
||||
setValue("addresses", value, { shouldDirty: true })
|
||||
+10
-15
@@ -33,17 +33,17 @@ import { isAddress } from "thirdweb/utils";
|
||||
import { Button, Heading, Text } from "tw-components";
|
||||
import { csvMimeTypes } from "utils/batch";
|
||||
|
||||
export interface ERC20AirdropAddressInput {
|
||||
export interface AirdropAddressInput {
|
||||
address: string;
|
||||
quantity: string;
|
||||
isValid?: boolean;
|
||||
}
|
||||
interface AirdropUploadProps {
|
||||
setAirdrop: (airdrop: ERC20AirdropAddressInput[]) => void;
|
||||
setAirdrop: (airdrop: AirdropAddressInput[]) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
async function checkIsAddress(
|
||||
item: ERC20AirdropAddressInput,
|
||||
item: AirdropAddressInput,
|
||||
thirdwebClient: ThirdwebClient,
|
||||
) {
|
||||
const { address, ...rest } = item;
|
||||
@@ -75,10 +75,7 @@ async function checkIsAddress(
|
||||
};
|
||||
}
|
||||
function processAirdropData(
|
||||
data: (
|
||||
| (ERC20AirdropAddressInput & { resolvedAddress: string })
|
||||
| undefined
|
||||
)[],
|
||||
data: ((AirdropAddressInput & { resolvedAddress: string }) | undefined)[],
|
||||
) {
|
||||
const seen = new Set();
|
||||
const filteredData = data
|
||||
@@ -95,14 +92,12 @@ function processAirdropData(
|
||||
return ordered;
|
||||
}
|
||||
|
||||
export const AirdropUploadERC20: React.FC<AirdropUploadProps> = ({
|
||||
export const AirdropUpload: React.FC<AirdropUploadProps> = ({
|
||||
setAirdrop,
|
||||
onClose,
|
||||
}) => {
|
||||
const thirdwebClient = useThirdwebClient();
|
||||
const [validAirdrop, setValidAirdrop] = useState<ERC20AirdropAddressInput[]>(
|
||||
[],
|
||||
);
|
||||
const [validAirdrop, setValidAirdrop] = useState<AirdropAddressInput[]>([]);
|
||||
const [noCsv, setNoCsv] = useState(false);
|
||||
const reset = useCallback(() => {
|
||||
setValidAirdrop([]);
|
||||
@@ -124,8 +119,8 @@ export const AirdropUploadERC20: React.FC<AirdropUploadProps> = ({
|
||||
Papa.parse(csv, {
|
||||
header: true,
|
||||
complete: (results) => {
|
||||
const data: ERC20AirdropAddressInput[] = (
|
||||
results.data as ERC20AirdropAddressInput[]
|
||||
const data: AirdropAddressInput[] = (
|
||||
results.data as AirdropAddressInput[]
|
||||
)
|
||||
.map(({ address, quantity }) => ({
|
||||
address: (address || "").trim(),
|
||||
@@ -278,7 +273,7 @@ export const AirdropUploadERC20: React.FC<AirdropUploadProps> = ({
|
||||
};
|
||||
|
||||
interface AirdropTableProps {
|
||||
data: ERC20AirdropAddressInput[];
|
||||
data: AirdropAddressInput[];
|
||||
portalRef: React.RefObject<HTMLDivElement>;
|
||||
}
|
||||
const AirdropTable: React.FC<AirdropTableProps> = ({ data, portalRef }) => {
|
||||
@@ -316,7 +311,7 @@ const AirdropTable: React.FC<AirdropTableProps> = ({ data, portalRef }) => {
|
||||
return quantity || "1";
|
||||
},
|
||||
},
|
||||
] as Column<ERC20AirdropAddressInput>[];
|
||||
] as Column<AirdropAddressInput>[];
|
||||
}, []);
|
||||
const {
|
||||
getTableProps,
|
||||
+71
-60
@@ -101,78 +101,89 @@ export const TokenClaimButton: React.FC<TokenClaimButtonProps> = ({
|
||||
colorScheme="primary"
|
||||
isDisabled={!form.formState.isDirty || isPending}
|
||||
onClick={form.handleSubmit(async (d) => {
|
||||
if (!d.to) {
|
||||
return toast.error(
|
||||
"Need to speficy an address to receive tokens",
|
||||
);
|
||||
}
|
||||
trackEvent({
|
||||
category: "token",
|
||||
action: "claim",
|
||||
label: "attempt",
|
||||
});
|
||||
if (!account) {
|
||||
return toast.error("No account detected");
|
||||
}
|
||||
const transaction = ERC20Ext.claimTo({
|
||||
contract,
|
||||
to: d.to,
|
||||
quantity: d.amount,
|
||||
from: account.address,
|
||||
});
|
||||
try {
|
||||
if (!d.to) {
|
||||
return toast.error(
|
||||
"Need to specify an address to receive tokens",
|
||||
);
|
||||
}
|
||||
trackEvent({
|
||||
category: "token",
|
||||
action: "claim",
|
||||
label: "attempt",
|
||||
});
|
||||
if (!account) {
|
||||
return toast.error("No account detected");
|
||||
}
|
||||
const transaction = ERC20Ext.claimTo({
|
||||
contract,
|
||||
to: d.to,
|
||||
quantity: d.amount,
|
||||
from: account.address,
|
||||
});
|
||||
|
||||
const approveTx = await ERC20Ext.getApprovalForTransaction({
|
||||
transaction,
|
||||
account,
|
||||
});
|
||||
const approveTx = await ERC20Ext.getApprovalForTransaction({
|
||||
transaction,
|
||||
account,
|
||||
});
|
||||
|
||||
if (approveTx) {
|
||||
const promise = sendAndConfirmTransaction.mutateAsync(
|
||||
approveTx,
|
||||
{
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
},
|
||||
},
|
||||
);
|
||||
toast.promise(promise, {
|
||||
loading: "Approving ERC20 tokens for this claim",
|
||||
success: "Tokens approved successfully",
|
||||
error: "Failed to approve token",
|
||||
});
|
||||
|
||||
await promise;
|
||||
}
|
||||
|
||||
if (approveTx) {
|
||||
const promise = sendAndConfirmTransaction.mutateAsync(
|
||||
approveTx,
|
||||
transaction,
|
||||
{
|
||||
onSuccess: () => {
|
||||
trackEvent({
|
||||
category: "token",
|
||||
action: "claim",
|
||||
label: "success",
|
||||
});
|
||||
form.reset({ amount: "0", to: account?.address });
|
||||
setOpen(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
trackEvent({
|
||||
category: "token",
|
||||
action: "claim",
|
||||
label: "error",
|
||||
error,
|
||||
});
|
||||
console.error(error);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
toast.promise(promise, {
|
||||
loading: "Approving ERC20 tokens for this claim",
|
||||
success: "Tokens approved successfully",
|
||||
error: "Failed to approve token",
|
||||
loading: "Claiming tokens",
|
||||
success: "Token claimed successfully",
|
||||
error: "Failed to claim tokens",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error("Failed to claim tokens");
|
||||
trackEvent({
|
||||
category: "token",
|
||||
action: "claim",
|
||||
label: "error",
|
||||
error,
|
||||
});
|
||||
|
||||
await promise;
|
||||
}
|
||||
|
||||
const promise = sendAndConfirmTransaction.mutateAsync(
|
||||
transaction,
|
||||
{
|
||||
onSuccess: () => {
|
||||
trackEvent({
|
||||
category: "token",
|
||||
action: "claim",
|
||||
label: "success",
|
||||
});
|
||||
form.reset({ amount: "0", to: account?.address });
|
||||
setOpen(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
trackEvent({
|
||||
category: "token",
|
||||
action: "claim",
|
||||
label: "error",
|
||||
error,
|
||||
});
|
||||
console.error(error);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
toast.promise(promise, {
|
||||
loading: "Claiming tokens",
|
||||
success: "Token claimed successfully",
|
||||
error: "Failed to claim tokens",
|
||||
});
|
||||
})}
|
||||
>
|
||||
Claim Tokens
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user