Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c7d8411b9 | ||
|
|
16ab7da64f | ||
|
|
467ba50907 | ||
|
|
2f627e1483 | ||
|
|
6b80f75e3a | ||
|
|
c43c686eac | ||
|
|
92896ddf3f | ||
|
|
a08b62be9a | ||
|
|
7f116577ab | ||
|
|
d227bc389b | ||
|
|
6c711e8783 | ||
|
|
6f581c80b3 | ||
|
|
b1ac15c2ac | ||
|
|
98eefdf90b | ||
|
|
857d4dfc16 | ||
|
|
22aad59e7d | ||
|
|
a3b2f8f4b0 |
@@ -21,8 +21,8 @@ module.exports = [
|
||||
"/storage",
|
||||
// -- end scale category
|
||||
|
||||
// -- nebula
|
||||
"/nebula",
|
||||
// -- ai
|
||||
"/ai",
|
||||
// -- contracts
|
||||
"/contracts",
|
||||
"/contracts/modular-contracts",
|
||||
|
||||
@@ -10,8 +10,11 @@
|
||||
"ignoreDependencies": [
|
||||
"@thirdweb-dev/service-utils",
|
||||
"@thirdweb-dev/vault-sdk",
|
||||
"thirdweb",
|
||||
"@types/color",
|
||||
"fast-xml-parser"
|
||||
"fast-xml-parser",
|
||||
"@workspace/ui",
|
||||
"tailwindcss-animate"
|
||||
],
|
||||
"next": true,
|
||||
"project": ["src/**"]
|
||||
|
||||
@@ -115,6 +115,7 @@ const SENTRY_OPTIONS: SentryBuildOptions = {
|
||||
const FRAMER_ADDITIONAL_LANGUAGES = ["es"];
|
||||
|
||||
const baseNextConfig: NextConfig = {
|
||||
transpilePackages: ["@workspace/ui"],
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
@@ -196,7 +197,6 @@ const baseNextConfig: NextConfig = {
|
||||
]),
|
||||
];
|
||||
},
|
||||
serverExternalPackages: ["pino-pretty"],
|
||||
};
|
||||
|
||||
function getConfig(): NextConfig {
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"@thirdweb-dev/vault-sdk": "workspace:*",
|
||||
"@vercel/functions": "2.2.2",
|
||||
"@vercel/og": "^0.6.8",
|
||||
"@workspace/ui": "workspace:*",
|
||||
"abitype": "1.0.8",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
autoprefixer: {},
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@workspace/ui/postcss.config";
|
||||
@@ -460,6 +460,12 @@ async function redirects() {
|
||||
permanent: false,
|
||||
source: "/universal-bridge",
|
||||
},
|
||||
// redirect /nebula to /ai
|
||||
{
|
||||
destination: "/ai",
|
||||
permanent: false,
|
||||
source: "/nebula",
|
||||
},
|
||||
...legacyDashboardToTeamRedirects,
|
||||
...projectPageRedirects,
|
||||
...teamPageRedirects,
|
||||
|
||||
@@ -224,7 +224,11 @@ export function reportChainConfigurationAdded(properties: {
|
||||
// ASSETS
|
||||
// ----------------------------
|
||||
|
||||
type AssetContractType = "DropERC20" | "DropERC1155" | "DropERC721";
|
||||
type AssetContractType =
|
||||
| "DropERC20"
|
||||
| "DropERC1155"
|
||||
| "DropERC721"
|
||||
| "ERC20Asset";
|
||||
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
@@ -334,6 +338,15 @@ export function reportAssetCreationSuccessful(properties: {
|
||||
});
|
||||
}
|
||||
|
||||
type CoinCreationStep =
|
||||
| "erc20-asset:deploy-contract"
|
||||
| "erc20-asset:airdrop-tokens"
|
||||
| "erc20-asset:approve-airdrop-tokens"
|
||||
| "drop-erc20:deploy-contract"
|
||||
| "drop-erc20:set-claim-conditions"
|
||||
| "drop-erc20:mint-tokens"
|
||||
| "drop-erc20:airdrop-tokens";
|
||||
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
* - To track number of failed asset creations
|
||||
@@ -355,11 +368,7 @@ export function reportAssetCreationFailed(
|
||||
}
|
||||
| {
|
||||
assetType: "coin";
|
||||
step:
|
||||
| "deploy-contract"
|
||||
| "set-claim-conditions"
|
||||
| "mint-tokens"
|
||||
| "airdrop-tokens";
|
||||
step: CoinCreationStep;
|
||||
}
|
||||
),
|
||||
) {
|
||||
@@ -517,3 +526,24 @@ export function reportTokenUpsellClicked(params: {
|
||||
}) {
|
||||
posthog.capture("token upsell clicked", params);
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// CHAIN INFRASTRUCTURE CHECKOUT
|
||||
// ----------------------------
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
* - To record explicit user acknowledgement when proceeding to checkout without RPC
|
||||
* - To measure how often customers choose to omit RPC while purchasing Insight and/or Account Abstraction
|
||||
* - To correlate potential support issues arising from missing RPC
|
||||
*
|
||||
* ### Who is responsible for this event?
|
||||
* @jnsdls
|
||||
*/
|
||||
export function reportChainInfraRpcOmissionAgreed(properties: {
|
||||
chainId: number;
|
||||
frequency: "monthly" | "annual";
|
||||
includeInsight: boolean;
|
||||
includeAccountAbstraction: boolean;
|
||||
}) {
|
||||
posthog.capture("chain infra checkout rpc omission agreed", properties);
|
||||
}
|
||||
|
||||
@@ -29,16 +29,18 @@ export function MultiNetworkSelector(props: {
|
||||
client: ThirdwebClient;
|
||||
chainIds?: number[];
|
||||
}) {
|
||||
let { allChains, idToChain } = useAllChainsData();
|
||||
|
||||
if (props.chainIds && props.chainIds.length > 0) {
|
||||
allChains = allChains.filter((chain) =>
|
||||
props.chainIds?.includes(chain.chainId),
|
||||
);
|
||||
}
|
||||
const { allChains, idToChain } = useAllChainsData();
|
||||
|
||||
const options = useMemo(() => {
|
||||
let sortedChains = allChains;
|
||||
let chains = allChains.filter((chain) => chain.status !== "deprecated");
|
||||
|
||||
if (props.chainIds && props.chainIds.length > 0) {
|
||||
chains = allChains.filter((chain) =>
|
||||
props.chainIds?.includes(chain.chainId),
|
||||
);
|
||||
}
|
||||
|
||||
let sortedChains = chains;
|
||||
|
||||
if (props.priorityChains) {
|
||||
const priorityChainsSet = new Set();
|
||||
@@ -69,7 +71,13 @@ export function MultiNetworkSelector(props: {
|
||||
value: String(chain.chainId),
|
||||
};
|
||||
});
|
||||
}, [allChains, props.priorityChains, idToChain, props.hideTestnets]);
|
||||
}, [
|
||||
allChains,
|
||||
props.priorityChains,
|
||||
idToChain,
|
||||
props.hideTestnets,
|
||||
props.chainIds,
|
||||
]);
|
||||
|
||||
const searchFn = useCallback(
|
||||
(option: Option, searchValue: string) => {
|
||||
@@ -155,16 +163,38 @@ export function SingleNetworkSelector(props: {
|
||||
disableDeprecated?: boolean;
|
||||
placeholder?: string;
|
||||
client: ThirdwebClient;
|
||||
priorityChains?: number[];
|
||||
}) {
|
||||
const { allChains, idToChain } = useAllChainsData();
|
||||
|
||||
const chainsToShow = useMemo(() => {
|
||||
let chains = allChains;
|
||||
|
||||
chains = chains.filter((chain) => chain.status !== "deprecated");
|
||||
|
||||
if (props.disableTestnets) {
|
||||
chains = chains.filter((chain) => !chain.testnet);
|
||||
}
|
||||
|
||||
if (props.priorityChains) {
|
||||
const priorityChainsSet = new Set();
|
||||
for (const chainId of props.priorityChains || []) {
|
||||
priorityChainsSet.add(chainId);
|
||||
}
|
||||
|
||||
const priorityChains = (props.priorityChains || [])
|
||||
.map((chainId) => {
|
||||
return idToChain.get(chainId);
|
||||
})
|
||||
.filter((v) => !!v);
|
||||
|
||||
const otherChains = allChains.filter(
|
||||
(chain) => !priorityChainsSet.has(chain.chainId),
|
||||
);
|
||||
|
||||
chains = [...priorityChains, ...otherChains];
|
||||
}
|
||||
|
||||
if (props.chainIds) {
|
||||
const chainIdSet = new Set(props.chainIds);
|
||||
chains = chains.filter((chain) => chainIdSet.has(chain.chainId));
|
||||
@@ -180,6 +210,8 @@ export function SingleNetworkSelector(props: {
|
||||
props.chainIds,
|
||||
props.disableTestnets,
|
||||
props.disableDeprecated,
|
||||
props.priorityChains,
|
||||
idToChain,
|
||||
]);
|
||||
|
||||
const options = useMemo(() => {
|
||||
|
||||
@@ -204,7 +204,7 @@ export function TokenSelector(props: {
|
||||
searchPlaceholder="Search by name or symbol"
|
||||
showCheck={props.showCheck}
|
||||
side={props.side}
|
||||
value={selectedValue}
|
||||
value={tokensQuery.isPending ? undefined : selectedValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,12 +3,15 @@ import { cn } from "@/lib/utils";
|
||||
export type Segment = {
|
||||
label: string;
|
||||
percent: number;
|
||||
value: string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
type DistributionBarChartProps = {
|
||||
segments: Segment[];
|
||||
title: string;
|
||||
title?: string;
|
||||
titleClassName?: string;
|
||||
barClassName?: string;
|
||||
};
|
||||
|
||||
export function DistributionBarChart(props: DistributionBarChartProps) {
|
||||
@@ -21,24 +24,36 @@ export function DistributionBarChart(props: DistributionBarChartProps) {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<h3 className="font-medium text-sm">{props.title}</h3>
|
||||
<div
|
||||
className={cn(
|
||||
"font-medium text-muted-foreground text-sm",
|
||||
invalidTotalPercentage && "text-red-500",
|
||||
)}
|
||||
>
|
||||
Total: {totalPercentage}%
|
||||
{props.title && (
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<h3 className={cn("font-medium text-sm", props.titleClassName)}>
|
||||
{props.title}
|
||||
</h3>
|
||||
<div
|
||||
className={cn(
|
||||
"font-medium text-muted-foreground text-sm",
|
||||
invalidTotalPercentage && "text-red-500",
|
||||
)}
|
||||
>
|
||||
Total: {totalPercentage}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Bar */}
|
||||
<div className="flex h-3 overflow-hidden rounded-lg">
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-3 overflow-hidden rounded-lg",
|
||||
props.barClassName,
|
||||
)}
|
||||
>
|
||||
{props.segments.map((segment) => {
|
||||
return (
|
||||
<div
|
||||
className="flex h-full items-center justify-center transition-all duration-200"
|
||||
className={cn(
|
||||
"flex h-full items-center justify-center transition-all duration-200",
|
||||
segment.percent > 0 && "border-r-2 border-background",
|
||||
)}
|
||||
key={segment.label}
|
||||
style={{
|
||||
backgroundColor: segment.color,
|
||||
@@ -67,7 +82,7 @@ export function DistributionBarChart(props: DistributionBarChartProps) {
|
||||
"text-destructive-text",
|
||||
)}
|
||||
>
|
||||
{segment.label}: {segment.percent}%
|
||||
{segment.label}: {segment.value}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -34,7 +34,7 @@ export function MultiStepStatus<T extends string>(props: {
|
||||
}) {
|
||||
return (
|
||||
<DynamicHeight>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4 overflow-hidden">
|
||||
{props.steps.map((step) => (
|
||||
<div className="flex items-start space-x-3 " key={step.label}>
|
||||
{step.status.type === "completed" ? (
|
||||
@@ -46,7 +46,7 @@ export function MultiStepStatus<T extends string>(props: {
|
||||
) : (
|
||||
<CircleIcon className="mt-0.5 size-5 flex-shrink-0 text-muted-foreground/70" />
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<div className="grow">
|
||||
<p
|
||||
className={`font-medium ${
|
||||
step.status.type === "pending"
|
||||
@@ -73,7 +73,7 @@ export function MultiStepStatus<T extends string>(props: {
|
||||
{step.status.type === "error"
|
||||
? props.renderError?.(step, step.status.message) || (
|
||||
<div className="mt-1 space-y-2">
|
||||
<p className="mb-1 text-red-500 text-sm">
|
||||
<p className="mb-1 text-red-500 text-sm whitespace-pre-wrap break-all">
|
||||
{step.status.message}
|
||||
</p>
|
||||
<Button
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"use client";
|
||||
import { CheckIcon, CopyIcon, XIcon } from "lucide-react";
|
||||
import { CheckIcon, CircleSlashIcon, CopyIcon, XIcon } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { isAddress, type ThirdwebClient, ZERO_ADDRESS } from "thirdweb";
|
||||
import { Blobbie, type SocialProfile, useSocialProfiles } from "thirdweb/react";
|
||||
@@ -23,6 +23,7 @@ export function WalletAddress(props: {
|
||||
className?: string;
|
||||
iconClassName?: string;
|
||||
client: ThirdwebClient;
|
||||
fallbackIcon?: React.ReactNode;
|
||||
}) {
|
||||
// default back to zero address if no address provided
|
||||
const address = useMemo(() => props.address || ZERO_ADDRESS, [props.address]);
|
||||
@@ -59,9 +60,16 @@ export function WalletAddress(props: {
|
||||
// special case for zero address
|
||||
if (address === ZERO_ADDRESS) {
|
||||
return (
|
||||
<span className={cn("cursor-pointer font-mono", props.className)}>
|
||||
{shortenedAddress}
|
||||
</span>
|
||||
<div className="flex items-center gap-2 py-2">
|
||||
<CircleSlashIcon
|
||||
className={cn("size-6 text-muted-foreground/70", props.iconClassName)}
|
||||
/>
|
||||
<span
|
||||
className={cn("cursor-pointer font-mono text-sm", props.className)}
|
||||
>
|
||||
{shortenedAddress}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -82,6 +90,7 @@ export function WalletAddress(props: {
|
||||
iconClassName={props.iconClassName}
|
||||
profiles={profiles.data || []}
|
||||
thirdwebClient={props.client}
|
||||
fallbackIcon={props.fallbackIcon}
|
||||
/>
|
||||
)}
|
||||
<span className="cursor-pointer font-mono">
|
||||
@@ -173,6 +182,7 @@ function WalletAvatar(props: {
|
||||
profiles: SocialProfile[];
|
||||
thirdwebClient: ThirdwebClient;
|
||||
iconClassName?: string;
|
||||
fallbackIcon?: React.ReactNode;
|
||||
}) {
|
||||
const avatar = useMemo(() => {
|
||||
return props.profiles.find(
|
||||
@@ -199,6 +209,8 @@ function WalletAvatar(props: {
|
||||
className={cn("size-5 object-cover", props.iconClassName)}
|
||||
src={resolvedAvatarSrc}
|
||||
/>
|
||||
) : props.fallbackIcon ? (
|
||||
props.fallbackIcon
|
||||
) : (
|
||||
<Blobbie
|
||||
address={props.address}
|
||||
|
||||
@@ -300,6 +300,7 @@ const contractTypeToAssetTypeRecord: Record<string, string | undefined> = {
|
||||
DropERC20: "Coin",
|
||||
DropERC721: "NFT Collection",
|
||||
DropERC1155: "NFT Collection",
|
||||
ERC20Asset: "Coin",
|
||||
};
|
||||
|
||||
const NetworkFilterCell = React.memo(function NetworkFilterCell({
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { DialogDescription } from "@radix-ui/react-dialog";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import type { ProjectService } from "@thirdweb-dev/service-utils";
|
||||
import { SERVICES } from "@thirdweb-dev/service-utils";
|
||||
@@ -18,6 +17,7 @@ import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import type { ThirdwebClient } from "thirdweb/dist/types/client/client";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { analyticsServerProxy } from "@/actions/proxies";
|
||||
import {
|
||||
type SponsoredTransaction,
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
} from "thirdweb";
|
||||
import { type Chain, type ChainMetadata, localhost } from "thirdweb/chains";
|
||||
import {
|
||||
PayEmbed,
|
||||
BuyWidget,
|
||||
useActiveAccount,
|
||||
useActiveWallet,
|
||||
useActiveWalletChain,
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
import { privateKeyToAccount, type Wallet } from "thirdweb/wallets";
|
||||
import { apiServerProxy } from "@/actions/proxies";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DynamicHeight } from "@/components/ui/DynamicHeight";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -251,39 +250,32 @@ export const MismatchButton = forwardRef<
|
||||
>
|
||||
<DialogContent
|
||||
className={cn(
|
||||
"gap-0 p-0",
|
||||
dialog === "no-funds" && "md:!max-w-[480px]",
|
||||
dialog === "pay" && "md:!max-w-[360px] border-none bg-transparent",
|
||||
"gap-0 p-0 md:!max-w-[480px]",
|
||||
dialog === "pay" && "border-none bg-transparent",
|
||||
)}
|
||||
dialogCloseClassName="focus:ring-0"
|
||||
>
|
||||
<DynamicHeight>
|
||||
{dialog === "no-funds" && (
|
||||
<NoFundsDialogContent
|
||||
chain={txChain}
|
||||
client={props.client}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
onCloseModal={() => setDialog(undefined)}
|
||||
openPayModal={() => {
|
||||
setDialog("pay");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{dialog === "no-funds" && (
|
||||
<NoFundsDialogContent
|
||||
chain={txChain}
|
||||
client={props.client}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
onCloseModal={() => setDialog(undefined)}
|
||||
openPayModal={() => {
|
||||
setDialog("pay");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{dialog === "pay" && (
|
||||
<PayEmbed
|
||||
className="!w-auto"
|
||||
client={props.client}
|
||||
payOptions={{
|
||||
prefillBuy: {
|
||||
amount: "0.01",
|
||||
chain: txChain,
|
||||
},
|
||||
}}
|
||||
theme={getSDKTheme(theme === "dark" ? "dark" : "light")}
|
||||
/>
|
||||
)}
|
||||
</DynamicHeight>
|
||||
{dialog === "pay" && (
|
||||
<BuyWidget
|
||||
className="!w-full"
|
||||
client={props.client}
|
||||
chain={txChain}
|
||||
amount="0.01"
|
||||
theme={getSDKTheme(theme === "dark" ? "dark" : "light")}
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
@@ -362,11 +354,7 @@ function NoFundsDialogContent(props: {
|
||||
(x) => x.enabled && x.service === "pay",
|
||||
) ? (
|
||||
// pay case
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={props.openPayModal}
|
||||
variant="primary"
|
||||
>
|
||||
<Button className="w-full" onClick={props.openPayModal}>
|
||||
Buy Funds
|
||||
</Button>
|
||||
) : // no funds options available
|
||||
|
||||
@@ -25,6 +25,7 @@ export function CopyAddressButton(props: {
|
||||
copyIconPosition={props.copyIconPosition}
|
||||
textToCopy={props.address}
|
||||
textToShow={shortenedAddress}
|
||||
iconClassName={props.iconClassName}
|
||||
tooltip={props.tooltip || "Copy Address"}
|
||||
variant={props.variant}
|
||||
/>
|
||||
|
||||
@@ -17,7 +17,7 @@ export function DynamicHeight(props: {
|
||||
boxSizing: "border-box",
|
||||
height: height ? `${height}px` : "auto",
|
||||
overflow: "hidden",
|
||||
transition: props.transition || "height 250ms ease",
|
||||
transition: props.transition || "height 220ms ease",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -1,83 +1,3 @@
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
defaultVariants: {
|
||||
size: "default",
|
||||
variant: "default",
|
||||
},
|
||||
variants: {
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
icon: "h-10 w-10",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
},
|
||||
variant: {
|
||||
default: "bg-foreground text-background hover:bg-foreground/90",
|
||||
destructive:
|
||||
"bg-destructive hover:bg-destructive/90 text-destructive-foreground ",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "underline-offset-4 hover:underline",
|
||||
outline:
|
||||
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
|
||||
primary: "bg-primary hover:bg-primary/90 text-primary-foreground ",
|
||||
secondary:
|
||||
"bg-secondary hover:bg-secondary/80 text-secondary-foreground ",
|
||||
upsell:
|
||||
"bg-green-600 text-white hover:bg-green-700 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5 transition-all duration-200",
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, disabled, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
// "button" elements automatically handle the `disabled` attribute.
|
||||
// For non-button elements rendered via `asChild` (e.g. <a>), we still want
|
||||
// to visually convey the disabled state and prevent user interaction.
|
||||
// We do that by conditionally adding the same utility classes that the
|
||||
// `disabled:` pseudo-variant would normally apply and by setting
|
||||
// `aria-disabled` for accessibility.
|
||||
const disabledClass = disabled ? "pointer-events-none opacity-50" : "";
|
||||
|
||||
const btnOnlyProps =
|
||||
Comp === "button"
|
||||
? {
|
||||
type:
|
||||
(props as React.ButtonHTMLAttributes<HTMLButtonElement>).type ||
|
||||
("button" as const),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<Comp
|
||||
aria-disabled={disabled ? true : undefined}
|
||||
className={cn(
|
||||
buttonVariants({ className, size, variant }),
|
||||
disabledClass,
|
||||
)}
|
||||
disabled={disabled}
|
||||
ref={ref}
|
||||
{...props}
|
||||
{...btnOnlyProps}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button, buttonVariants };
|
||||
"use client";
|
||||
export type { ButtonProps } from "@workspace/ui/components/button";
|
||||
export { Button, buttonVariants } from "@workspace/ui/components/button";
|
||||
|
||||
@@ -98,6 +98,7 @@ export function TabButtons(props: {
|
||||
shadowColor?: string;
|
||||
tabIconClassName?: string;
|
||||
hideBottomLine?: boolean;
|
||||
bottomLineClassName?: string;
|
||||
}) {
|
||||
const { containerRef, lineRef, activeTabRef } =
|
||||
useUnderline<HTMLButtonElement>();
|
||||
@@ -106,7 +107,12 @@ export function TabButtons(props: {
|
||||
<div className={cn("relative", props.containerClassName)}>
|
||||
{/* Bottom line */}
|
||||
{!props.hideBottomLine && (
|
||||
<div className="absolute right-0 bottom-0 left-0 h-[1px] bg-border" />
|
||||
<div
|
||||
className={cn(
|
||||
"absolute right-0 bottom-0 left-0 h-[1px] bg-border",
|
||||
props.bottomLineClassName,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ScrollShadow
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
export const THIRDWEB_DEPLOYER_ADDRESS =
|
||||
"0xdd99b75f095d0c4d5112aCe938e4e6ed962fb024";
|
||||
|
||||
export const THIRDWEB_PUBLISHER_ADDRESS =
|
||||
"0xdd99b75f095d0c4d5112aCe938e4e6ed962fb024";
|
||||
|
||||
export const DEFAULT_FEE_RECIPIENT =
|
||||
"0x1af20c6b23373350ad464700b5965ce4b0d2ad94";
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"use client";
|
||||
|
||||
import { CircleAlertIcon, InfoIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { createContext, useCallback, useContext, useState } from "react";
|
||||
|
||||
@@ -11,7 +11,12 @@ export function useAddContractToProject() {
|
||||
contractAddress: string;
|
||||
chainId: string;
|
||||
deploymentType: "asset" | undefined;
|
||||
contractType: "DropERC20" | "DropERC721" | "DropERC1155" | undefined;
|
||||
contractType:
|
||||
| "ERC20Asset"
|
||||
| "DropERC721"
|
||||
| "DropERC1155"
|
||||
| "DropERC20"
|
||||
| undefined;
|
||||
}) => {
|
||||
const res = await apiServerProxy({
|
||||
body: JSON.stringify({
|
||||
|
||||
+1
-2
@@ -1,5 +1,4 @@
|
||||
// FIXME: export listing status type
|
||||
import type { ListingStatus } from "thirdweb/dist/types/extensions/marketplace/types";
|
||||
import type { ListingStatus } from "thirdweb/extensions/marketplace";
|
||||
|
||||
export const LISTING_STATUS: Record<ListingStatus, string> = {
|
||||
ACTIVE: "Active",
|
||||
|
||||
+10
-16
@@ -5,13 +5,12 @@ import {
|
||||
toUnits,
|
||||
} from "thirdweb";
|
||||
import { getContract } from "thirdweb/contract";
|
||||
import type { OverrideEntry } from "thirdweb/dist/types/utils/extensions/drops/types";
|
||||
import type { Prettify } from "thirdweb/dist/types/utils/type-utils";
|
||||
import { getContractMetadata } from "thirdweb/extensions/common";
|
||||
import * as ERC20Ext from "thirdweb/extensions/erc20";
|
||||
import * as ERC721Ext from "thirdweb/extensions/erc721";
|
||||
import * as ERC1155Ext from "thirdweb/extensions/erc1155";
|
||||
import { download } from "thirdweb/storage";
|
||||
import type { OverrideEntry } from "thirdweb/utils";
|
||||
import { maxUint256 } from "thirdweb/utils";
|
||||
import type { z } from "zod";
|
||||
import type {
|
||||
@@ -26,20 +25,15 @@ type SnapshotEntry = {
|
||||
currencyAddress?: string | undefined;
|
||||
};
|
||||
|
||||
type CombinedClaimCondition = Prettify<
|
||||
Omit<
|
||||
LegacyClaimCondition,
|
||||
"price" | "waitInSeconds" | "availableSupply" | "currencyMetadata"
|
||||
> & {
|
||||
price: bigint;
|
||||
currencyMetadata: Omit<
|
||||
LegacyClaimCondition["currencyMetadata"],
|
||||
"value"
|
||||
> & {
|
||||
value: bigint;
|
||||
};
|
||||
}
|
||||
>;
|
||||
type CombinedClaimCondition = Omit<
|
||||
LegacyClaimCondition,
|
||||
"price" | "waitInSeconds" | "availableSupply" | "currencyMetadata"
|
||||
> & {
|
||||
price: bigint;
|
||||
currencyMetadata: Omit<LegacyClaimCondition["currencyMetadata"], "value"> & {
|
||||
value: bigint;
|
||||
};
|
||||
};
|
||||
|
||||
type Options =
|
||||
| {
|
||||
|
||||
+1
@@ -25,6 +25,7 @@ export type ContractPageMetadata = {
|
||||
isAccount: boolean;
|
||||
isAccountPermissionsSupported: boolean;
|
||||
functionSelectors: string[];
|
||||
showClaimRewards: boolean;
|
||||
};
|
||||
|
||||
export async function getContractPageMetadata(contract: ThirdwebContract) {
|
||||
|
||||
+37
@@ -1,6 +1,8 @@
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { contractType as getContractType } from "thirdweb/extensions/thirdweb";
|
||||
import { getDeployedEntrypointERC20 } from "thirdweb/tokens";
|
||||
import { resolveFunctionSelectors } from "@/lib/selectors";
|
||||
import { getValidReward } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/utils/rewards";
|
||||
import {
|
||||
isERC20ClaimConditionsSupported,
|
||||
isERC721ClaimConditionsSupported,
|
||||
@@ -43,6 +45,7 @@ type ContractPageMetadata = {
|
||||
isAccount: boolean;
|
||||
isAccountPermissionsSupported: boolean;
|
||||
functionSelectors: string[];
|
||||
showClaimRewards: boolean;
|
||||
};
|
||||
|
||||
export async function getContractPageMetadataSetup(
|
||||
@@ -53,10 +56,14 @@ export async function getContractPageMetadataSetup(
|
||||
functionSelectorsResult,
|
||||
isInsightSupportedResult,
|
||||
contractTypeResult,
|
||||
claimRewardResult,
|
||||
] = await Promise.allSettled([
|
||||
resolveFunctionSelectors(contract),
|
||||
isAnalyticsSupportedFn(contract.chain.id),
|
||||
getContractType({ contract }),
|
||||
isClaimRewardsSupported({
|
||||
assetContract: contract,
|
||||
}),
|
||||
]);
|
||||
|
||||
const functionSelectors =
|
||||
@@ -72,6 +79,11 @@ export async function getContractPageMetadataSetup(
|
||||
const contractType =
|
||||
contractTypeResult.status === "fulfilled" ? contractTypeResult.value : null;
|
||||
|
||||
const showClaimRewards =
|
||||
claimRewardResult.status === "fulfilled"
|
||||
? !!claimRewardResult.value
|
||||
: false;
|
||||
|
||||
return {
|
||||
embedType: getEmbedTypeToShow(functionSelectors),
|
||||
functionSelectors,
|
||||
@@ -93,5 +105,30 @@ export async function getContractPageMetadataSetup(
|
||||
isSplitSupported: contractType === "Split",
|
||||
isVoteContract: contractType === "VoteERC20",
|
||||
supportedERCs: supportedERCs(functionSelectors),
|
||||
showClaimRewards,
|
||||
};
|
||||
}
|
||||
|
||||
async function isClaimRewardsSupported(params: {
|
||||
assetContract: ThirdwebContract;
|
||||
}): Promise<boolean> {
|
||||
try {
|
||||
const entrypointContract = await getDeployedEntrypointERC20({
|
||||
chain: params.assetContract.chain,
|
||||
client: params.assetContract.client,
|
||||
});
|
||||
|
||||
if (!entrypointContract) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const reward = await getValidReward({
|
||||
assetContract: params.assetContract,
|
||||
entrypointContract,
|
||||
});
|
||||
|
||||
return !!reward;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
+6
@@ -140,6 +140,12 @@ export function getContractPageSidebarLinks(data: {
|
||||
href: `${layoutPrefix}/permissions`,
|
||||
label: "Permissions",
|
||||
},
|
||||
{
|
||||
exactMatch: true,
|
||||
hide: !data.metadata.showClaimRewards,
|
||||
href: `${layoutPrefix}/rewards`,
|
||||
label: "Rewards",
|
||||
},
|
||||
];
|
||||
|
||||
const extensionsToShow = extensionsLinks.filter((l) => !l.hide);
|
||||
|
||||
+131
-64
@@ -1,11 +1,12 @@
|
||||
"use client";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import {
|
||||
ArrowRightIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
ExternalLinkIcon,
|
||||
} from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { type ThirdwebClient, type ThirdwebContract, toTokens } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { WalletAddress } from "@/components/blocks/wallet-address";
|
||||
@@ -20,7 +21,6 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
type TokenTransfersData,
|
||||
useTokenTransfers,
|
||||
@@ -45,6 +45,32 @@ function RecentTransfersUI(props: {
|
||||
explorerUrl: string;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
const groupedData = useMemo(() => {
|
||||
const data: Array<{
|
||||
group: TokenTransfersData[];
|
||||
transactionHash: string;
|
||||
blockTimestamp: string;
|
||||
}> = [];
|
||||
|
||||
for (const transfer of props.data) {
|
||||
const existingGroup = data.find(
|
||||
(group) => group.transactionHash === transfer.transaction_hash,
|
||||
);
|
||||
|
||||
if (existingGroup) {
|
||||
existingGroup.group.push(transfer);
|
||||
} else {
|
||||
data.push({
|
||||
group: [transfer],
|
||||
transactionHash: transfer.transaction_hash,
|
||||
blockTimestamp: transfer.block_timestamp,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}, [props.data]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="p-4 lg:p-6 bg-card border rounded-b-none border-b-0 rounded-lg">
|
||||
@@ -74,76 +100,89 @@ function RecentTransfersUI(props: {
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: EXPECTED
|
||||
<SkeletonRow key={index} />
|
||||
))
|
||||
: props.data.map((transfer) => (
|
||||
: groupedData.map((group) => (
|
||||
<TableRow
|
||||
className="fade-in-0 animate-in duration-300"
|
||||
key={
|
||||
transfer.transaction_hash +
|
||||
transfer.amount +
|
||||
transfer.block_number +
|
||||
transfer.from_address
|
||||
}
|
||||
key={group.transactionHash}
|
||||
>
|
||||
<TableCell className="text-sm">
|
||||
<WalletAddress
|
||||
address={transfer.from_address}
|
||||
client={props.client}
|
||||
/>
|
||||
{/* From */}
|
||||
<TableCell className="relative space-y-1">
|
||||
{group.group.map((transfer) => (
|
||||
<div
|
||||
className="h-10 flex items-center gap-6 w-[150px]"
|
||||
key={transfer.log_index}
|
||||
>
|
||||
<WalletAddress
|
||||
address={transfer.from_address}
|
||||
client={props.client}
|
||||
iconClassName="size-4.5"
|
||||
/>
|
||||
<ArrowRightIcon className="size-4 text-muted-foreground/50 absolute -right-1 lg:right-3" />
|
||||
</div>
|
||||
))}
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">
|
||||
<WalletAddress
|
||||
address={transfer.to_address}
|
||||
client={props.client}
|
||||
/>
|
||||
|
||||
{/* To */}
|
||||
<TableCell className="relative space-y-1">
|
||||
{group.group.map((transfer) => (
|
||||
<div
|
||||
className="h-10 flex items-center gap-6 w-[150px]"
|
||||
key={transfer.log_index}
|
||||
>
|
||||
<WalletAddress
|
||||
address={transfer.to_address}
|
||||
client={props.client}
|
||||
key={transfer.log_index}
|
||||
iconClassName="size-4.5"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</TableCell>
|
||||
<TableCell className="text-sm ">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span>
|
||||
{tokenAmountFormatter.format(
|
||||
Number(
|
||||
toTokens(
|
||||
BigInt(transfer.amount),
|
||||
props.tokenMetadata.decimals,
|
||||
),
|
||||
),
|
||||
)}
|
||||
</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{props.tokenMetadata.symbol}
|
||||
</span>
|
||||
|
||||
{/* Amount */}
|
||||
<TableCell className="space-y-1">
|
||||
{group.group.map((transfer) => (
|
||||
<div
|
||||
className="h-10 flex items-center"
|
||||
key={transfer.log_index}
|
||||
>
|
||||
<TokenAmount
|
||||
amount={transfer.amount}
|
||||
decimals={props.tokenMetadata.decimals}
|
||||
symbol={props.tokenMetadata.symbol}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</TableCell>
|
||||
|
||||
{/* timestamp */}
|
||||
<TableCell>
|
||||
<div
|
||||
key={group.blockTimestamp}
|
||||
className="capitalize text-muted-foreground text-sm"
|
||||
>
|
||||
{timestamp(group.blockTimestamp)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">
|
||||
{formatDistanceToNow(
|
||||
new Date(
|
||||
transfer.block_timestamp.endsWith("Z")
|
||||
? transfer.block_timestamp
|
||||
: `${transfer.block_timestamp}Z`,
|
||||
),
|
||||
{
|
||||
addSuffix: true,
|
||||
},
|
||||
)}
|
||||
</TableCell>
|
||||
|
||||
{/* transaction */}
|
||||
<TableCell>
|
||||
<Button
|
||||
asChild
|
||||
className="h-8 w-8 p-0"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
>
|
||||
<a
|
||||
className={cn(
|
||||
"flex items-center justify-center",
|
||||
"hover:bg-accent hover:text-accent-foreground",
|
||||
)}
|
||||
href={`${props.explorerUrl}/tx/${transfer.transaction_hash}`}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
<div className="flex items-center justify-center">
|
||||
<Button
|
||||
asChild
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="text-muted-foreground hover:text-foreground rounded-full size-9 p-0 flex items-center justify-center"
|
||||
>
|
||||
<ExternalLinkIcon className="h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
<a
|
||||
href={`${props.explorerUrl}/tx/${group.transactionHash}`}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<ExternalLinkIcon className="size-3.5" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
@@ -183,6 +222,34 @@ function RecentTransfersUI(props: {
|
||||
);
|
||||
}
|
||||
|
||||
function timestamp(block_timestamp: string) {
|
||||
return formatDistanceToNow(
|
||||
new Date(
|
||||
block_timestamp.endsWith("Z") ? block_timestamp : `${block_timestamp}Z`,
|
||||
),
|
||||
{
|
||||
addSuffix: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function TokenAmount(props: {
|
||||
amount: string;
|
||||
decimals: number;
|
||||
symbol: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span>
|
||||
{tokenAmountFormatter.format(
|
||||
Number(toTokens(BigInt(props.amount), props.decimals)),
|
||||
)}
|
||||
</span>
|
||||
<span className="text-muted-foreground text-xs">{props.symbol}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SkeletonRow() {
|
||||
return (
|
||||
<TableRow className="fade-in-0 h-[73px] animate-in duration-300">
|
||||
@@ -199,7 +266,7 @@ function SkeletonRow() {
|
||||
<Skeleton className="h-6 w-32" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="h-6 w-6" />
|
||||
<Skeleton className="size-9 rounded-full" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
+1
-4
@@ -47,7 +47,6 @@ import {
|
||||
DEFAULT_FEE_BPS,
|
||||
DEFAULT_FEE_BPS_NEW,
|
||||
DEFAULT_FEE_RECIPIENT,
|
||||
THIRDWEB_PUBLISHER_ADDRESS,
|
||||
} from "@/constants/addresses";
|
||||
import { LAST_USED_PROJECT_ID, LAST_USED_TEAM_ID } from "@/constants/cookie";
|
||||
import { ZERO_FEE_CHAINS } from "@/constants/fee-config";
|
||||
@@ -218,9 +217,7 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
|
||||
const defaultFeeRecipientFunction = metadata.abi.find(
|
||||
(a) => a.type === "function" && a.name === "DEFAULT_FEE_RECIPIENT",
|
||||
);
|
||||
const hasInbuiltDefaultFeeConfig =
|
||||
defaultFeeRecipientFunction &&
|
||||
metadata.publisher === THIRDWEB_PUBLISHER_ADDRESS;
|
||||
const hasInbuiltDefaultFeeConfig = defaultFeeRecipientFunction;
|
||||
|
||||
const isFeeExempt =
|
||||
walletChain?.id && ZERO_FEE_CHAINS.includes(walletChain.id);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import "../../global.css";
|
||||
import "@workspace/ui/global.css";
|
||||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import NextTopLoader from "nextjs-toploader";
|
||||
|
||||
@@ -76,9 +76,6 @@ export function TeamHighlightsCard({
|
||||
: "activeUsers"
|
||||
}
|
||||
aggregateFn={(_data, key) => {
|
||||
if (key === "activeUsers") {
|
||||
return Math.max(...timeSeriesData.map((d) => d[key]));
|
||||
}
|
||||
return timeSeriesData.reduce((acc, curr) => acc + curr[key], 0);
|
||||
}}
|
||||
chartConfig={chartConfig}
|
||||
|
||||
+95
-16
@@ -1,11 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { useQueryState } from "nuqs";
|
||||
import { useMemo, useTransition } from "react";
|
||||
import { useMemo, useState, useTransition } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { getChainInfraCheckoutURL } from "@/actions/billing";
|
||||
import { reportChainInfraRpcOmissionAgreed } from "@/analytics/report";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { InsightIcon } from "@/icons/InsightIcon";
|
||||
import { RPCIcon } from "@/icons/RPCIcon";
|
||||
@@ -44,7 +53,7 @@ const SERVICE_CONFIG = {
|
||||
icon: "RPCIcon",
|
||||
label: "RPC",
|
||||
monthlyPrice: 1500,
|
||||
required: true,
|
||||
required: false,
|
||||
sku: "chain:infra:rpc" as const,
|
||||
},
|
||||
} satisfies Record<
|
||||
@@ -80,19 +89,26 @@ export function DeployInfrastructureForm(props: {
|
||||
searchParams.addons.withOptions({ history: "replace", startTransition }),
|
||||
);
|
||||
|
||||
const [rpcParam, setRpcParam] = useQueryState(
|
||||
"rpc",
|
||||
searchParams.rpc.withOptions({ history: "replace", startTransition }),
|
||||
);
|
||||
|
||||
const addons = useMemo(() => {
|
||||
return addonsStr ? addonsStr.split(",").filter(Boolean) : [];
|
||||
}, [addonsStr]);
|
||||
|
||||
const includeInsight = addons.includes("insight");
|
||||
const includeAA = addons.includes("aa");
|
||||
const includeRPC = rpcParam === "on";
|
||||
|
||||
const selectedOrder = useMemo(() => {
|
||||
const arr: (keyof typeof SERVICE_CONFIG)[] = ["rpc"];
|
||||
const arr: (keyof typeof SERVICE_CONFIG)[] = [];
|
||||
if (includeRPC) arr.push("rpc");
|
||||
if (includeInsight) arr.push("insight");
|
||||
if (includeAA) arr.push("accountAbstraction");
|
||||
return arr;
|
||||
}, [includeInsight, includeAA]);
|
||||
}, [includeInsight, includeAA, includeRPC]);
|
||||
|
||||
// NEW: count selected services and prepare bundle discount hint
|
||||
const selectedCount = selectedOrder.length;
|
||||
@@ -112,9 +128,9 @@ export function DeployInfrastructureForm(props: {
|
||||
return {
|
||||
accountAbstraction: includeAA,
|
||||
insight: includeInsight,
|
||||
rpc: true,
|
||||
rpc: includeRPC,
|
||||
} as const;
|
||||
}, [includeInsight, includeAA]);
|
||||
}, [includeInsight, includeAA, includeRPC]);
|
||||
|
||||
const pricePerService = useMemo(() => {
|
||||
const isAnnual = frequency === "annual";
|
||||
@@ -172,10 +188,13 @@ export function DeployInfrastructureForm(props: {
|
||||
|
||||
const chainId = props.chain.chainId;
|
||||
|
||||
const checkout = () => {
|
||||
const [showRpcWarning, setShowRpcWarning] = useState(false);
|
||||
|
||||
const proceedToCheckout = () => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
const skus: ChainInfraSKU[] = [SERVICE_CONFIG.rpc.sku];
|
||||
const skus: ChainInfraSKU[] = [];
|
||||
if (includeRPC) skus.push(SERVICE_CONFIG.rpc.sku);
|
||||
if (includeInsight) skus.push(SERVICE_CONFIG.insight.sku);
|
||||
if (includeAA) skus.push(SERVICE_CONFIG.accountAbstraction.sku);
|
||||
|
||||
@@ -186,11 +205,9 @@ export function DeployInfrastructureForm(props: {
|
||||
teamSlug: props.teamSlug,
|
||||
});
|
||||
|
||||
// If the action returns, it means redirect did not happen and we have an error
|
||||
if (res.status === "error") {
|
||||
toast.error(res.error);
|
||||
} else if (res.status === "success") {
|
||||
// replace the current page with the checkout page (which will then redirect back to us)
|
||||
window.location.href = res.data;
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -202,6 +219,15 @@ export function DeployInfrastructureForm(props: {
|
||||
});
|
||||
};
|
||||
|
||||
const checkout = () => {
|
||||
const hasAddons = includeInsight || includeAA;
|
||||
if (!includeRPC && hasAddons) {
|
||||
setShowRpcWarning(true);
|
||||
return;
|
||||
}
|
||||
proceedToCheckout();
|
||||
};
|
||||
|
||||
const periodLabel = frequency === "annual" ? "/yr" : "/mo";
|
||||
const isAnnual = frequency === "annual";
|
||||
|
||||
@@ -211,21 +237,22 @@ export function DeployInfrastructureForm(props: {
|
||||
<div className="flex flex-col gap-4">
|
||||
<h3 className="text-lg font-semibold">Select Services</h3>
|
||||
|
||||
{/* Required service */}
|
||||
{/* RPC (now optional) */}
|
||||
<div className="flex flex-col gap-2 mb-6">
|
||||
<ServiceCard
|
||||
description={SERVICE_CONFIG.rpc.description}
|
||||
disabled
|
||||
icon={SERVICE_CONFIG.rpc.icon}
|
||||
label={SERVICE_CONFIG.rpc.label}
|
||||
onToggle={() => {}}
|
||||
onToggle={() => {
|
||||
const newVal = !includeRPC;
|
||||
setRpcParam(newVal ? "on" : "off");
|
||||
}}
|
||||
originalPrice={
|
||||
isAnnual ? SERVICE_CONFIG.rpc.monthlyPrice * 12 : undefined
|
||||
}
|
||||
periodLabel={periodLabel}
|
||||
price={pricePerService.rpc}
|
||||
required
|
||||
selected
|
||||
selected={includeRPC}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -350,7 +377,11 @@ export function DeployInfrastructureForm(props: {
|
||||
|
||||
<Button
|
||||
className="w-full"
|
||||
disabled={isTransitionPending || !props.isOwner}
|
||||
disabled={
|
||||
isTransitionPending ||
|
||||
!props.isOwner ||
|
||||
selectedOrder.length === 0
|
||||
}
|
||||
onClick={checkout}
|
||||
>
|
||||
Proceed to Checkout
|
||||
@@ -362,6 +393,54 @@ export function DeployInfrastructureForm(props: {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* RPC Omission Warning Modal */}
|
||||
<Dialog open={showRpcWarning} onOpenChange={setShowRpcWarning}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Proceed without RPC (not recommended)</DialogTitle>
|
||||
<DialogDescription className="space-y-3">
|
||||
<p>
|
||||
RPC powers core functionality used by <strong>Insight</strong>{" "}
|
||||
and <strong>Account Abstraction</strong>.
|
||||
</p>
|
||||
<div className="space-y-1">
|
||||
<p>Without RPC, you may experience:</p>
|
||||
<ul className="list-disc pl-5 space-y-1">
|
||||
<li>Delayed or missing data in Insight</li>
|
||||
<li>
|
||||
Transaction failures or degraded reliability for Account
|
||||
Abstraction
|
||||
</li>
|
||||
<li>Limited or unsupported features across both services</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>
|
||||
thirdweb <strong>cannot guarantee</strong> that Insight or
|
||||
Account Abstraction will work as expected without RPC. To ensure
|
||||
reliability, keep RPC enabled.
|
||||
</p>
|
||||
<p>If you still want to continue, confirm below.</p>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter className="gap-2">
|
||||
<Button
|
||||
onClick={() => {
|
||||
reportChainInfraRpcOmissionAgreed({
|
||||
chainId,
|
||||
frequency: frequency === "annual" ? "annual" : "monthly",
|
||||
includeInsight,
|
||||
includeAccountAbstraction: includeAA,
|
||||
});
|
||||
setShowRpcWarning(false);
|
||||
proceedToCheckout();
|
||||
}}
|
||||
variant="destructive"
|
||||
>
|
||||
I understand — proceed without RPC
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+1
@@ -3,4 +3,5 @@ import { parseAsString, parseAsStringEnum } from "nuqs/server";
|
||||
export const searchParams = {
|
||||
addons: parseAsString.withDefault(""),
|
||||
freq: parseAsStringEnum(["monthly", "annual"]).withDefault("monthly"),
|
||||
rpc: parseAsStringEnum(["on", "off"]).withDefault("on"),
|
||||
};
|
||||
|
||||
+1
-1
@@ -1,8 +1,8 @@
|
||||
import { DialogDescription } from "@radix-ui/react-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
|
||||
-3
@@ -70,9 +70,6 @@ export function ProjectHighlightsCard(props: {
|
||||
: "activeUsers"
|
||||
}
|
||||
aggregateFn={(_data, key) => {
|
||||
if (key === "activeUsers") {
|
||||
return Math.max(...timeSeriesData.map((d) => d[key]));
|
||||
}
|
||||
return timeSeriesData.reduce((acc, curr) => acc + curr[key], 0);
|
||||
}}
|
||||
chartConfig={chartConfig}
|
||||
|
||||
+15
-1
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { CodeIcon, WebhookIcon } from "lucide-react";
|
||||
import { ArrowRightLeftIcon, CodeIcon, WebhookIcon } from "lucide-react";
|
||||
import { FeatureCard } from "./FeatureCard.client";
|
||||
|
||||
export function AdvancedSection({
|
||||
@@ -45,6 +45,20 @@ export function AdvancedSection({
|
||||
label: "Setup Webhooks",
|
||||
}}
|
||||
/>
|
||||
<FeatureCard
|
||||
title="Swap Tokens"
|
||||
icon={ArrowRightLeftIcon}
|
||||
setupTime={5}
|
||||
color="green"
|
||||
id="swap_tokens"
|
||||
features={["Swap any token", "Cross-chain swaps"]}
|
||||
description="Swap tokens cross-chain with dedicated swapping endpoints."
|
||||
link={{
|
||||
href: `https://portal.thirdweb.com/payments/swap`,
|
||||
label: "Setup Swaps",
|
||||
target: "_blank",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { isProd } from "@/constants/env-utils";
|
||||
|
||||
export async function createTokenOnUniversalBridge(params: {
|
||||
chainId: number;
|
||||
tokenAddress: string;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
const domain = isProd ? "thirdweb.com" : "thirdweb-dev.com";
|
||||
const res = await fetch(`https://bridge.${domain}/v1/tokens`, {
|
||||
body: JSON.stringify({
|
||||
chainId: params.chainId.toString(),
|
||||
tokenAddress: params.tokenAddress,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": params.client.clientId,
|
||||
},
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
+1
-1
@@ -34,7 +34,7 @@ export function SocialUrlsFieldset<T extends WithSocialUrls>(props: {
|
||||
<h2 className="mb-2 font-medium text-sm">Social URLs</h2>
|
||||
|
||||
{fields.length > 0 && (
|
||||
<div className="mb-4 space-y-3">
|
||||
<div className="mb-4 space-y-4">
|
||||
{fields.map((field, index) => (
|
||||
<div
|
||||
className="flex gap-3 max-sm:mb-6 max-sm:border-b max-sm:border-dashed max-sm:pb-6"
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import type { ThirdwebClient } from "thirdweb/dist/types/client/client";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { useAllChainsData } from "@/hooks/chains/allChains";
|
||||
import { ChainIconClient } from "@/icons/ChainIcon";
|
||||
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ export function StepCard(props: {
|
||||
{props.children}
|
||||
|
||||
{(props.prevButton || props.nextButton) && (
|
||||
<div className="flex justify-end gap-3 border-t p-6">
|
||||
<div className="flex justify-end gap-3 border-t p-4 md:p-6">
|
||||
{props.prevButton && (
|
||||
<Button
|
||||
className="gap-2"
|
||||
|
||||
+2
-1
@@ -9,6 +9,7 @@ import { FormFieldSetup } from "@/components/blocks/FormFieldSetup";
|
||||
import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { AdminAddressesFieldset } from "../../_common/admin-addresses-fieldset";
|
||||
import { SocialUrlsFieldset } from "../../_common/SocialUrls";
|
||||
@@ -95,7 +96,7 @@ export function NFTCollectionInfoFieldset(props: {
|
||||
isRequired
|
||||
label="Chain"
|
||||
>
|
||||
<ClientOnly ssr={null}>
|
||||
<ClientOnly ssr={<Skeleton className="h-10" />}>
|
||||
<SingleNetworkSelector
|
||||
chainId={Number(form.watch("chain"))}
|
||||
className="bg-background"
|
||||
|
||||
+53
-21
@@ -1,6 +1,7 @@
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import * as z from "zod";
|
||||
import { addressSchema, socialUrlsSchema } from "../../_common/schema";
|
||||
import { getInitialTickValue, isValidTickValue } from "../utils/calculate-tick";
|
||||
|
||||
export const tokenInfoFormSchema = z.object({
|
||||
chain: z.string().min(1, "Chain is required"),
|
||||
@@ -15,38 +16,69 @@ export const tokenInfoFormSchema = z.object({
|
||||
.max(10, "Symbol must be 10 characters or less"),
|
||||
});
|
||||
|
||||
const priceAmountSchema = z.string().refine(
|
||||
(value) => {
|
||||
const number = Number(value);
|
||||
return !Number.isNaN(number) && number >= 0;
|
||||
},
|
||||
{
|
||||
message: "Amount must be number larger than or equal to 0",
|
||||
},
|
||||
);
|
||||
|
||||
export const tokenDistributionFormSchema = z.object({
|
||||
// airdrop
|
||||
airdropAddresses: z.array(
|
||||
z.object({
|
||||
address: addressSchema,
|
||||
quantity: z.string(),
|
||||
}),
|
||||
),
|
||||
// UI states
|
||||
airdropEnabled: z.boolean(),
|
||||
saleAllocationPercentage: z.string().refine(
|
||||
(value) => {
|
||||
const number = Number(value);
|
||||
if (Number.isNaN(number)) {
|
||||
// sales ---
|
||||
erc20Asset_poolMode: z.object({
|
||||
startingPricePerToken: priceAmountSchema.refine((value) => {
|
||||
const numValue = Number(value);
|
||||
if (numValue === 0) {
|
||||
return false;
|
||||
}
|
||||
return number >= 0 && number <= 100;
|
||||
},
|
||||
{
|
||||
message: "Must be a number between 0 and 100",
|
||||
},
|
||||
),
|
||||
const tick = getInitialTickValue({
|
||||
startingPricePerToken: Number(value),
|
||||
});
|
||||
|
||||
return isValidTickValue(tick);
|
||||
}, "Invalid price"),
|
||||
saleAllocationPercentage: z.string().refine(
|
||||
(value) => {
|
||||
const number = Number(value);
|
||||
if (Number.isNaN(number)) {
|
||||
return false;
|
||||
}
|
||||
return number >= 0 && number <= 100;
|
||||
},
|
||||
{
|
||||
message: "Must be a number between 0 and 100",
|
||||
},
|
||||
),
|
||||
}),
|
||||
dropERC20Mode: z.object({
|
||||
pricePerToken: priceAmountSchema,
|
||||
saleTokenAddress: addressSchema,
|
||||
saleAllocationPercentage: z.string().refine(
|
||||
(value) => {
|
||||
const number = Number(value);
|
||||
if (Number.isNaN(number)) {
|
||||
return false;
|
||||
}
|
||||
return number >= 0 && number <= 100;
|
||||
},
|
||||
{
|
||||
message: "Must be a number between 0 and 100",
|
||||
},
|
||||
),
|
||||
}),
|
||||
saleEnabled: z.boolean(),
|
||||
salePrice: z.string().refine(
|
||||
(value) => {
|
||||
const number = Number(value);
|
||||
return !Number.isNaN(number) && number >= 0;
|
||||
},
|
||||
{
|
||||
message: "Must be number larger than or equal to 0",
|
||||
},
|
||||
),
|
||||
saleTokenAddress: z.string(),
|
||||
saleMode: z.enum(["erc20-asset:pool", "drop-erc20:token-drop"]),
|
||||
supply: z.string().min(1, "Supply is required"),
|
||||
});
|
||||
|
||||
|
||||
+246
-147
@@ -8,34 +8,37 @@ import {
|
||||
sendAndConfirmTransaction,
|
||||
type ThirdwebClient,
|
||||
toUnits,
|
||||
toWei,
|
||||
} from "thirdweb";
|
||||
import { deployERC20Contract } from "thirdweb/deploys";
|
||||
import type { ClaimConditionsInput } from "thirdweb/dist/types/utils/extensions/drops/types";
|
||||
import {
|
||||
approve,
|
||||
claimTo,
|
||||
getActiveClaimCondition,
|
||||
setClaimConditions as setClaimConditionsExtension,
|
||||
transferBatch,
|
||||
} from "thirdweb/extensions/erc20";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import {
|
||||
createToken,
|
||||
distributeToken,
|
||||
getDeployedEntrypointERC20,
|
||||
} from "thirdweb/tokens";
|
||||
import type { ClaimConditionsInput } from "thirdweb/utils";
|
||||
import { create7702MinimalAccount } from "thirdweb/wallets/smart";
|
||||
import { revalidatePathAction } from "@/actions/revalidate";
|
||||
import {
|
||||
reportAssetCreationFailed,
|
||||
reportContractDeployed,
|
||||
} from "@/analytics/report";
|
||||
import { reportContractDeployed } from "@/analytics/report";
|
||||
import type { Team } from "@/api/team/get-team";
|
||||
import {
|
||||
DEFAULT_FEE_BPS_NEW,
|
||||
DEFAULT_FEE_RECIPIENT,
|
||||
} from "@/constants/addresses";
|
||||
import { useAllChainsData } from "@/hooks/chains/allChains";
|
||||
import { useAddContractToProject } from "@/hooks/project-contracts";
|
||||
import { defineDashboardChain } from "@/lib/defineDashboardChain";
|
||||
import { parseError } from "@/utils/errorParser";
|
||||
import { pollWithTimeout } from "@/utils/pollWithTimeout";
|
||||
import { createTokenOnUniversalBridge } from "../_apis/create-token-on-bridge";
|
||||
import type { CreateAssetFormValues } from "./_common/form";
|
||||
import { CreateTokenAssetPageUI } from "./create-token-page.client";
|
||||
import { getInitialTickValue } from "./utils/calculate-tick";
|
||||
|
||||
export function CreateTokenAssetPage(props: {
|
||||
accountAddress: string;
|
||||
@@ -47,7 +50,6 @@ export function CreateTokenAssetPage(props: {
|
||||
teamPlan: Team["billingPlan"];
|
||||
}) {
|
||||
const activeAccount = useActiveAccount();
|
||||
const { idToChain } = useAllChainsData();
|
||||
const addContractToProject = useAddContractToProject();
|
||||
const contractAddressRef = useRef<string | undefined>(undefined);
|
||||
|
||||
@@ -83,7 +85,149 @@ export function CreateTokenAssetPage(props: {
|
||||
});
|
||||
}
|
||||
|
||||
async function deployContract(params: {
|
||||
// ERC20Asset ---
|
||||
|
||||
async function Erc20Asset_deployContract(params: {
|
||||
values: CreateAssetFormValues;
|
||||
gasless: boolean;
|
||||
}) {
|
||||
const account = getAccount(params.gasless);
|
||||
|
||||
const socialUrls = params.values.socialUrls.reduce(
|
||||
(acc, url) => {
|
||||
if (url.url && url.platform) {
|
||||
acc[url.platform] = url.url;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>,
|
||||
);
|
||||
|
||||
const salePercent = params.values.saleEnabled
|
||||
? Number(params.values.erc20Asset_poolMode.saleAllocationPercentage)
|
||||
: 0;
|
||||
|
||||
const saleAmount = Math.floor(
|
||||
Number(params.values.supply) * (salePercent / 100),
|
||||
);
|
||||
|
||||
const contractAddress = await createToken({
|
||||
account,
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
chain: defineChain(Number(params.values.chain)),
|
||||
client: props.client,
|
||||
launchConfig:
|
||||
params.values.saleEnabled && saleAmount !== 0
|
||||
? {
|
||||
kind: "pool",
|
||||
config: {
|
||||
amount: BigInt(saleAmount),
|
||||
initialTick: getInitialTickValue({
|
||||
startingPricePerToken: Number(
|
||||
params.values.erc20Asset_poolMode.startingPricePerToken,
|
||||
),
|
||||
}),
|
||||
developerRewardBps: 1250, // 12.5%
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
params: {
|
||||
description: params.values.description,
|
||||
image: params.values.image,
|
||||
maxSupply: BigInt(params.values.supply),
|
||||
name: params.values.name,
|
||||
social_urls: socialUrls,
|
||||
symbol: params.values.symbol,
|
||||
},
|
||||
developerAddress: "0x1Af20C6B23373350aD464700B5965CE4B0D2aD94",
|
||||
});
|
||||
|
||||
// add contract to project in background
|
||||
addContractToProject.mutateAsync({
|
||||
chainId: params.values.chain,
|
||||
contractAddress: contractAddress,
|
||||
contractType: "ERC20Asset",
|
||||
deploymentType: "asset",
|
||||
projectId: props.projectId,
|
||||
teamId: props.teamId,
|
||||
});
|
||||
|
||||
reportContractDeployed({
|
||||
address: contractAddress,
|
||||
chainId: Number(params.values.chain),
|
||||
contractName: "DropERC20",
|
||||
deploymentType: "asset",
|
||||
publisher: account.address,
|
||||
});
|
||||
|
||||
contractAddressRef.current = contractAddress;
|
||||
|
||||
return {
|
||||
contractAddress: contractAddress,
|
||||
};
|
||||
}
|
||||
|
||||
async function ERC20Asset_airdropTokens(params: {
|
||||
values: CreateAssetFormValues;
|
||||
gasless: boolean;
|
||||
}) {
|
||||
const { values, gasless } = params;
|
||||
const account = getAccount(gasless);
|
||||
const contract = getDeployedContract({ chain: values.chain });
|
||||
|
||||
const airdropTx = await distributeToken({
|
||||
chain: contract.chain,
|
||||
client: props.client,
|
||||
contents: values.airdropAddresses.map((recipient) => ({
|
||||
amount: recipient.quantity,
|
||||
recipient: recipient.address,
|
||||
})),
|
||||
tokenAddress: contract.address,
|
||||
});
|
||||
|
||||
await sendAndConfirmTransaction({
|
||||
account,
|
||||
transaction: airdropTx,
|
||||
});
|
||||
}
|
||||
|
||||
async function ERC20Asset_approveAirdropTokens(params: {
|
||||
values: CreateAssetFormValues;
|
||||
gasless: boolean;
|
||||
}) {
|
||||
const { values, gasless } = params;
|
||||
const account = getAccount(gasless);
|
||||
const contract = getDeployedContract({ chain: values.chain });
|
||||
|
||||
const totalAmountToAirdrop = values.airdropAddresses.reduce(
|
||||
(acc, recipient) => acc + Number(recipient.quantity),
|
||||
0,
|
||||
);
|
||||
|
||||
const entrypoint = await getDeployedEntrypointERC20({
|
||||
chain: contract.chain,
|
||||
client: props.client,
|
||||
});
|
||||
|
||||
if (!entrypoint) {
|
||||
throw new Error("Entrypoint not found");
|
||||
}
|
||||
|
||||
const approvalTx = approve({
|
||||
amountWei: toWei(totalAmountToAirdrop.toString()),
|
||||
contract: contract,
|
||||
spender: entrypoint.address,
|
||||
});
|
||||
|
||||
await sendAndConfirmTransaction({
|
||||
account,
|
||||
transaction: approvalTx,
|
||||
});
|
||||
}
|
||||
|
||||
// DropERC20 ----
|
||||
|
||||
async function DropERC20_deployContract(params: {
|
||||
values: CreateAssetFormValues;
|
||||
gasless: boolean;
|
||||
}) {
|
||||
@@ -101,73 +245,53 @@ export function CreateTokenAssetPage(props: {
|
||||
{} as Record<string, string>,
|
||||
);
|
||||
|
||||
try {
|
||||
const contractAddress = await deployERC20Contract({
|
||||
account,
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
chain: defineDashboardChain(
|
||||
Number(values.chain),
|
||||
idToChain.get(Number(values.chain)),
|
||||
),
|
||||
client: props.client,
|
||||
params: {
|
||||
description: values.description,
|
||||
image: values.image,
|
||||
// metadata
|
||||
name: values.name,
|
||||
// platform fees
|
||||
platformFeeBps: BigInt(DEFAULT_FEE_BPS_NEW),
|
||||
platformFeeRecipient: DEFAULT_FEE_RECIPIENT,
|
||||
// primary sale
|
||||
saleRecipient: account.address,
|
||||
social_urls: socialUrls,
|
||||
symbol: values.symbol,
|
||||
},
|
||||
type: "DropERC20",
|
||||
});
|
||||
const contractAddress = await deployERC20Contract({
|
||||
account,
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
chain: defineChain(Number(values.chain)),
|
||||
client: props.client,
|
||||
params: {
|
||||
description: values.description,
|
||||
image: values.image,
|
||||
// metadata
|
||||
name: values.name,
|
||||
// platform fees
|
||||
platformFeeBps: BigInt(DEFAULT_FEE_BPS_NEW),
|
||||
platformFeeRecipient: DEFAULT_FEE_RECIPIENT,
|
||||
// primary sale
|
||||
saleRecipient: account.address,
|
||||
social_urls: socialUrls,
|
||||
symbol: values.symbol,
|
||||
},
|
||||
type: "DropERC20",
|
||||
});
|
||||
|
||||
contractAddressRef.current = contractAddress;
|
||||
contractAddressRef.current = contractAddress;
|
||||
|
||||
// add contract to project in background
|
||||
addContractToProject.mutateAsync({
|
||||
chainId: values.chain,
|
||||
contractAddress: contractAddress,
|
||||
contractType: "DropERC20",
|
||||
deploymentType: "asset",
|
||||
projectId: props.projectId,
|
||||
teamId: props.teamId,
|
||||
});
|
||||
// add contract to project in background
|
||||
addContractToProject.mutateAsync({
|
||||
chainId: values.chain,
|
||||
contractAddress: contractAddress,
|
||||
contractType: "DropERC20",
|
||||
deploymentType: "asset",
|
||||
projectId: props.projectId,
|
||||
teamId: props.teamId,
|
||||
});
|
||||
|
||||
reportContractDeployed({
|
||||
address: contractAddress,
|
||||
chainId: Number(values.chain),
|
||||
contractName: "DropERC20",
|
||||
deploymentType: "asset",
|
||||
publisher: "deployer.thirdweb.eth",
|
||||
});
|
||||
reportContractDeployed({
|
||||
address: contractAddress,
|
||||
chainId: Number(values.chain),
|
||||
contractName: "DropERC20",
|
||||
deploymentType: "asset",
|
||||
publisher: "deployer.thirdweb.eth",
|
||||
});
|
||||
|
||||
return {
|
||||
contractAddress: contractAddress,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
const parsedError = parseError(e);
|
||||
const errorMessage =
|
||||
typeof parsedError === "string" ? parsedError : "Unknown error";
|
||||
|
||||
reportAssetCreationFailed({
|
||||
assetType: "coin",
|
||||
contractType: "DropERC20",
|
||||
error: errorMessage,
|
||||
step: "deploy-contract",
|
||||
});
|
||||
|
||||
console.error(errorMessage);
|
||||
throw e;
|
||||
}
|
||||
return {
|
||||
contractAddress: contractAddress,
|
||||
};
|
||||
}
|
||||
|
||||
async function airdropTokens(params: {
|
||||
async function DropERC20_airdropTokens(params: {
|
||||
values: CreateAssetFormValues;
|
||||
gasless: boolean;
|
||||
}) {
|
||||
@@ -183,34 +307,21 @@ export function CreateTokenAssetPage(props: {
|
||||
throw new Error("No connected account");
|
||||
}
|
||||
|
||||
try {
|
||||
const airdropTx = transferBatch({
|
||||
batch: values.airdropAddresses.map((recipient) => ({
|
||||
amount: recipient.quantity,
|
||||
to: recipient.address,
|
||||
})),
|
||||
contract,
|
||||
});
|
||||
const airdropTx = transferBatch({
|
||||
batch: values.airdropAddresses.map((recipient) => ({
|
||||
amount: recipient.quantity,
|
||||
to: recipient.address,
|
||||
})),
|
||||
contract,
|
||||
});
|
||||
|
||||
await sendAndConfirmTransaction({
|
||||
account,
|
||||
transaction: airdropTx,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
const errorMessage = parseError(e);
|
||||
|
||||
reportAssetCreationFailed({
|
||||
assetType: "coin",
|
||||
contractType: "DropERC20",
|
||||
error: errorMessage,
|
||||
step: "airdrop-tokens",
|
||||
});
|
||||
throw e;
|
||||
}
|
||||
await sendAndConfirmTransaction({
|
||||
account,
|
||||
transaction: airdropTx,
|
||||
});
|
||||
}
|
||||
|
||||
async function mintTokens(params: {
|
||||
async function DropERC20_mintTokens(params: {
|
||||
values: CreateAssetFormValues;
|
||||
gasless: boolean;
|
||||
}) {
|
||||
@@ -235,39 +346,25 @@ export function CreateTokenAssetPage(props: {
|
||||
|
||||
const totalSupply = Number(values.supply);
|
||||
const salePercent = values.saleEnabled
|
||||
? Number(values.saleAllocationPercentage)
|
||||
? Number(values.dropERC20Mode.saleAllocationPercentage)
|
||||
: 0;
|
||||
|
||||
const ownerAndAirdropPercent = 100 - salePercent;
|
||||
const ownerSupplyTokens = (totalSupply * ownerAndAirdropPercent) / 100;
|
||||
|
||||
try {
|
||||
const claimTx = claimTo({
|
||||
contract,
|
||||
quantity: ownerSupplyTokens.toString(),
|
||||
to: account.address,
|
||||
});
|
||||
const claimTx = claimTo({
|
||||
contract,
|
||||
quantity: ownerSupplyTokens.toString(),
|
||||
to: account.address,
|
||||
});
|
||||
|
||||
await sendAndConfirmTransaction({
|
||||
account,
|
||||
transaction: claimTx,
|
||||
});
|
||||
} catch (e) {
|
||||
const errorMessage = parseError(e);
|
||||
console.error(e);
|
||||
|
||||
reportAssetCreationFailed({
|
||||
assetType: "coin",
|
||||
contractType: "DropERC20",
|
||||
error: errorMessage,
|
||||
step: "mint-tokens",
|
||||
});
|
||||
|
||||
throw e;
|
||||
}
|
||||
await sendAndConfirmTransaction({
|
||||
account,
|
||||
transaction: claimTx,
|
||||
});
|
||||
}
|
||||
|
||||
async function setClaimConditions(params: {
|
||||
async function DropERC20_setClaimConditions(params: {
|
||||
values: CreateAssetFormValues;
|
||||
gasless: boolean;
|
||||
}) {
|
||||
@@ -279,7 +376,7 @@ export function CreateTokenAssetPage(props: {
|
||||
const account = getAccount(gasless);
|
||||
|
||||
const salePercent = values.saleEnabled
|
||||
? Number(values.saleAllocationPercentage)
|
||||
? Number(values.dropERC20Mode.saleAllocationPercentage)
|
||||
: 0;
|
||||
|
||||
const totalSupply = Number(values.supply);
|
||||
@@ -288,10 +385,10 @@ export function CreateTokenAssetPage(props: {
|
||||
const phases: ClaimConditionsInput[] = [
|
||||
{
|
||||
currencyAddress:
|
||||
getAddress(values.saleTokenAddress) ===
|
||||
getAddress(values.dropERC20Mode.saleTokenAddress) ===
|
||||
getAddress(NATIVE_TOKEN_ADDRESS)
|
||||
? undefined
|
||||
: values.saleTokenAddress,
|
||||
: values.dropERC20Mode.saleTokenAddress,
|
||||
maxClaimablePerWallet: values.saleEnabled ? undefined : 0n,
|
||||
maxClaimableSupply: totalSupplyWei,
|
||||
metadata: {
|
||||
@@ -307,7 +404,10 @@ export function CreateTokenAssetPage(props: {
|
||||
price: "0",
|
||||
},
|
||||
],
|
||||
price: values.saleEnabled && salePercent > 0 ? values.salePrice : "0",
|
||||
price:
|
||||
values.saleEnabled && salePercent > 0
|
||||
? values.dropERC20Mode.pricePerToken
|
||||
: "0",
|
||||
startTime: new Date(),
|
||||
},
|
||||
];
|
||||
@@ -317,23 +417,10 @@ export function CreateTokenAssetPage(props: {
|
||||
phases,
|
||||
});
|
||||
|
||||
try {
|
||||
await sendAndConfirmTransaction({
|
||||
account,
|
||||
transaction: preparedTx,
|
||||
});
|
||||
} catch (e) {
|
||||
const errorMessage = parseError(e);
|
||||
console.error(e);
|
||||
|
||||
reportAssetCreationFailed({
|
||||
assetType: "coin",
|
||||
contractType: "DropERC20",
|
||||
error: errorMessage,
|
||||
step: "set-claim-conditions",
|
||||
});
|
||||
throw e;
|
||||
}
|
||||
await sendAndConfirmTransaction({
|
||||
account,
|
||||
transaction: preparedTx,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -341,12 +428,24 @@ export function CreateTokenAssetPage(props: {
|
||||
accountAddress={props.accountAddress}
|
||||
client={props.client}
|
||||
createTokenFunctions={{
|
||||
airdropTokens,
|
||||
deployContract,
|
||||
mintTokens,
|
||||
setClaimConditions,
|
||||
ERC20Asset: {
|
||||
airdropTokens: ERC20Asset_airdropTokens,
|
||||
deployContract: Erc20Asset_deployContract,
|
||||
approveAirdropTokens: ERC20Asset_approveAirdropTokens,
|
||||
},
|
||||
DropERC20: {
|
||||
deployContract: DropERC20_deployContract,
|
||||
airdropTokens: DropERC20_airdropTokens,
|
||||
mintTokens: DropERC20_mintTokens,
|
||||
setClaimConditions: DropERC20_setClaimConditions,
|
||||
},
|
||||
}}
|
||||
onLaunchSuccess={() => {
|
||||
onLaunchSuccess={(params) => {
|
||||
createTokenOnUniversalBridge({
|
||||
chainId: params.chainId,
|
||||
client: props.client,
|
||||
tokenAddress: params.contractAddress,
|
||||
});
|
||||
revalidatePathAction(
|
||||
`/team/${props.teamSlug}/project/${props.projectId}/tokens`,
|
||||
"page",
|
||||
|
||||
+127
-40
@@ -1,16 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import {
|
||||
defineChain,
|
||||
getAddress,
|
||||
NATIVE_TOKEN_ADDRESS,
|
||||
type ThirdwebClient,
|
||||
} from "thirdweb";
|
||||
import { useActiveWalletChain } from "thirdweb/react";
|
||||
import {
|
||||
getDeployedContractFactory,
|
||||
isPoolRouterEnabled,
|
||||
} from "thirdweb/tokens";
|
||||
import { reportAssetCreationStepConfigured } from "@/analytics/report";
|
||||
import type { Team } from "@/api/team/get-team";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { StepCard } from "../_common/step-card";
|
||||
import {
|
||||
type CreateAssetFormValues,
|
||||
type TokenDistributionFormValues,
|
||||
@@ -28,21 +36,33 @@ type CreateTokenFunctionsParams = {
|
||||
};
|
||||
|
||||
export type CreateTokenFunctions = {
|
||||
deployContract: (params: CreateTokenFunctionsParams) => Promise<{
|
||||
contractAddress: string;
|
||||
}>;
|
||||
setClaimConditions: (params: CreateTokenFunctionsParams) => Promise<void>;
|
||||
mintTokens: (params: CreateTokenFunctionsParams) => Promise<void>;
|
||||
airdropTokens: (params: CreateTokenFunctionsParams) => Promise<void>;
|
||||
ERC20Asset: {
|
||||
deployContract: (params: CreateTokenFunctionsParams) => Promise<{
|
||||
contractAddress: string;
|
||||
}>;
|
||||
airdropTokens: (values: CreateTokenFunctionsParams) => Promise<void>;
|
||||
approveAirdropTokens: (values: CreateTokenFunctionsParams) => Promise<void>;
|
||||
};
|
||||
DropERC20: {
|
||||
deployContract: (params: CreateTokenFunctionsParams) => Promise<{
|
||||
contractAddress: string;
|
||||
}>;
|
||||
setClaimConditions: (params: CreateTokenFunctionsParams) => Promise<void>;
|
||||
mintTokens: (params: CreateTokenFunctionsParams) => Promise<void>;
|
||||
airdropTokens: (params: CreateTokenFunctionsParams) => Promise<void>;
|
||||
};
|
||||
};
|
||||
|
||||
const checksummedNativeTokenAddress = getAddress(NATIVE_TOKEN_ADDRESS);
|
||||
const nativeTokenAddress = getAddress(NATIVE_TOKEN_ADDRESS);
|
||||
|
||||
export function CreateTokenAssetPageUI(props: {
|
||||
accountAddress: string;
|
||||
client: ThirdwebClient;
|
||||
createTokenFunctions: CreateTokenFunctions;
|
||||
onLaunchSuccess: () => void;
|
||||
onLaunchSuccess: (params: {
|
||||
chainId: number;
|
||||
contractAddress: string;
|
||||
}) => void;
|
||||
teamSlug: string;
|
||||
projectSlug: string;
|
||||
teamPlan: Team["billingPlan"];
|
||||
@@ -50,11 +70,11 @@ export function CreateTokenAssetPageUI(props: {
|
||||
const [step, setStep] = useState<"token-info" | "distribution" | "launch">(
|
||||
"token-info",
|
||||
);
|
||||
const activeWalletChain = useActiveWalletChain();
|
||||
const activeChain = useActiveWalletChain();
|
||||
|
||||
const tokenInfoForm = useForm<TokenInfoFormValues>({
|
||||
defaultValues: {
|
||||
chain: activeWalletChain?.id.toString() || "1",
|
||||
chain: activeChain?.id.toString() || "8453",
|
||||
description: "",
|
||||
image: undefined,
|
||||
name: "",
|
||||
@@ -74,22 +94,70 @@ export function CreateTokenAssetPageUI(props: {
|
||||
reValidateMode: "onChange",
|
||||
});
|
||||
|
||||
const isERC20AssetSupportedQuery = useQuery({
|
||||
queryKey: ["is-erc20-asset-supported", tokenInfoForm.watch("chain")],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const res = await getDeployedContractFactory({
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
chain: defineChain(Number(tokenInfoForm.watch("chain"))),
|
||||
client: props.client,
|
||||
});
|
||||
return !!res;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const isAssetRouterEnabledQuery = useQuery({
|
||||
queryFn: async () => {
|
||||
try {
|
||||
return await isPoolRouterEnabled({
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
chain: defineChain(Number(tokenInfoForm.watch("chain"))),
|
||||
client: props.client,
|
||||
});
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
queryKey: ["is-asset-router-enabled", tokenInfoForm.watch("chain")],
|
||||
});
|
||||
|
||||
const defaultSaleMode = isERC20AssetSupportedQuery.data
|
||||
? "erc20-asset:pool"
|
||||
: "drop-erc20:token-drop";
|
||||
|
||||
const tokenDistributionForm = useForm<TokenDistributionFormValues>({
|
||||
resolver: zodResolver(tokenDistributionFormSchema),
|
||||
reValidateMode: "onChange",
|
||||
values: {
|
||||
airdropAddresses: [],
|
||||
// airdrop
|
||||
airdropEnabled: false,
|
||||
// sale fieldset
|
||||
saleAllocationPercentage: "0",
|
||||
saleEnabled: false,
|
||||
salePrice: "0.1",
|
||||
saleTokenAddress: checksummedNativeTokenAddress,
|
||||
supply: "1000000",
|
||||
erc20Asset_poolMode: {
|
||||
startingPricePerToken: "0.000000001", // 1gwei per token
|
||||
saleAllocationPercentage: "100",
|
||||
},
|
||||
dropERC20Mode: {
|
||||
pricePerToken: "0.1",
|
||||
saleAllocationPercentage: "100",
|
||||
saleTokenAddress: nativeTokenAddress,
|
||||
},
|
||||
supply: "1000000000", // 1 billion
|
||||
saleEnabled: !(
|
||||
defaultSaleMode === "erc20-asset:pool" &&
|
||||
!isAssetRouterEnabledQuery.data
|
||||
),
|
||||
saleMode: defaultSaleMode,
|
||||
},
|
||||
mode: "onChange",
|
||||
resolver: zodResolver(tokenDistributionFormSchema),
|
||||
reValidateMode: "onChange",
|
||||
});
|
||||
|
||||
const distributionFieldIsPending =
|
||||
isERC20AssetSupportedQuery.isPending || isAssetRouterEnabledQuery.isPending;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{step === "token-info" && (
|
||||
@@ -97,10 +165,10 @@ export function CreateTokenAssetPageUI(props: {
|
||||
client={props.client}
|
||||
form={tokenInfoForm}
|
||||
onChainUpdated={() => {
|
||||
// if the chain is updated, set the sale token address to the native token address
|
||||
// reset the token address to the native token address on chain change
|
||||
tokenDistributionForm.setValue(
|
||||
"saleTokenAddress",
|
||||
checksummedNativeTokenAddress,
|
||||
"dropERC20Mode.saleTokenAddress",
|
||||
nativeTokenAddress,
|
||||
);
|
||||
}}
|
||||
onNext={() => {
|
||||
@@ -113,25 +181,44 @@ export function CreateTokenAssetPageUI(props: {
|
||||
/>
|
||||
)}
|
||||
|
||||
{step === "distribution" && (
|
||||
<TokenDistributionFieldset
|
||||
accountAddress={props.accountAddress}
|
||||
chainId={tokenInfoForm.watch("chain")}
|
||||
client={props.client}
|
||||
form={tokenDistributionForm}
|
||||
onNext={() => {
|
||||
reportAssetCreationStepConfigured({
|
||||
assetType: "coin",
|
||||
step: "token-distribution",
|
||||
});
|
||||
setStep("launch");
|
||||
}}
|
||||
onPrevious={() => {
|
||||
setStep("token-info");
|
||||
}}
|
||||
tokenSymbol={tokenInfoForm.watch("symbol")}
|
||||
/>
|
||||
)}
|
||||
{step === "distribution" &&
|
||||
(distributionFieldIsPending ? (
|
||||
<StepCard
|
||||
nextButton={{
|
||||
disabled: true,
|
||||
type: "submit",
|
||||
}}
|
||||
prevButton={{
|
||||
onClick: () => {
|
||||
setStep("token-info");
|
||||
},
|
||||
}}
|
||||
title="Coin Distribution"
|
||||
>
|
||||
<div className="h-[400px] flex items-center justify-center">
|
||||
<Spinner className="size-10" />
|
||||
</div>
|
||||
</StepCard>
|
||||
) : (
|
||||
<TokenDistributionFieldset
|
||||
form={tokenDistributionForm}
|
||||
accountAddress={props.accountAddress}
|
||||
chainId={tokenInfoForm.watch("chain")}
|
||||
isRouterEnabled={isAssetRouterEnabledQuery.data === true}
|
||||
client={props.client}
|
||||
onNext={() => {
|
||||
reportAssetCreationStepConfigured({
|
||||
assetType: "coin",
|
||||
step: "token-distribution",
|
||||
});
|
||||
setStep("launch");
|
||||
}}
|
||||
onPrevious={() => {
|
||||
setStep("token-info");
|
||||
}}
|
||||
tokenSymbol={tokenInfoForm.watch("symbol")}
|
||||
/>
|
||||
))}
|
||||
|
||||
{step === "launch" && (
|
||||
<LaunchTokenStatus
|
||||
|
||||
+46
-23
@@ -1,7 +1,10 @@
|
||||
import type { Meta, StoryObj } from "@storybook/nextjs";
|
||||
import { ConnectButton, ThirdwebProvider } from "thirdweb/react";
|
||||
import { storybookThirdwebClient } from "@/storybook/utils";
|
||||
import { CreateTokenAssetPageUI } from "./create-token-page.client";
|
||||
import {
|
||||
CreateTokenAssetPageUI,
|
||||
type CreateTokenFunctions,
|
||||
} from "./create-token-page.client";
|
||||
|
||||
const meta = {
|
||||
component: CreateTokenAssetPageUI,
|
||||
@@ -9,9 +12,9 @@ const meta = {
|
||||
(Story) => (
|
||||
<ThirdwebProvider>
|
||||
<div className="container max-w-5xl py-10">
|
||||
<ConnectButton client={storybookThirdwebClient} />
|
||||
<div className="h-10" />
|
||||
<Story />
|
||||
<div className="h-10" />
|
||||
<ConnectButton client={storybookThirdwebClient} />
|
||||
</div>
|
||||
</ThirdwebProvider>
|
||||
),
|
||||
@@ -22,19 +25,33 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
const mockCreateTokenFunctions = {
|
||||
airdropTokens: async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
const mockCreateTokenFunctions: CreateTokenFunctions = {
|
||||
ERC20Asset: {
|
||||
airdropTokens: async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
},
|
||||
approveAirdropTokens: async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
},
|
||||
deployContract: async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
return { contractAddress: "0x123" };
|
||||
},
|
||||
},
|
||||
deployContract: async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
return { contractAddress: "0x123" };
|
||||
},
|
||||
mintTokens: async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
},
|
||||
setClaimConditions: async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
DropERC20: {
|
||||
airdropTokens: async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
},
|
||||
deployContract: async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
return { contractAddress: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" };
|
||||
},
|
||||
setClaimConditions: async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
},
|
||||
mintTokens: async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -56,9 +73,12 @@ export const ErrorOnDeploy: Story = {
|
||||
client: storybookThirdwebClient,
|
||||
createTokenFunctions: {
|
||||
...mockCreateTokenFunctions,
|
||||
deployContract: async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
throw new Error("Failed to deploy contract");
|
||||
ERC20Asset: {
|
||||
...mockCreateTokenFunctions.ERC20Asset,
|
||||
deployContract: async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
throw new Error("Failed to deploy contract");
|
||||
},
|
||||
},
|
||||
},
|
||||
onLaunchSuccess: () => {},
|
||||
@@ -74,11 +94,14 @@ export const StorageErrorOnDeploy: Story = {
|
||||
client: storybookThirdwebClient,
|
||||
createTokenFunctions: {
|
||||
...mockCreateTokenFunctions,
|
||||
deployContract: async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
throw new Error(
|
||||
"You have reached your storage limit. Please add a valid payment method to continue using the service.",
|
||||
);
|
||||
ERC20Asset: {
|
||||
...mockCreateTokenFunctions.ERC20Asset,
|
||||
deployContract: async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
throw new Error(
|
||||
"You have reached your storage limit. Please add a valid payment method to continue using the service.",
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
onLaunchSuccess: () => {},
|
||||
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
"use client";
|
||||
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { DistributionBarChart } from "@/components/blocks/distribution-chart";
|
||||
import { FormFieldSetup } from "@/components/blocks/FormFieldSetup";
|
||||
import { TokenSelector } from "@/components/blocks/TokenSelector";
|
||||
import { DynamicHeight } from "@/components/ui/DynamicHeight";
|
||||
import { DecimalInput } from "@/components/ui/decimal-input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import type { TokenDistributionForm } from "../_common/form";
|
||||
|
||||
export function DropERC20_TokenSaleSection(props: {
|
||||
form: TokenDistributionForm;
|
||||
chainId: string;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
const totalSupply = Number(props.form.watch("supply"));
|
||||
const sellSupply = Math.floor(
|
||||
(totalSupply *
|
||||
Number(props.form.watch("dropERC20Mode.saleAllocationPercentage"))) /
|
||||
100,
|
||||
);
|
||||
|
||||
const isEnabled = props.form.watch("saleEnabled");
|
||||
|
||||
const protocolFee = 2;
|
||||
const convenienceFee = 0.5;
|
||||
const recipientFee = 100 - protocolFee - convenienceFee;
|
||||
|
||||
return (
|
||||
<DynamicHeight>
|
||||
<div className="relative border-t border-dashed p-6">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 className="font-semibold text-xl tracking-tight">Sale</h2>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Make your coin available for purchase by setting a price
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Switch
|
||||
checked={isEnabled}
|
||||
onCheckedChange={(checked) => {
|
||||
props.form.setValue("saleEnabled", checked);
|
||||
if (!checked) {
|
||||
props.form.setValue(
|
||||
"dropERC20Mode.saleAllocationPercentage",
|
||||
"0",
|
||||
);
|
||||
props.form.setValue("dropERC20Mode.pricePerToken", "0");
|
||||
} else if (!props.form.getValues("airdropEnabled")) {
|
||||
props.form.setValue(
|
||||
"dropERC20Mode.saleAllocationPercentage",
|
||||
"100",
|
||||
{
|
||||
shouldValidate: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isEnabled && (
|
||||
<div>
|
||||
<div className="mt-4 flex flex-col lg:flex-row gap-5 lg:max-w-2xl fade-in-0 duration-300 animate-in">
|
||||
<FormFieldSetup
|
||||
errorMessage={
|
||||
props.form.formState.errors.dropERC20Mode
|
||||
?.saleAllocationPercentage?.message
|
||||
}
|
||||
helperText={`${sellSupply} tokens`}
|
||||
isRequired
|
||||
label="Sell % of Total Supply"
|
||||
>
|
||||
<div className="relative lg:w-64">
|
||||
<DecimalInput
|
||||
maxValue={100}
|
||||
onChange={(value) => {
|
||||
props.form.setValue(
|
||||
"dropERC20Mode.saleAllocationPercentage",
|
||||
value,
|
||||
);
|
||||
}}
|
||||
value={props.form.watch(
|
||||
"dropERC20Mode.saleAllocationPercentage",
|
||||
)}
|
||||
/>
|
||||
<span className="-translate-y-1/2 absolute top-1/2 right-3 text-muted-foreground text-sm">
|
||||
%
|
||||
</span>
|
||||
</div>
|
||||
</FormFieldSetup>
|
||||
|
||||
<FormFieldSetup
|
||||
className="grow"
|
||||
errorMessage={
|
||||
props.form.formState.errors.dropERC20Mode?.pricePerToken
|
||||
?.message ||
|
||||
props.form.formState.errors.dropERC20Mode?.saleTokenAddress
|
||||
?.message
|
||||
}
|
||||
isRequired
|
||||
label="Price per Token"
|
||||
>
|
||||
<div className="relative flex items-center">
|
||||
<DecimalInput
|
||||
onChange={(value) => {
|
||||
props.form.setValue("dropERC20Mode.pricePerToken", value);
|
||||
}}
|
||||
value={props.form.watch("dropERC20Mode.pricePerToken")}
|
||||
className="rounded-r-none w-42"
|
||||
/>
|
||||
|
||||
<TokenSelector
|
||||
addNativeTokenIfMissing={true}
|
||||
chainId={Number(props.chainId)}
|
||||
className="bg-background border-l-0 rounded-l-none"
|
||||
client={props.client}
|
||||
disableAddress={true}
|
||||
popoverContentClassName="!w-[320px]"
|
||||
onChange={(value) => {
|
||||
props.form.setValue(
|
||||
"dropERC20Mode.saleTokenAddress",
|
||||
value.address,
|
||||
);
|
||||
}}
|
||||
selectedToken={{
|
||||
address: props.form.watch(
|
||||
"dropERC20Mode.saleTokenAddress",
|
||||
),
|
||||
chainId: Number(props.chainId),
|
||||
}}
|
||||
showCheck={true}
|
||||
/>
|
||||
</div>
|
||||
</FormFieldSetup>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 relative border-t border-dashed pt-5">
|
||||
<p className="text-muted-foreground text-sm mb-3">
|
||||
All primary sales are subjected to{" "}
|
||||
<em className="text-foreground font-medium not-italic">2.5%</em>{" "}
|
||||
fee
|
||||
</p>
|
||||
|
||||
<div className="relative">
|
||||
<DistributionBarChart
|
||||
segments={[
|
||||
{
|
||||
label: "Your Wallet",
|
||||
percent: recipientFee,
|
||||
color: "hsl(var(--chart-1))",
|
||||
value: `${recipientFee}%`,
|
||||
},
|
||||
{
|
||||
label: "Protocol",
|
||||
percent: protocolFee,
|
||||
color: "hsl(var(--chart-2))",
|
||||
value: `${protocolFee}%`,
|
||||
},
|
||||
{
|
||||
label: "Convenience Fee",
|
||||
percent: convenienceFee,
|
||||
color: "hsl(var(--chart-3))",
|
||||
value: `${convenienceFee}%`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DynamicHeight>
|
||||
);
|
||||
}
|
||||
+4
-4
@@ -66,10 +66,10 @@ export function TokenAirdropSection(props: {
|
||||
|
||||
return (
|
||||
<DynamicHeight>
|
||||
<div className="relative border-t border-dashed">
|
||||
<div className="flex items-center justify-between gap-3 px-6 py-5">
|
||||
<div className="relative border-t border-dashed px-4 py-8 md:px-6">
|
||||
<div className="flex items-center justify-between gap-3 ">
|
||||
<div>
|
||||
<h2 className="font-semibold text-lg">Airdrop</h2>
|
||||
<h2 className="font-semibold text-xl tracking-tight">Airdrop</h2>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Airdrop tokens to a list of addresses with each address receiving
|
||||
a specific quantity
|
||||
@@ -88,7 +88,7 @@ export function TokenAirdropSection(props: {
|
||||
</div>
|
||||
|
||||
{isEnabled && (
|
||||
<div className="flex justify-start px-6 pb-6">
|
||||
<div className="flex justify-start mt-4 fade-in-0 duration-300 animate-in">
|
||||
{airdropAddresses.length > 0 ? (
|
||||
<div className="flex w-full flex-col gap-4 rounded-lg border bg-background p-4 md:flex-row lg:items-center lg:justify-between">
|
||||
{/* left */}
|
||||
|
||||
+50
-24
@@ -14,21 +14,28 @@ import type {
|
||||
TokenDistributionForm,
|
||||
TokenDistributionFormValues,
|
||||
} from "../_common/form";
|
||||
import { DropERC20_TokenSaleSection } from "./drop-erc20-token-sale";
|
||||
import { TokenAirdropSection } from "./token-airdrop";
|
||||
import { TokenSaleSection } from "./token-sale";
|
||||
|
||||
const compactNumberFormatter = new Intl.NumberFormat("en-US", {
|
||||
maximumFractionDigits: 10,
|
||||
notation: "compact",
|
||||
});
|
||||
|
||||
export function TokenDistributionFieldset(props: {
|
||||
accountAddress: string;
|
||||
onNext: () => void;
|
||||
onPrevious: () => void;
|
||||
form: TokenDistributionForm;
|
||||
chainId: string;
|
||||
form: TokenDistributionForm;
|
||||
client: ThirdwebClient;
|
||||
tokenSymbol: string | undefined;
|
||||
isRouterEnabled: boolean;
|
||||
}) {
|
||||
const { form } = props;
|
||||
const distributionError = getDistributionError(form);
|
||||
|
||||
const distributionError = getDistributionError(form);
|
||||
const supplyId = useId();
|
||||
|
||||
return (
|
||||
@@ -45,40 +52,51 @@ export function TokenDistributionFieldset(props: {
|
||||
title="Coin Distribution"
|
||||
>
|
||||
<div>
|
||||
<div className="space-y-6 p-4 md:px-6 md:py-6">
|
||||
<div className="p-4 md:px-6 md:py-6">
|
||||
<FormFieldSetup
|
||||
errorMessage={form.formState.errors.supply?.message}
|
||||
htmlFor={supplyId}
|
||||
helperText={`${compactNumberFormatter.format(Number(form.watch("supply")))} tokens`}
|
||||
isRequired
|
||||
label="Total Supply"
|
||||
>
|
||||
<div className="relative">
|
||||
<div className="relative max-w-96">
|
||||
<Input id={supplyId} {...form.register("supply")} />
|
||||
<span className="-translate-y-1/2 absolute top-1/2 right-3 text-muted-foreground text-sm">
|
||||
{props.tokenSymbol || "Tokens"}
|
||||
</span>
|
||||
</div>
|
||||
</FormFieldSetup>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<TokenDistributionBarChart
|
||||
distributionFormValues={form.watch()}
|
||||
/>
|
||||
|
||||
{distributionError && (
|
||||
<div className="text-destructive-text text-sm">
|
||||
{distributionError}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{form.watch("saleMode") === "drop-erc20:token-drop" ? (
|
||||
<DropERC20_TokenSaleSection
|
||||
chainId={props.chainId}
|
||||
client={props.client}
|
||||
form={form}
|
||||
/>
|
||||
) : (
|
||||
<TokenSaleSection
|
||||
chainId={props.chainId}
|
||||
isRouterEnabled={props.isRouterEnabled}
|
||||
client={props.client}
|
||||
form={form}
|
||||
/>
|
||||
)}
|
||||
|
||||
<TokenAirdropSection client={props.client} form={form} />
|
||||
<TokenSaleSection
|
||||
chainId={props.chainId}
|
||||
client={props.client}
|
||||
form={form}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-3 p-4 py-8 md:px-6 border-t border-dashed">
|
||||
<TokenDistributionBarChart
|
||||
distributionFormValues={form.watch()}
|
||||
/>
|
||||
|
||||
{distributionError && (
|
||||
<div className="text-destructive-text text-sm">
|
||||
{distributionError}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</StepCard>
|
||||
</form>
|
||||
@@ -97,10 +115,12 @@ function getDistributionError(form: TokenDistributionForm) {
|
||||
}
|
||||
|
||||
const saleSupplyPercentage = SafeNumber(
|
||||
form.watch("saleAllocationPercentage"),
|
||||
form.watch("saleMode") === "erc20-asset:pool"
|
||||
? form.watch("erc20Asset_poolMode.saleAllocationPercentage")
|
||||
: form.watch("dropERC20Mode.saleAllocationPercentage"),
|
||||
);
|
||||
|
||||
const saleSupply = (saleSupplyPercentage / 100) * supply;
|
||||
const saleSupply = Math.round((saleSupplyPercentage / 100) * supply);
|
||||
const ownerSupply = Math.max(supply - totalAirdrop - saleSupply, 0);
|
||||
const totalSumOfSupply = totalAirdrop + saleSupply + ownerSupply;
|
||||
|
||||
@@ -127,7 +147,10 @@ export function TokenDistributionBarChart(props: {
|
||||
);
|
||||
const airdropPercentage = (totalAirdropSupply / totalSupply) * 100;
|
||||
const salePercentage = Number(
|
||||
props.distributionFormValues.saleAllocationPercentage,
|
||||
props.distributionFormValues.saleMode === "erc20-asset:pool"
|
||||
? props.distributionFormValues.erc20Asset_poolMode
|
||||
.saleAllocationPercentage
|
||||
: props.distributionFormValues.dropERC20Mode.saleAllocationPercentage,
|
||||
);
|
||||
|
||||
const ownerPercentage = Math.max(100 - airdropPercentage - salePercentage, 0);
|
||||
@@ -137,16 +160,19 @@ export function TokenDistributionBarChart(props: {
|
||||
color: "hsl(var(--chart-1))",
|
||||
label: "Owner",
|
||||
percent: ownerPercentage,
|
||||
value: `${ownerPercentage}%`,
|
||||
},
|
||||
{
|
||||
color: "hsl(var(--chart-3))",
|
||||
label: "Airdrop",
|
||||
percent: airdropPercentage,
|
||||
value: `${airdropPercentage}%`,
|
||||
},
|
||||
{
|
||||
color: "hsl(var(--chart-4))",
|
||||
label: "Sale",
|
||||
percent: salePercentage,
|
||||
value: `${salePercentage}%`,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
+231
-82
@@ -1,110 +1,139 @@
|
||||
"use client";
|
||||
|
||||
import { DollarSignIcon, XIcon } from "lucide-react";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { DistributionBarChart } from "@/components/blocks/distribution-chart";
|
||||
import { FormFieldSetup } from "@/components/blocks/FormFieldSetup";
|
||||
import { TokenSelector } from "@/components/blocks/TokenSelector";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { DynamicHeight } from "@/components/ui/DynamicHeight";
|
||||
import { DecimalInput } from "@/components/ui/decimal-input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { useAllChainsData } from "@/hooks/chains/allChains";
|
||||
import type { TokenDistributionForm } from "../_common/form";
|
||||
|
||||
export function TokenSaleSection(props: {
|
||||
form: TokenDistributionForm;
|
||||
chainId: string;
|
||||
client: ThirdwebClient;
|
||||
isRouterEnabled: boolean;
|
||||
}) {
|
||||
const totalSupply = Number(props.form.watch("supply"));
|
||||
const sellSupply = Math.floor(
|
||||
(totalSupply * Number(props.form.watch("saleAllocationPercentage"))) / 100,
|
||||
);
|
||||
const { idToChain } = useAllChainsData();
|
||||
const chainMeta = idToChain.get(Number(props.chainId));
|
||||
const isSaleEnabled = props.form.watch("saleEnabled");
|
||||
const protocolFee = 20;
|
||||
const leftOverFee = 100 - protocolFee;
|
||||
const convenienceFee = (12.5 * leftOverFee) / 100;
|
||||
const deployerFee = leftOverFee - convenienceFee;
|
||||
|
||||
const isEnabled = props.form.watch("saleEnabled");
|
||||
return (
|
||||
<DynamicHeight>
|
||||
<div className="relative border-t border-dashed p-6">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 className="font-semibold text-lg">Sale</h2>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Make your coin available for purchase by setting a price
|
||||
</p>
|
||||
<div className="relative border-t border-dashed px-4 md:px-6 py-8">
|
||||
<div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 className="font-semibold text-xl tracking-tight">Sale</h2>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
List your coin on a decentralized exchange and earn rewards on
|
||||
every trade
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Switch
|
||||
checked={isSaleEnabled}
|
||||
disabled={!props.isRouterEnabled}
|
||||
onCheckedChange={(checked) => {
|
||||
if (!props.isRouterEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
props.form.setValue("saleEnabled", checked);
|
||||
|
||||
if (checked && !props.form.getValues("airdropEnabled")) {
|
||||
props.form.setValue(
|
||||
"erc20Asset_poolMode.saleAllocationPercentage",
|
||||
"100",
|
||||
{
|
||||
shouldValidate: true,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
props.form.setValue(
|
||||
"erc20Asset_poolMode.saleAllocationPercentage",
|
||||
"0",
|
||||
{
|
||||
shouldValidate: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Switch
|
||||
checked={isEnabled}
|
||||
onCheckedChange={(checked) => {
|
||||
props.form.setValue("saleEnabled", checked);
|
||||
if (!checked) {
|
||||
props.form.setValue("saleAllocationPercentage", "0");
|
||||
props.form.setValue("salePrice", "0");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{props.isRouterEnabled === false && (
|
||||
<div className="mt-4 flex items-center gap-1.5 fade-in-0 duration-300 animate-in">
|
||||
<div className="rounded-full border bg-background p-1">
|
||||
<XIcon className="size-3 text-muted-foreground" />
|
||||
</div>
|
||||
<h2 className="text-muted-foreground text-sm">
|
||||
Not Available on {chainMeta?.name || props.chainId}
|
||||
</h2>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isEnabled && (
|
||||
<div className="mt-4 flex flex-col gap-6">
|
||||
<FormFieldSetup
|
||||
errorMessage={
|
||||
props.form.formState.errors.saleAllocationPercentage?.message
|
||||
}
|
||||
helperText={`${sellSupply} tokens`}
|
||||
isRequired
|
||||
label="Sell % of Total Supply"
|
||||
>
|
||||
<div className="relative">
|
||||
<DecimalInput
|
||||
maxValue={100}
|
||||
onChange={(value) => {
|
||||
props.form.setValue("saleAllocationPercentage", value);
|
||||
}}
|
||||
value={props.form.watch("saleAllocationPercentage")}
|
||||
/>
|
||||
<span className="-translate-y-1/2 absolute top-1/2 right-3 text-muted-foreground text-sm">
|
||||
%
|
||||
</span>
|
||||
</div>
|
||||
</FormFieldSetup>
|
||||
{isSaleEnabled && props.isRouterEnabled === true && (
|
||||
<div className="pt-4 fade-in-0 duration-300 animate-in">
|
||||
<PoolConfig
|
||||
chainId={props.chainId}
|
||||
client={props.client}
|
||||
form={props.form}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<FormFieldSetup
|
||||
errorMessage={props.form.formState.errors.salePrice?.message}
|
||||
isRequired
|
||||
label="Price per Token"
|
||||
>
|
||||
<div className="relative">
|
||||
<DecimalInput
|
||||
onChange={(value) => {
|
||||
props.form.setValue("salePrice", value);
|
||||
}}
|
||||
value={props.form.watch("salePrice")}
|
||||
/>
|
||||
<div className="mt-5 border p-4 bg-background rounded-lg relative">
|
||||
<div className="flex mb-4">
|
||||
<div className="p-2 rounded-full border bg-card">
|
||||
<DollarSignIcon className="size-5 text-muted-foreground" />
|
||||
</div>
|
||||
</FormFieldSetup>
|
||||
</div>
|
||||
<h3 className="font-medium text-base mt-2"> Sale Rewards </h3>
|
||||
<p className="text-muted-foreground text-sm mb-4">
|
||||
All trades on the market are subjected to{" "}
|
||||
<em className="text-foreground font-medium not-italic"> 1% </em>{" "}
|
||||
fee distributed as:
|
||||
</p>
|
||||
|
||||
<FormFieldSetup
|
||||
errorMessage={
|
||||
props.form.formState.errors.saleTokenAddress?.message
|
||||
}
|
||||
isRequired
|
||||
label="Currency"
|
||||
>
|
||||
<TokenSelector
|
||||
addNativeTokenIfMissing={true}
|
||||
chainId={Number(props.chainId)}
|
||||
className="bg-background"
|
||||
client={props.client}
|
||||
disableAddress={true}
|
||||
onChange={(value) => {
|
||||
props.form.setValue("saleTokenAddress", value.address);
|
||||
}}
|
||||
selectedToken={{
|
||||
address: props.form.watch("saleTokenAddress"),
|
||||
chainId: Number(props.chainId),
|
||||
}}
|
||||
showCheck={true}
|
||||
<div className="relative">
|
||||
<div className="absolute -top-6 right-0 text-sm">Total: 1%</div>
|
||||
<DistributionBarChart
|
||||
segments={[
|
||||
{
|
||||
label: "Your Wallet",
|
||||
percent: deployerFee,
|
||||
color: "hsl(var(--chart-1))",
|
||||
value: `${deployerFee / 100}%`,
|
||||
},
|
||||
{
|
||||
label: "Protocol",
|
||||
percent: protocolFee,
|
||||
color: "hsl(var(--chart-2))",
|
||||
value: `${protocolFee / 100}%`,
|
||||
},
|
||||
{
|
||||
label: "Convenience fee",
|
||||
percent: convenienceFee,
|
||||
color: "hsl(var(--chart-3))",
|
||||
value: `${convenienceFee / 100}%`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</FormFieldSetup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -112,3 +141,123 @@ export function TokenSaleSection(props: {
|
||||
</DynamicHeight>
|
||||
);
|
||||
}
|
||||
|
||||
function PoolConfig(props: {
|
||||
form: TokenDistributionForm;
|
||||
chainId: string;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
const { idToChain } = useAllChainsData();
|
||||
const chainMeta = idToChain.get(Number(props.chainId));
|
||||
|
||||
const totalSupply = Number(props.form.watch("supply"));
|
||||
const sellSupply = Math.floor(
|
||||
(totalSupply *
|
||||
Number(
|
||||
props.form.watch("erc20Asset_poolMode.saleAllocationPercentage"),
|
||||
)) /
|
||||
100,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Pricing Strategy */}
|
||||
<FormFieldSetup
|
||||
isRequired
|
||||
label="Pricing Strategy"
|
||||
errorMessage={undefined}
|
||||
helperText="Bonding Curve pricing is a static formula for pricing tokens based on supply and demand"
|
||||
>
|
||||
<Select value="bonding-curve">
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="bonding-curve">Bonding Curve</SelectItem>
|
||||
<SelectItem value="fixed-price" disabled>
|
||||
<span className="flex items-center gap-2">
|
||||
Dynamic Bonding Curve{" "}
|
||||
<Badge variant="secondary">Coming Soon</Badge>
|
||||
</span>
|
||||
</SelectItem>
|
||||
<SelectItem value="dutch-auction" disabled>
|
||||
<span className="flex items-center gap-2">
|
||||
Fixed Price <Badge variant="secondary">Coming Soon</Badge>
|
||||
</span>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormFieldSetup>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 lg:max-w-[536px]">
|
||||
{/* supply % */}
|
||||
<FormFieldSetup
|
||||
errorMessage={
|
||||
props.form.formState.errors.erc20Asset_poolMode
|
||||
?.saleAllocationPercentage?.message
|
||||
}
|
||||
helperText={`${compactNumberFormatter.format(sellSupply)} tokens`}
|
||||
isRequired
|
||||
label="Sell % of Total Supply"
|
||||
>
|
||||
<div className="relative">
|
||||
<DecimalInput
|
||||
maxValue={100}
|
||||
onChange={(value) => {
|
||||
props.form.setValue(
|
||||
"erc20Asset_poolMode.saleAllocationPercentage",
|
||||
value,
|
||||
{
|
||||
shouldValidate: true,
|
||||
},
|
||||
);
|
||||
}}
|
||||
value={props.form.watch(
|
||||
"erc20Asset_poolMode.saleAllocationPercentage",
|
||||
)}
|
||||
/>
|
||||
<span className="-translate-y-1/2 absolute top-1/2 right-3 text-sm text-muted-foreground">
|
||||
%
|
||||
</span>
|
||||
</div>
|
||||
</FormFieldSetup>
|
||||
|
||||
{/* starting price */}
|
||||
<FormFieldSetup
|
||||
errorMessage={
|
||||
props.form.formState.errors.erc20Asset_poolMode
|
||||
?.startingPricePerToken?.message
|
||||
}
|
||||
isRequired
|
||||
label="Starting price per token"
|
||||
>
|
||||
<div className="relative">
|
||||
<DecimalInput
|
||||
className="pr-10"
|
||||
onChange={(value) => {
|
||||
props.form.setValue(
|
||||
"erc20Asset_poolMode.startingPricePerToken",
|
||||
value,
|
||||
{
|
||||
shouldValidate: true,
|
||||
},
|
||||
);
|
||||
}}
|
||||
value={props.form.watch(
|
||||
"erc20Asset_poolMode.startingPricePerToken",
|
||||
)}
|
||||
/>
|
||||
<span className="-translate-y-1/2 absolute top-1/2 right-3 text-sm text-muted-foreground">
|
||||
{chainMeta?.nativeCurrency.symbol || "ETH"}
|
||||
</span>
|
||||
</div>
|
||||
</FormFieldSetup>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const compactNumberFormatter = new Intl.NumberFormat("en-US", {
|
||||
maximumFractionDigits: 10,
|
||||
notation: "compact",
|
||||
});
|
||||
|
||||
+114
-48
@@ -38,10 +38,15 @@ import type { CreateTokenFunctions } from "../create-token-page.client";
|
||||
import { TokenDistributionBarChart } from "../distribution/token-distribution";
|
||||
|
||||
const stepIds = {
|
||||
"airdrop-tokens": "airdrop-tokens",
|
||||
"deploy-contract": "deploy-contract",
|
||||
"mint-tokens": "mint-tokens",
|
||||
"set-claim-conditions": "set-claim-conditions",
|
||||
// asset ---
|
||||
"erc20-asset:airdrop-tokens": "erc20-asset:airdrop-tokens",
|
||||
"erc20-asset:approve-airdrop-tokens": "erc20-asset:approve-airdrop-tokens",
|
||||
"erc20-asset:deploy-contract": "erc20-asset:deploy-contract",
|
||||
// fallback ---
|
||||
"drop-erc20:deploy-contract": "drop-erc20:deploy-contract",
|
||||
"drop-erc20:set-claim-conditions": "drop-erc20:set-claim-conditions",
|
||||
"drop-erc20:mint-tokens": "drop-erc20:mint-tokens",
|
||||
"drop-erc20:airdrop-tokens": "drop-erc20:airdrop-tokens",
|
||||
} as const;
|
||||
|
||||
type StepId = keyof typeof stepIds;
|
||||
@@ -51,7 +56,10 @@ export function LaunchTokenStatus(props: {
|
||||
values: CreateAssetFormValues;
|
||||
onPrevious: () => void;
|
||||
client: ThirdwebClient;
|
||||
onLaunchSuccess: () => void;
|
||||
onLaunchSuccess: (params: {
|
||||
chainId: number;
|
||||
contractAddress: string;
|
||||
}) => void;
|
||||
teamSlug: string;
|
||||
projectSlug: string;
|
||||
teamPlan: Team["billingPlan"];
|
||||
@@ -60,7 +68,7 @@ export function LaunchTokenStatus(props: {
|
||||
const { createTokenFunctions } = props;
|
||||
const [steps, setSteps] = useState<MultiStepState<StepId>[]>([]);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [contractLink, setContractLink] = useState<string | null>(null);
|
||||
const [contractAddress, setContractAddress] = useState<string | null>(null);
|
||||
const activeWallet = useActiveWallet();
|
||||
const walletRequiresApproval = activeWallet?.id !== "inApp";
|
||||
|
||||
@@ -85,35 +93,67 @@ export function LaunchTokenStatus(props: {
|
||||
}
|
||||
|
||||
async function handleSubmitClick() {
|
||||
const initialSteps: MultiStepState<StepId>[] = [
|
||||
{
|
||||
id: stepIds["deploy-contract"],
|
||||
label: "Deploy contract",
|
||||
status: { type: "idle" },
|
||||
},
|
||||
{
|
||||
id: stepIds["set-claim-conditions"],
|
||||
label: "Set claim conditions",
|
||||
status: { type: "idle" },
|
||||
},
|
||||
{
|
||||
id: stepIds["mint-tokens"],
|
||||
label: "Mint tokens",
|
||||
status: { type: "idle" },
|
||||
},
|
||||
];
|
||||
if (formValues.saleMode === "erc20-asset:pool") {
|
||||
const initialSteps: MultiStepState<StepId>[] = [
|
||||
{
|
||||
id: stepIds["erc20-asset:deploy-contract"],
|
||||
label: "Deploy contract",
|
||||
status: { type: "idle" },
|
||||
},
|
||||
];
|
||||
|
||||
if (formValues.airdropEnabled && formValues.airdropAddresses.length > 0) {
|
||||
initialSteps.push({
|
||||
id: stepIds["airdrop-tokens"],
|
||||
label: "Airdrop tokens",
|
||||
status: { type: "idle" },
|
||||
});
|
||||
if (formValues.airdropEnabled && formValues.airdropAddresses.length > 0) {
|
||||
initialSteps.push({
|
||||
id: stepIds["erc20-asset:approve-airdrop-tokens"],
|
||||
label: "Approve spending tokens for airdrop",
|
||||
status: { type: "idle" },
|
||||
});
|
||||
|
||||
initialSteps.push({
|
||||
id: stepIds["erc20-asset:airdrop-tokens"],
|
||||
label: "Airdrop tokens",
|
||||
status: { type: "idle" },
|
||||
});
|
||||
}
|
||||
|
||||
setSteps(initialSteps);
|
||||
setIsModalOpen(true);
|
||||
executeSteps(initialSteps, 0, isGasless);
|
||||
} else {
|
||||
const initialSteps: MultiStepState<StepId>[] = [
|
||||
{
|
||||
id: stepIds["drop-erc20:deploy-contract"],
|
||||
label: "Deploy contract",
|
||||
status: { type: "idle" },
|
||||
},
|
||||
{
|
||||
id: stepIds["drop-erc20:set-claim-conditions"],
|
||||
label: "Set claim conditions",
|
||||
status: { type: "idle" },
|
||||
},
|
||||
];
|
||||
|
||||
// if user is selling 100% of the tokens, owner share is 0% - so skip minting
|
||||
if (Number(formValues.dropERC20Mode.saleAllocationPercentage) !== 100) {
|
||||
initialSteps.push({
|
||||
id: stepIds["drop-erc20:mint-tokens"],
|
||||
label: "Mint tokens",
|
||||
status: { type: "idle" },
|
||||
});
|
||||
}
|
||||
|
||||
if (formValues.airdropEnabled && formValues.airdropAddresses.length > 0) {
|
||||
initialSteps.push({
|
||||
id: stepIds["drop-erc20:airdrop-tokens"],
|
||||
label: "Airdrop tokens",
|
||||
status: { type: "idle" },
|
||||
});
|
||||
}
|
||||
|
||||
setSteps(initialSteps);
|
||||
setIsModalOpen(true);
|
||||
executeSteps(initialSteps, 0, isGasless);
|
||||
}
|
||||
|
||||
setSteps(initialSteps);
|
||||
setIsModalOpen(true);
|
||||
executeSteps(initialSteps, 0, isGasless);
|
||||
}
|
||||
|
||||
const isComplete = steps.every((step) => step.status.type === "completed");
|
||||
@@ -125,17 +165,28 @@ export function LaunchTokenStatus(props: {
|
||||
values: formValues,
|
||||
};
|
||||
|
||||
if (stepId === "deploy-contract") {
|
||||
const result = await createTokenFunctions.deployContract(params);
|
||||
setContractLink(
|
||||
`/team/${props.teamSlug}/${props.projectSlug}/contract/${formValues.chain}/${result.contractAddress}`,
|
||||
);
|
||||
} else if (stepId === "set-claim-conditions") {
|
||||
await createTokenFunctions.setClaimConditions(params);
|
||||
} else if (stepId === "mint-tokens") {
|
||||
await createTokenFunctions.mintTokens(params);
|
||||
} else if (stepId === "airdrop-tokens") {
|
||||
await createTokenFunctions.airdropTokens(params);
|
||||
// erc20-asset
|
||||
if (stepId === "erc20-asset:deploy-contract") {
|
||||
const result =
|
||||
await createTokenFunctions.ERC20Asset.deployContract(params);
|
||||
setContractAddress(result.contractAddress);
|
||||
} else if (stepId === "erc20-asset:airdrop-tokens") {
|
||||
await createTokenFunctions.ERC20Asset.airdropTokens(params);
|
||||
} else if (stepId === "erc20-asset:approve-airdrop-tokens") {
|
||||
await createTokenFunctions.ERC20Asset.approveAirdropTokens(params);
|
||||
}
|
||||
|
||||
// drop-erc20
|
||||
else if (stepId === "drop-erc20:deploy-contract") {
|
||||
const result =
|
||||
await createTokenFunctions.DropERC20.deployContract(params);
|
||||
setContractAddress(result.contractAddress);
|
||||
} else if (stepId === "drop-erc20:set-claim-conditions") {
|
||||
await createTokenFunctions.DropERC20.setClaimConditions(params);
|
||||
} else if (stepId === "drop-erc20:mint-tokens") {
|
||||
await createTokenFunctions.DropERC20.mintTokens(params);
|
||||
} else if (stepId === "drop-erc20:airdrop-tokens") {
|
||||
await createTokenFunctions.DropERC20.airdropTokens(params);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,7 +216,10 @@ export function LaunchTokenStatus(props: {
|
||||
|
||||
reportAssetCreationFailed({
|
||||
assetType: "coin",
|
||||
contractType: "DropERC20",
|
||||
contractType:
|
||||
formValues.saleMode === "drop-erc20:token-drop"
|
||||
? "DropERC20"
|
||||
: "ERC20Asset",
|
||||
error: errorMessage,
|
||||
step: currentStep.id,
|
||||
});
|
||||
@@ -181,10 +235,18 @@ export function LaunchTokenStatus(props: {
|
||||
|
||||
reportAssetCreationSuccessful({
|
||||
assetType: "coin",
|
||||
contractType: "DropERC20",
|
||||
contractType:
|
||||
formValues.saleMode === "drop-erc20:token-drop"
|
||||
? "DropERC20"
|
||||
: "ERC20Asset",
|
||||
});
|
||||
|
||||
props.onLaunchSuccess();
|
||||
if (contractAddress) {
|
||||
props.onLaunchSuccess({
|
||||
chainId: Number(formValues.chain),
|
||||
contractAddress,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRetry(step: MultiStepState<StepId>, gasless: boolean) {
|
||||
@@ -196,6 +258,10 @@ export function LaunchTokenStatus(props: {
|
||||
await executeSteps(steps, startIndex, gasless);
|
||||
}
|
||||
|
||||
const contractLink = contractAddress
|
||||
? `/team/${props.teamSlug}/${props.projectSlug}/contract/${formValues.chain}/${contractAddress}`
|
||||
: null;
|
||||
|
||||
return (
|
||||
<StepCard
|
||||
nextButton={{
|
||||
@@ -302,7 +368,7 @@ export function LaunchTokenStatus(props: {
|
||||
className="gap-0 overflow-hidden p-0 md:max-w-[480px]"
|
||||
dialogCloseClassName="hidden"
|
||||
>
|
||||
<div className="flex flex-col gap-6 p-6">
|
||||
<div className="flex flex-col gap-6 p-6 overflow-hidden">
|
||||
<DialogHeader className="space-y-0.5">
|
||||
<DialogTitle className="font-semibold text-xl tracking-tight">
|
||||
Status
|
||||
|
||||
+20
-5
@@ -8,6 +8,7 @@ import { FormFieldSetup } from "@/components/blocks/FormFieldSetup";
|
||||
import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { SocialUrlsFieldset } from "../../_common/SocialUrls";
|
||||
import { StepCard } from "../../_common/step-card";
|
||||
@@ -47,7 +48,7 @@ export function TokenInfoFieldset(props: {
|
||||
client={props.client}
|
||||
setValue={(file) =>
|
||||
form.setValue("image", file, {
|
||||
shouldTouch: true,
|
||||
shouldValidate: true,
|
||||
})
|
||||
}
|
||||
value={form.watch("image")}
|
||||
@@ -59,7 +60,7 @@ export function TokenInfoFieldset(props: {
|
||||
{/* name + symbol */}
|
||||
<div className="flex flex-col gap-6 lg:flex-row lg:gap-4">
|
||||
<FormFieldSetup
|
||||
className="grow"
|
||||
className="lg:max-w-[300px] grow"
|
||||
errorMessage={form.formState.errors.name?.message}
|
||||
htmlFor={nameId}
|
||||
isRequired
|
||||
@@ -73,7 +74,7 @@ export function TokenInfoFieldset(props: {
|
||||
</FormFieldSetup>
|
||||
|
||||
<FormFieldSetup
|
||||
className="lg:max-w-[200px]"
|
||||
className="lg:max-w-[150px]"
|
||||
errorMessage={form.formState.errors.symbol?.message}
|
||||
htmlFor={symbolId}
|
||||
isRequired
|
||||
@@ -94,14 +95,28 @@ export function TokenInfoFieldset(props: {
|
||||
isRequired
|
||||
label="Chain"
|
||||
>
|
||||
<ClientOnly ssr={null}>
|
||||
<ClientOnly ssr={<Skeleton className="h-10" />}>
|
||||
<SingleNetworkSelector
|
||||
priorityChains={[
|
||||
8453, // base
|
||||
84532, // base sepolia
|
||||
1, // ethereum
|
||||
56, // bnb smart chain mainnet (bsc)
|
||||
42161, // arbitrum one mainnet
|
||||
43114, // avalanche mainnet
|
||||
146, // sonic
|
||||
137, // polygon
|
||||
80094, // berachain mainnet
|
||||
10, // optimism
|
||||
]}
|
||||
chainId={Number(form.watch("chain"))}
|
||||
className="bg-background"
|
||||
client={props.client}
|
||||
disableChainId
|
||||
onChange={(chain) => {
|
||||
form.setValue("chain", chain.toString());
|
||||
form.setValue("chain", chain.toString(), {
|
||||
shouldValidate: true,
|
||||
});
|
||||
props.onChainUpdated();
|
||||
}}
|
||||
/>
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
const MIN_TICK = -887200;
|
||||
const MAX_TICK = 887200;
|
||||
const TICK_SPACING = 200;
|
||||
|
||||
export function getInitialTickValue(params: { startingPricePerToken: number }) {
|
||||
const calculatedTick =
|
||||
Math.log(params.startingPricePerToken) / Math.log(1.0001);
|
||||
|
||||
// Round to nearest tick spacing
|
||||
const tick = Math.round(calculatedTick / TICK_SPACING) * TICK_SPACING;
|
||||
|
||||
return tick;
|
||||
}
|
||||
|
||||
export function isValidTickValue(tick: number) {
|
||||
return tick >= MIN_TICK && tick <= MAX_TICK;
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,3 @@
|
||||
import { DialogDescription } from "@radix-ui/react-dialog";
|
||||
import { AlertTriangleIcon } from "lucide-react";
|
||||
import type { WebhookConfig } from "@/api/project/webhook-configs";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
@@ -6,6 +5,7 @@ import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
import type { Meta } from "@storybook/nextjs";
|
||||
import { toUnits } from "thirdweb";
|
||||
import { base } from "thirdweb/chains";
|
||||
import { ThirdwebProvider } from "thirdweb/react";
|
||||
import { storybookThirdwebClient } from "@/storybook/utils";
|
||||
import { ClaimRewardsPageUI } from "./claim-rewards-page";
|
||||
|
||||
const meta = {
|
||||
component: ClaimRewardsPageUI,
|
||||
title: "contracts/extensions/claim-rewards",
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<ThirdwebProvider>
|
||||
<div className="container max-w-[1154px] py-10">
|
||||
<Story />
|
||||
</div>
|
||||
</ThirdwebProvider>
|
||||
),
|
||||
],
|
||||
} satisfies Meta<typeof ClaimRewardsPageUI>;
|
||||
|
||||
export default meta;
|
||||
|
||||
const recipient = "0x8C00f3F231c88CcAc2382AaC6e09A78D4F42129d";
|
||||
const developer = "0x1Af20C6B23373350aD464700B5965CE4B0D2aD94";
|
||||
|
||||
function unclaimedFeesStub(token0Amount: bigint, token1Amount: bigint) {
|
||||
return {
|
||||
token0: {
|
||||
address: "0x1234567890123456789012345678901234567890",
|
||||
amount: token0Amount,
|
||||
symbol: "FOO",
|
||||
},
|
||||
token1: {
|
||||
address: "0x0987654321098765432109876543210987654321",
|
||||
amount: token1Amount,
|
||||
symbol: "BAR",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function LargeAmounts() {
|
||||
return (
|
||||
<Variant
|
||||
token0Amount={toUnits("100000", 18)}
|
||||
token1Amount={toUnits("500000", 18)}
|
||||
includeChainExplorer
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SmallAmounts() {
|
||||
return (
|
||||
<Variant
|
||||
token0Amount={toUnits("0.001", 18)}
|
||||
token1Amount={toUnits("0.0005", 18)}
|
||||
includeChainExplorer
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ZeroAmount() {
|
||||
return (
|
||||
<Variant
|
||||
token0Amount={toUnits("0", 18)}
|
||||
token1Amount={toUnits("0", 18)}
|
||||
includeChainExplorer
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ZeroAmountNoChainExplorer() {
|
||||
return (
|
||||
<Variant token0Amount={toUnits("0", 18)} token1Amount={toUnits("0", 18)} />
|
||||
);
|
||||
}
|
||||
|
||||
function Variant(props: {
|
||||
token0Amount: bigint;
|
||||
token1Amount: bigint;
|
||||
includeChainExplorer?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<ClaimRewardsPageUI
|
||||
developerBps={5000}
|
||||
chainSlug="base"
|
||||
recipient={recipient}
|
||||
developer={developer}
|
||||
client={storybookThirdwebClient}
|
||||
handleClaim={() => {}}
|
||||
isClaimPending={false}
|
||||
chain={base}
|
||||
unclaimedFees={unclaimedFeesStub(props.token0Amount, props.token1Amount)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+322
@@ -0,0 +1,322 @@
|
||||
"use client";
|
||||
|
||||
import { DollarSignIcon, ExternalLinkIcon, SplitIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
type Chain,
|
||||
type ThirdwebClient,
|
||||
type ThirdwebContract,
|
||||
toTokens,
|
||||
} from "thirdweb";
|
||||
import { TokenIcon, TokenProvider, useSendTransaction } from "thirdweb/react";
|
||||
import { claimRewards } from "thirdweb/tokens";
|
||||
import { DistributionBarChart } from "@/components/blocks/distribution-chart";
|
||||
import { WalletAddress } from "@/components/blocks/wallet-address";
|
||||
import { TransactionButton } from "@/components/tx-button";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { parseError } from "@/utils/errorParser";
|
||||
import { tryCatch } from "@/utils/try-catch";
|
||||
import type { getValidReward } from "../utils/rewards";
|
||||
|
||||
export function ClaimRewardsPage(props: {
|
||||
assetContractClient: ThirdwebContract;
|
||||
entrypointContractClient: ThirdwebContract;
|
||||
reward: NonNullable<Awaited<ReturnType<typeof getValidReward>>>;
|
||||
unclaimedFees: {
|
||||
token0: {
|
||||
address: string;
|
||||
amount: bigint;
|
||||
symbol: string;
|
||||
};
|
||||
token1: {
|
||||
address: string;
|
||||
amount: bigint;
|
||||
symbol: string;
|
||||
};
|
||||
};
|
||||
chainSlug: string;
|
||||
}) {
|
||||
const sendTx = useSendTransaction();
|
||||
const router = useDashboardRouter();
|
||||
|
||||
async function handleClaim() {
|
||||
const claimRewardsTx = claimRewards({
|
||||
asset: props.assetContractClient.address,
|
||||
contract: props.entrypointContractClient,
|
||||
});
|
||||
|
||||
const claimRewardsResult = await tryCatch(
|
||||
sendTx.mutateAsync(claimRewardsTx),
|
||||
);
|
||||
|
||||
if (claimRewardsResult.error) {
|
||||
toast.error("Failed to distribute rewards", {
|
||||
description: parseError(claimRewardsResult.error),
|
||||
});
|
||||
} else {
|
||||
toast.success("Rewards distributed successfully");
|
||||
router.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ClaimRewardsPageUI
|
||||
unclaimedFees={props.unclaimedFees}
|
||||
recipient={props.reward.recipient}
|
||||
developer={props.reward.developer}
|
||||
handleClaim={handleClaim}
|
||||
isClaimPending={sendTx.isPending}
|
||||
client={props.assetContractClient.client}
|
||||
chain={props.assetContractClient.chain}
|
||||
chainSlug={props.chainSlug}
|
||||
developerBps={props.reward.developerBps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function calculateFees(developerBps: number) {
|
||||
// 20% of is protocol fees
|
||||
// remaining is split between developer and recipient
|
||||
|
||||
const protocolFees = 20;
|
||||
const remaining = 100 - protocolFees;
|
||||
|
||||
const developerPercentageFinal = (remaining * developerBps) / 10000;
|
||||
|
||||
return {
|
||||
protocolFees,
|
||||
developerPercentage: developerPercentageFinal,
|
||||
recipientPercentage: 100 - protocolFees - developerPercentageFinal,
|
||||
};
|
||||
}
|
||||
|
||||
export function ClaimRewardsPageUI(props: {
|
||||
unclaimedFees: {
|
||||
token0: {
|
||||
address: string;
|
||||
amount: bigint;
|
||||
symbol: string;
|
||||
};
|
||||
token1: {
|
||||
address: string;
|
||||
amount: bigint;
|
||||
symbol: string;
|
||||
};
|
||||
};
|
||||
recipient: string;
|
||||
developer: string;
|
||||
developerBps: number;
|
||||
handleClaim: () => void;
|
||||
isClaimPending: boolean;
|
||||
client: ThirdwebClient;
|
||||
chain: Chain;
|
||||
chainSlug: string;
|
||||
}) {
|
||||
const fees = calculateFees(props.developerBps);
|
||||
|
||||
const recipientColor = `hsl(var(--chart-1))`;
|
||||
const developerColor = `hsl(var(--chart-2))`;
|
||||
const protocolFeesColor = `hsl(var(--chart-3))`;
|
||||
|
||||
const hasUnclaimedRewards =
|
||||
props.unclaimedFees.token0.amount > 0 ||
|
||||
props.unclaimedFees.token1.amount > 0;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="bg-card rounded-lg border">
|
||||
<div className="p-4 lg:p-6 border-b border-dashed">
|
||||
<div className="flex mb-3">
|
||||
<div className="rounded-full border bg-background p-2">
|
||||
<DollarSignIcon className="size-5 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 className="font-semibold text-2xl tracking-tight mb-0.5">
|
||||
Rewards
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Earnings received by Liquidity Providers (LPs) in exchange for
|
||||
depositing tokens into {props.unclaimedFees.token0.symbol} /{" "}
|
||||
{props.unclaimedFees.token1.symbol} Uniswap liquidity pool
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 lg:px-6 py-8 border-b border-dashed">
|
||||
<div className="mb-4">
|
||||
<h3 className="font-medium text-base mb-1">Unclaimed Rewards</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
The rewards that are earned but haven't been distributed yet
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 lg:flex-row">
|
||||
<TokenReward
|
||||
token={props.unclaimedFees.token0}
|
||||
client={props.client}
|
||||
chain={props.chain}
|
||||
chainSlug={props.chainSlug}
|
||||
/>
|
||||
<TokenReward
|
||||
token={props.unclaimedFees.token1}
|
||||
client={props.client}
|
||||
chain={props.chain}
|
||||
chainSlug={props.chainSlug}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 lg:px-6 py-8">
|
||||
<h3 className="font-medium text-base mb-1">Reward Distribution</h3>
|
||||
<p className="text-muted-foreground text-sm mb-4">
|
||||
The unclaimed rewards will be distributed as:
|
||||
</p>
|
||||
|
||||
<DistributionBarChart
|
||||
segments={[
|
||||
{
|
||||
label: "Recipient",
|
||||
color: recipientColor,
|
||||
percent: fees.recipientPercentage,
|
||||
value: `${fees.recipientPercentage}%`,
|
||||
},
|
||||
{
|
||||
label: "Protocol",
|
||||
color: protocolFeesColor,
|
||||
percent: fees.protocolFees,
|
||||
value: `${fees.protocolFees}%`,
|
||||
},
|
||||
{
|
||||
label: "Developer",
|
||||
color: developerColor,
|
||||
percent: fees.developerPercentage,
|
||||
value: `${fees.developerPercentage}%`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className="h-6" />
|
||||
|
||||
<div className="flex gap-5">
|
||||
<div>
|
||||
<p className="font-medium text-sm">Recipient</p>
|
||||
<WalletAddress
|
||||
address={props.recipient}
|
||||
client={props.client}
|
||||
iconClassName="size-3"
|
||||
className="h-auto py-1 text-xs"
|
||||
fallbackIcon={
|
||||
<div
|
||||
className="size-3 rounded-full"
|
||||
style={{
|
||||
backgroundColor: recipientColor,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="border-l pl-5">
|
||||
<p className="font-medium text-sm">Developer</p>
|
||||
<WalletAddress
|
||||
address={props.developer}
|
||||
client={props.client}
|
||||
iconClassName="size-3"
|
||||
className="h-auto py-1 text-xs"
|
||||
fallbackIcon={
|
||||
<div
|
||||
className="size-3 rounded-full"
|
||||
style={{
|
||||
backgroundColor: developerColor,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 lg:p-6 border-t border-dashed flex-col lg:flex-row flex lg:justify-between lg:items-center gap-4">
|
||||
{hasUnclaimedRewards && (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Click on "Distribute Rewards" to distribute unclaimed rewards
|
||||
</p>
|
||||
)}
|
||||
|
||||
{!hasUnclaimedRewards && (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
There are no unclaimed rewards available for distribution
|
||||
</p>
|
||||
)}
|
||||
<TransactionButton
|
||||
onClick={props.handleClaim}
|
||||
className="bg-background rounded-full"
|
||||
transactionCount={undefined}
|
||||
isPending={props.isClaimPending}
|
||||
txChainID={props.chain.id}
|
||||
isLoggedIn={true}
|
||||
client={props.client}
|
||||
disabled={!hasUnclaimedRewards}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
<SplitIcon className="size-3.5 text-muted-foreground" />
|
||||
Distribute Rewards
|
||||
</TransactionButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TokenReward(props: {
|
||||
token: {
|
||||
address: string;
|
||||
amount: bigint;
|
||||
symbol: string;
|
||||
};
|
||||
client: ThirdwebClient;
|
||||
chain: Chain;
|
||||
chainSlug: string;
|
||||
}) {
|
||||
const fallbackIcon = (
|
||||
<div className="size-9 rounded-full from-muted to-card bg-gradient-to-br flex items-center justify-center text-muted-foreground font-bold uppercase">
|
||||
{props.token.symbol[0]}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="border p-3 rounded-xl flex items-center gap-3 min-w-[300px] bg-background relative hover:border-active-border">
|
||||
<div className="rounded-full border shrink-0">
|
||||
<TokenProvider
|
||||
address={props.token.address}
|
||||
client={props.client}
|
||||
chain={props.chain}
|
||||
>
|
||||
<TokenIcon
|
||||
className="size-9 rounded-full"
|
||||
loadingComponent={fallbackIcon}
|
||||
fallbackComponent={fallbackIcon}
|
||||
/>
|
||||
</TokenProvider>
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
<p className="font-bold text-sm">
|
||||
{toTokens(props.token.amount, 18)} {props.token.symbol}
|
||||
</p>
|
||||
<Link
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={`/${props.chainSlug}/${props.token.address}`}
|
||||
className="flex items-center gap-1.5 text-muted-foreground before:absolute before:inset-0"
|
||||
>
|
||||
<span className="text-xs font-mono">
|
||||
{props.token.address.slice(0, 6)}...
|
||||
{props.token.address.slice(-4)}
|
||||
</span>
|
||||
<ExternalLinkIcon className="size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getContract } from "thirdweb";
|
||||
import { getDeployedEntrypointERC20 } from "thirdweb/tokens";
|
||||
import { getProject } from "@/api/project/projects";
|
||||
import { getContractPageParamsInfo } from "../../../../../../../(dashboard)/(chain)/[chain_id]/[contractAddress]/_utils/getContractFromParams";
|
||||
import type { ProjectContractPageParams } from "../types";
|
||||
import { ClaimRewardsPage } from "./components/claim-rewards-page";
|
||||
import { getValidReward } from "./utils/rewards";
|
||||
import { getUnclaimedFees } from "./utils/unclaimed-fees";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<ProjectContractPageParams>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const project = await getProject(params.team_slug, params.project_slug);
|
||||
|
||||
if (!project) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const info = await getContractPageParamsInfo({
|
||||
chainIdOrSlug: params.chainIdOrSlug,
|
||||
contractAddress: params.contractAddress,
|
||||
teamId: project.teamId,
|
||||
});
|
||||
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const chain = info.clientContract.chain;
|
||||
const assetContractServer = info.serverContract;
|
||||
const serverClient = assetContractServer.client;
|
||||
|
||||
const { address: entrypointContractAddress } =
|
||||
await getDeployedEntrypointERC20({
|
||||
chain,
|
||||
client: serverClient,
|
||||
});
|
||||
|
||||
// Note: must use server contract/client here
|
||||
const reward = await getValidReward({
|
||||
assetContract: assetContractServer,
|
||||
entrypointContract: getContract({
|
||||
address: entrypointContractAddress,
|
||||
chain,
|
||||
client: serverClient,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!reward) {
|
||||
redirect(
|
||||
`/team/${params.team_slug}/${params.project_slug}/contract/${params.chainIdOrSlug}/${params.contractAddress}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Note: must use server contract/client here
|
||||
const unclaimedFees = await getUnclaimedFees({
|
||||
positionManager: getContract({
|
||||
address: reward.positionManager,
|
||||
chain,
|
||||
client: serverClient,
|
||||
}),
|
||||
reward: {
|
||||
tokenId: reward.positionId,
|
||||
recipient: reward.recipient,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<ClaimRewardsPage
|
||||
assetContractClient={info.clientContract}
|
||||
entrypointContractClient={getContract({
|
||||
address: entrypointContractAddress,
|
||||
chain: chain,
|
||||
client: info.clientContract.client,
|
||||
})}
|
||||
reward={reward}
|
||||
unclaimedFees={unclaimedFees}
|
||||
chainSlug={info.chainMetadata.slug}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { getRewards } from "thirdweb/tokens";
|
||||
|
||||
export async function getValidReward(params: {
|
||||
assetContract: ThirdwebContract;
|
||||
entrypointContract: ThirdwebContract;
|
||||
}) {
|
||||
try {
|
||||
const rewards = await getRewards({
|
||||
contract: params.entrypointContract,
|
||||
asset: params.assetContract.address,
|
||||
});
|
||||
|
||||
if (rewards.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// It's potentially possible to have multiple rewards locked up, but it's not the default use case.
|
||||
return rewards[0];
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { getContract, readContract } from "thirdweb";
|
||||
import { symbol } from "thirdweb/extensions/common";
|
||||
|
||||
const maxUint128 = 2n ** 128n - 1n;
|
||||
|
||||
export async function getUnclaimedFees(params: {
|
||||
positionManager: ThirdwebContract;
|
||||
reward: {
|
||||
tokenId: bigint;
|
||||
recipient: string;
|
||||
};
|
||||
}) {
|
||||
const collectResultPromise = readContract({
|
||||
contract: params.positionManager,
|
||||
method:
|
||||
"function collect((uint256 tokenId,address recipient,uint128 amount0Max,uint128 amount1Max)) returns (uint256,uint256)",
|
||||
params: [
|
||||
{
|
||||
tokenId: params.reward.tokenId,
|
||||
recipient: params.reward.recipient,
|
||||
amount0Max: maxUint128,
|
||||
amount1Max: maxUint128,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const positionsResultPromise = readContract({
|
||||
contract: params.positionManager,
|
||||
method:
|
||||
"function positions(uint256 tokenId) view returns (uint96,address,address,address,uint24,int24,int24,uint128,uint256,uint256,uint128,uint128)",
|
||||
params: [params.reward.tokenId],
|
||||
});
|
||||
|
||||
const [collectResult, positionsResult] = await Promise.all([
|
||||
collectResultPromise,
|
||||
positionsResultPromise,
|
||||
]);
|
||||
|
||||
// 0- nonce
|
||||
// 1- owner
|
||||
// 2- token0
|
||||
// 3- token1
|
||||
// 4 - fee
|
||||
// 5 - tickLower
|
||||
// 6 - tickUpper
|
||||
// 7 - liquidity
|
||||
// 8 - feeGrowthInside0LastX128
|
||||
// 9 - feeGrowthInside1LastX128
|
||||
// 10 - tokensOwed0
|
||||
// 11 - tokensOwed1
|
||||
|
||||
const client = params.positionManager.client;
|
||||
const chain = params.positionManager.chain;
|
||||
|
||||
const token0Address = positionsResult[2];
|
||||
const token1Address = positionsResult[3];
|
||||
|
||||
const [token0Symbol, token1Symbol] = await Promise.all([
|
||||
symbol({
|
||||
contract: getContract({
|
||||
address: token0Address,
|
||||
chain,
|
||||
client,
|
||||
}),
|
||||
}),
|
||||
symbol({
|
||||
contract: getContract({
|
||||
address: token1Address,
|
||||
chain,
|
||||
client,
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
token0: {
|
||||
address: token0Address,
|
||||
amount: collectResult[0],
|
||||
symbol: token0Symbol,
|
||||
},
|
||||
token1: {
|
||||
address: token1Address,
|
||||
amount: collectResult[1],
|
||||
symbol: token1Symbol,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Inter } from "next/font/google";
|
||||
import { cn } from "@/lib/utils";
|
||||
import "../../global.css";
|
||||
import "@workspace/ui/global.css";
|
||||
import { BridgeProviders } from "./components/client/Providers.client";
|
||||
|
||||
const fontSans = Inter({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import "../../global.css";
|
||||
import "@workspace/ui/global.css";
|
||||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import NextTopLoader from "nextjs-toploader";
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
import { ShieldCheckIcon } from "lucide-react";
|
||||
import type { Metadata } from "next";
|
||||
import { defineChain, getContract } from "thirdweb";
|
||||
import { getCurrencyMetadata } from "thirdweb/extensions/erc20";
|
||||
import { checksumAddress } from "thirdweb/utils";
|
||||
import { Bridge, defineChain, toTokens } from "thirdweb";
|
||||
import { getChainMetadata } from "thirdweb/chains";
|
||||
import { shortenAddress } from "thirdweb/utils";
|
||||
import { getPaymentLink } from "@/api/universal-bridge/links";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
|
||||
import {
|
||||
API_SERVER_SECRET,
|
||||
DASHBOARD_THIRDWEB_SECRET_KEY,
|
||||
} from "@/constants/server-envs";
|
||||
import { getConfiguredThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import { resolveEns } from "@/lib/ens";
|
||||
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
|
||||
import { PayPageWidget } from "../components/client/PayPageWidget.client";
|
||||
import { payAppThirdwebClient } from "../constants";
|
||||
|
||||
const title = "thirdweb Pay";
|
||||
const description = "Fast, secure, and simple payments.";
|
||||
@@ -32,40 +42,186 @@ export default async function PayPage({
|
||||
paymentId: id,
|
||||
});
|
||||
|
||||
const tokenContract = getContract({
|
||||
address: paymentLink.destinationToken.address, // for this RPC call, use the dashboard client
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
chain: defineChain(Number(paymentLink.destinationToken.chainId)),
|
||||
client: getClientThirdwebClient(undefined),
|
||||
});
|
||||
const {
|
||||
symbol,
|
||||
decimals,
|
||||
name: tokenName,
|
||||
} = await getCurrencyMetadata({
|
||||
contract: tokenContract,
|
||||
});
|
||||
const token = {
|
||||
address: checksumAddress(paymentLink.destinationToken.address),
|
||||
const projectMetadataPromise = getProjectMetadata(paymentLink.clientId);
|
||||
|
||||
const tokensPromise = Bridge.tokens({
|
||||
client: getConfiguredThirdwebClient({
|
||||
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
|
||||
teamId: undefined,
|
||||
}),
|
||||
chainId: Number(paymentLink.destinationToken.chainId),
|
||||
decimals,
|
||||
name: tokenName,
|
||||
symbol,
|
||||
};
|
||||
tokenAddress: paymentLink.destinationToken.address,
|
||||
});
|
||||
|
||||
const chainPromise = getChainMetadata(
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
defineChain(Number(paymentLink.destinationToken.chainId)),
|
||||
);
|
||||
|
||||
const recipientPromise = resolveEns(
|
||||
paymentLink.receiver,
|
||||
getConfiguredThirdwebClient({
|
||||
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
|
||||
teamId: undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
const [tokens, projectMetadata, chain, recipientEnsOrAddress] =
|
||||
await Promise.all([
|
||||
tokensPromise,
|
||||
projectMetadataPromise,
|
||||
chainPromise,
|
||||
recipientPromise,
|
||||
]);
|
||||
|
||||
const token = tokens[0];
|
||||
if (!token) {
|
||||
throw new Error("Token not found");
|
||||
}
|
||||
|
||||
return (
|
||||
<PayPageWidget
|
||||
amount={paymentLink.amount ? BigInt(paymentLink.amount) : undefined}
|
||||
chainId={Number(paymentLink.destinationToken.chainId)}
|
||||
clientId={undefined} // Payment links don't need to use the same client ID to be executed
|
||||
image={paymentLink.imageUrl}
|
||||
name={paymentLink.title}
|
||||
paymentLinkId={id}
|
||||
purchaseData={paymentLink.purchaseData}
|
||||
recipientAddress={paymentLink.receiver}
|
||||
redirectUri={redirectUri}
|
||||
theme={theme}
|
||||
token={token}
|
||||
/>
|
||||
<div className="flex z-10 flex-col lg:flex-row h-full w-full">
|
||||
<header className="min-w-full lg:min-w-[500px] border-b lg:border-r lg:h-full bg-card flex flex-col gap-4 items-start p-4 lg:p-8">
|
||||
<div>
|
||||
<div className="flex flex-row items-center justify-start gap-4">
|
||||
{projectMetadata.image && (
|
||||
<img
|
||||
src={
|
||||
resolveSchemeWithErrorHandler({
|
||||
uri: projectMetadata.image,
|
||||
client: payAppThirdwebClient,
|
||||
}) || ""
|
||||
}
|
||||
alt={projectMetadata.name}
|
||||
width={25}
|
||||
height={25}
|
||||
className="rounded-full overflow-hidden"
|
||||
/>
|
||||
)}
|
||||
<h2 className="text-xl font-bold">{projectMetadata.name}</h2>
|
||||
</div>
|
||||
{projectMetadata.description && (
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
{projectMetadata.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="hidden lg:block my-4 w-full">
|
||||
{paymentLink.amount && (
|
||||
<div className="flex flex-col gap-1 w-full my-4">
|
||||
<span className="text-muted-foreground text-xs">Details</span>
|
||||
<div className="font-medium flex-row flex justify-between items-center w-full">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
{token.iconUri && (
|
||||
<img
|
||||
src={resolveSchemeWithErrorHandler({
|
||||
uri: token.iconUri,
|
||||
client: getConfiguredThirdwebClient({
|
||||
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
|
||||
teamId: undefined,
|
||||
}),
|
||||
})}
|
||||
alt={token.name}
|
||||
width={25}
|
||||
height={25}
|
||||
className="size-5 rounded-full overflow-hidden"
|
||||
/>
|
||||
)}
|
||||
{toTokens(BigInt(paymentLink.amount), token.decimals)}{" "}
|
||||
{token.symbol}
|
||||
</div>
|
||||
{token.prices.USD && (
|
||||
<span>
|
||||
$
|
||||
{(
|
||||
Number(token.prices.USD) *
|
||||
Number(
|
||||
toTokens(BigInt(paymentLink.amount), token.decimals),
|
||||
)
|
||||
).toFixed(2)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{chain && (
|
||||
<div className="flex flex-col gap-1 w-full my-4">
|
||||
<span className="text-muted-foreground text-xs">Network</span>
|
||||
<div className="font-medium flex-row flex justify-between items-center w-full">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
{chain.icon?.url && (
|
||||
<img
|
||||
src={resolveSchemeWithErrorHandler({
|
||||
uri: chain.icon.url,
|
||||
client: getConfiguredThirdwebClient({
|
||||
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
|
||||
teamId: undefined,
|
||||
}),
|
||||
})}
|
||||
alt={chain.name}
|
||||
width={chain.icon.width}
|
||||
height={chain.icon.height}
|
||||
className="size-5 rounded-full overflow-hidden"
|
||||
/>
|
||||
)}
|
||||
{chain.name}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{recipientEnsOrAddress.ensName ||
|
||||
(recipientEnsOrAddress.address && (
|
||||
<div className="flex flex-col gap-1 w-full my-4">
|
||||
<span className="text-muted-foreground text-xs">Seller</span>
|
||||
<div className="font-medium flex-row flex justify-between items-center w-full">
|
||||
{recipientEnsOrAddress.ensName ??
|
||||
shortenAddress(recipientEnsOrAddress.address)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-auto hidden lg:block">
|
||||
<Badge className="flex items-center gap-1.5 bg-purple-100 text-purple-800 border-purple-200 dark:bg-purple-950 dark:text-purple-300 dark:border-purple-800">
|
||||
<ShieldCheckIcon className="size-3" />
|
||||
Secured by thirdweb
|
||||
</Badge>
|
||||
</div>
|
||||
</header>
|
||||
<main className="flex justify-center p-12 w-full items-center">
|
||||
<PayPageWidget
|
||||
amount={paymentLink.amount ? BigInt(paymentLink.amount) : undefined}
|
||||
chainId={Number(paymentLink.destinationToken.chainId)}
|
||||
clientId={undefined} // Payment links don't need to use the same client ID to be executed
|
||||
image={paymentLink.imageUrl}
|
||||
name={paymentLink.title}
|
||||
paymentLinkId={id}
|
||||
purchaseData={paymentLink.purchaseData}
|
||||
recipientAddress={paymentLink.receiver}
|
||||
redirectUri={redirectUri}
|
||||
theme={theme}
|
||||
token={token}
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
async function getProjectMetadata(clientId: string) {
|
||||
const url = new URL(`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v2/keys/lookup`);
|
||||
url.searchParams.append("clientId", clientId);
|
||||
const response = await fetch(url.toString(), {
|
||||
headers: {
|
||||
"x-service-api-key": API_SERVER_SECRET,
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch project");
|
||||
}
|
||||
|
||||
const { data } = (await response.json()) as {
|
||||
data: { name: string; image: string | null; description: string | null };
|
||||
};
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import "../../global.css";
|
||||
import "@workspace/ui/global.css";
|
||||
import { Inter } from "next/font/google";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -30,17 +30,8 @@ export default async function PayLayout({
|
||||
disableTransitionOnChange
|
||||
enableSystem={false}
|
||||
>
|
||||
<div className="relative mx-auto flex h-full w-full flex-col items-center justify-center overflow-x-hidden overflow-y-scroll py-10">
|
||||
<main className="container z-10 flex justify-center">
|
||||
{children}
|
||||
</main>
|
||||
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
alt=""
|
||||
className="-bottom-12 -right-12 pointer-events-none absolute lg:right-0 lg:bottom-0"
|
||||
src="/assets/login/background.svg"
|
||||
/>
|
||||
<div className="relative mx-auto flex h-full w-full items-center justify-center overflow-x-hidden overflow-y-scroll">
|
||||
{children}
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
</PayProviders>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { PayPageWidget } from "./components/client/PayPageWidget.client";
|
||||
import type { PayParams } from "./components/types";
|
||||
import { payAppThirdwebClient } from "./constants";
|
||||
|
||||
const title = "thirdweb Pay";
|
||||
const title = "thirdweb Payments";
|
||||
const description = "Fast, secure, and simple payments.";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import tailwindConfig from "@workspace/ui/tailwind.config";
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
const config: Config = {
|
||||
...tailwindConfig,
|
||||
content: [
|
||||
"./src/**/*.{ts,tsx}",
|
||||
// add contents of ui package
|
||||
"../../packages/ui/src/**/*.{ts,tsx}",
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -9,11 +9,11 @@
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
@@ -22,7 +22,7 @@
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"target": "ES2020"
|
||||
"target": "ES2022"
|
||||
},
|
||||
"exclude": ["node_modules", ".next", "lib/__generated__"],
|
||||
"include": [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/knip@5/schema.json",
|
||||
"ignore": ["src/components/ui/**", "src/app/insight/utils.ts"],
|
||||
"ignoreDependencies": ["server-only"],
|
||||
"ignoreDependencies": ["server-only", "@workspace/ui", "tailwindcss-animate"],
|
||||
"next": true,
|
||||
"project": ["src/**"]
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ const securityHeaders = [
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
transpilePackages: ["@workspace/ui"],
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
@@ -164,10 +165,6 @@ const nextConfig = {
|
||||
},
|
||||
];
|
||||
},
|
||||
webpack: (config) => {
|
||||
config.externals.push("pino-pretty", "lokijs", "encoding");
|
||||
return config;
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@radix-ui/react-tooltip": "1.2.7",
|
||||
"@tanstack/react-query": "5.81.5",
|
||||
"@workspace/ui": "workspace:*",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "4.1.0",
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
autoprefixer: {},
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@workspace/ui/postcss.config";
|
||||
@@ -63,7 +63,7 @@ export async function promptNebula(params: {
|
||||
const events = await stream(`${API_URL}/ai/chat`, {
|
||||
body: JSON.stringify(body),
|
||||
headers: {
|
||||
"x-client-id": process.env.NEXT_PUBLIC_THIRDWEB_CLIENT_ID!,
|
||||
"x-client-id": process.env.NEXT_PUBLIC_THIRDWEB_CLIENT_ID || "",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
type SessionContextFilter = {
|
||||
chain_ids: string[] | null;
|
||||
wallet_address: string | null;
|
||||
};
|
||||
|
||||
type NebulaUserMessageContentItem =
|
||||
| {
|
||||
type: "image";
|
||||
@@ -26,17 +21,6 @@ export type NebulaUserMessage = {
|
||||
content: NebulaUserMessageContent;
|
||||
};
|
||||
|
||||
export type NebulaSessionHistoryMessage =
|
||||
| {
|
||||
role: "assistant" | "action" | "image";
|
||||
content: string;
|
||||
timestamp: number;
|
||||
}
|
||||
| {
|
||||
role: "user";
|
||||
content: NebulaUserMessageContent | string;
|
||||
};
|
||||
|
||||
export type NebulaTxData = {
|
||||
chainId: number;
|
||||
data: `0x${string}`;
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
"use client";
|
||||
import { ArrowRightIcon, MessageCircleIcon } from "lucide-react";
|
||||
import {
|
||||
ArrowRightIcon,
|
||||
ExternalLinkIcon,
|
||||
MessageCircleIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { defineChain, prepareTransaction, type ThirdwebClient } from "thirdweb";
|
||||
@@ -28,10 +32,7 @@ import { Img } from "../../../components/ui/Img";
|
||||
import { Spinner } from "../../../components/ui/Spinner/Spinner";
|
||||
import { THIRDWEB_CLIENT } from "../../../lib/client";
|
||||
import { type NebulaContext, promptNebula } from "../api/chat";
|
||||
import type {
|
||||
NebulaSessionHistoryMessage,
|
||||
NebulaUserMessage,
|
||||
} from "../api/types";
|
||||
import type { NebulaUserMessage } from "../api/types";
|
||||
import { examplePrompts } from "../data/examplePrompts";
|
||||
import { resolveSchemeWithErrorHandler } from "./resolveSchemeWithErrorHandler";
|
||||
|
||||
@@ -346,8 +347,14 @@ export function ChatPageContent(props: {
|
||||
/>
|
||||
|
||||
{/* Footer disclaimer */}
|
||||
<p className="mt-3 text-center text-muted-foreground text-xs opacity-75 lg:text-sm">
|
||||
thirdweb AI can make mistakes. Please use with discretion
|
||||
<p className="flex items-center justify-center gap-1 mt-3 text-center text-muted-foreground hover:text-foreground text-xs opacity-75 lg:text-sm">
|
||||
<Link
|
||||
href="https://portal.thirdweb.com/ai/chat"
|
||||
target="_blank"
|
||||
>
|
||||
Learn how to integrate thirdweb AI into your apps
|
||||
</Link>
|
||||
<ExternalLinkIcon className="size-3" />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -392,8 +399,14 @@ export function ChatPageContent(props: {
|
||||
/>
|
||||
|
||||
{/* Footer disclaimer */}
|
||||
<p className="mt-3 text-center text-muted-foreground text-xs opacity-75 lg:text-sm">
|
||||
thirdweb AI can make mistakes. Please use with discretion
|
||||
<p className="flex items-center justify-center gap-1 mt-3 text-center text-muted-foreground hover:text-foreground text-xs opacity-75 lg:text-sm">
|
||||
<Link
|
||||
href="https://portal.thirdweb.com/ai/chat"
|
||||
target="_blank"
|
||||
>
|
||||
Learn how to integrate thirdweb AI into your apps
|
||||
</Link>
|
||||
<ExternalLinkIcon className="size-3" />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -569,7 +582,7 @@ function SimpleChats(props: {
|
||||
enableAutoScroll: boolean;
|
||||
sendMessage: (message: NebulaUserMessage) => void;
|
||||
}) {
|
||||
const { messages, setEnableAutoScroll, enableAutoScroll } = props;
|
||||
const { messages, enableAutoScroll } = props;
|
||||
const scrollAnchorRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// auto scroll to bottom when messages change
|
||||
@@ -1022,37 +1035,3 @@ function handleNebulaPromptError(params: {
|
||||
return newMessages;
|
||||
});
|
||||
}
|
||||
|
||||
function parseHistoryToMessages(history: NebulaSessionHistoryMessage[]) {
|
||||
const messages: ChatMessage[] = [];
|
||||
|
||||
for (const message of history) {
|
||||
switch (message.role) {
|
||||
case "user": {
|
||||
messages.push({
|
||||
content:
|
||||
typeof message.content === "string"
|
||||
? [
|
||||
{
|
||||
text: message.content,
|
||||
type: "text",
|
||||
},
|
||||
]
|
||||
: message.content,
|
||||
type: message.role,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "assistant": {
|
||||
messages.push({
|
||||
request_id: undefined,
|
||||
text: message.content,
|
||||
type: message.role,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
/* bg - neutral */
|
||||
--background: 0 0% 98%;
|
||||
--popover: 0 0% 100%;
|
||||
--card: 0 0% 100%;
|
||||
--secondary: 0 0% 90%;
|
||||
--muted: 0 0% 93%;
|
||||
--accent: 0 0% 93%;
|
||||
--inverted: 0 0% 4%;
|
||||
|
||||
/* bg - colorful */
|
||||
--primary: 221 83% 54%;
|
||||
--destructive: 360 72% 51%;
|
||||
|
||||
/* Text */
|
||||
--foreground: 0 0% 4%;
|
||||
--card-foreground: 0 0% 4%;
|
||||
--popover-foreground: 240 10% 4%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
--secondary-foreground: 0 0% 4%;
|
||||
--muted-foreground: 0 0% 40%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
--destructive-foreground: 0 0% 100%;
|
||||
--inverted-foreground: 0 0% 100%;
|
||||
--link-foreground: 221.21deg 83.19% 53.33%;
|
||||
--success-text: 142.09 70.56% 35.29%;
|
||||
--warning-text: 38 92% 40%;
|
||||
--destructive-text: 360 72% 60%;
|
||||
|
||||
/* Borders */
|
||||
--border: 0 0% 85%;
|
||||
--active-border: 0 0% 70%;
|
||||
--input: 0 0% 85%;
|
||||
--ring: 0 0% 80%;
|
||||
|
||||
/* Others */
|
||||
--radius: 0.5rem;
|
||||
|
||||
/* Sidebar */
|
||||
--sidebar-background: 0 0% 98%;
|
||||
--sidebar-foreground: 0 0% 4%;
|
||||
--sidebar-primary: 221 83% 54%;
|
||||
--sidebar-primary-foreground: 0 0% 100%;
|
||||
--sidebar-accent: 0 0% 93%;
|
||||
--sidebar-accent-foreground: 0 0% 9%;
|
||||
--sidebar-border: 0 0% 85%;
|
||||
--sidebar-ring: 0 0% 80%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
/* bg - neutral */
|
||||
--background: 0 0% 0%;
|
||||
--card: 0 0% 3.92%;
|
||||
--popover: 0 0% 0%;
|
||||
--secondary: 0 0% 11%;
|
||||
--muted: 0 0% 11%;
|
||||
--accent: 0 0% 11%;
|
||||
--inverted: 0 0% 100%;
|
||||
|
||||
/* bg - colorful */
|
||||
--primary: 221 83% 54%;
|
||||
--destructive: 360 72% 51%;
|
||||
|
||||
/* Text */
|
||||
--foreground: 0 0% 98%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted-foreground: 0 0% 63%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive-foreground: 0 0% 100%;
|
||||
--link-foreground: 215.88 100% 65%;
|
||||
--warning-text: 38 92% 50%;
|
||||
--destructive-text: 360 72% 55%;
|
||||
--success-text: 142 75% 50%;
|
||||
--inverted-foreground: 0 0% 0%;
|
||||
|
||||
/* Borders */
|
||||
--border: 0 0% 15%;
|
||||
--active-border: 0 0% 22%;
|
||||
--ring: 0 0% 30%;
|
||||
--input: 0 0% 15%;
|
||||
|
||||
/* sidebar */
|
||||
--sidebar-background: 0 0% 0%;
|
||||
--sidebar-foreground: 0 0% 98%;
|
||||
--sidebar-primary: 221 83% 54%;
|
||||
--sidebar-primary-foreground: 0 0% 100%;
|
||||
--sidebar-accent: 0 0% 11%;
|
||||
--sidebar-accent-foreground: 0 0% 98%;
|
||||
--sidebar-border: 0 0% 15%;
|
||||
--sidebar-ring: 0 0% 30%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
.shiki,
|
||||
.shiki span {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.dark .shiki,
|
||||
.dark .shiki span {
|
||||
color: var(--shiki-dark) !important;
|
||||
/* Optional, if you also want font styles */
|
||||
font-style: var(--shiki-dark-font-style) !important;
|
||||
font-weight: var(--shiki-dark-font-weight) !important;
|
||||
text-decoration: var(--shiki-dark-text-decoration) !important;
|
||||
}
|
||||
|
||||
/* Fix colors on auto-filled inputs */
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:hover,
|
||||
input:-webkit-autofill:focus,
|
||||
input:-webkit-autofill:active {
|
||||
/* Revert text color */
|
||||
-webkit-text-fill-color: hsl(var(--foreground)) !important;
|
||||
color: hsl(var(--foreground)) !important;
|
||||
caret-color: hsl(var(--foreground)) !important;
|
||||
|
||||
/* Revert background color */
|
||||
transition: background-color 5000s ease-in-out 0s;
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
.no-scrollbar::-webkit-scrollbar,
|
||||
.no-scrollbar *::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
/* Hide scrollbar for IE, Edge and Firefox */
|
||||
.no-scrollbar,
|
||||
.no-scrollbar * {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: hsl(var(--muted)) transparent;
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { metadataBase } from "@/lib/constants";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { AppSidebarLayout } from "./AppSidebar";
|
||||
import { Providers } from "./providers";
|
||||
import "./globals.css";
|
||||
import "@workspace/ui/global.css";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import NextTopLoader from "nextjs-toploader";
|
||||
import { Toaster } from "sonner";
|
||||
|
||||
@@ -3,35 +3,39 @@
|
||||
import { MoonIcon, SunIcon } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { ClientOnly } from "@/components/ClientOnly";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { setTheme, theme } = useTheme();
|
||||
|
||||
return (
|
||||
<div className="border-t pt-2">
|
||||
<ClientOnly
|
||||
ssr={<Skeleton className="size-[34px] rounded-full border bg-accent" />}
|
||||
>
|
||||
<Button
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
aria-label="Toggle theme"
|
||||
className="w-full text-muted-foreground hover:text-foreground px-2 py-1.5 text-left justify-start gap-2 capitalize h-auto font-normal"
|
||||
onClick={() => {
|
||||
setTheme(theme === "dark" ? "light" : "dark");
|
||||
}}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
>
|
||||
{theme === "light" ? (
|
||||
<SunIcon className="size-4" />
|
||||
) : (
|
||||
<MoonIcon className="size-4" />
|
||||
)}
|
||||
<ClientOnly ssr={<Skeleton className="size-4" />}>
|
||||
{theme === "light" ? (
|
||||
<SunIcon className="size-4" />
|
||||
) : (
|
||||
<MoonIcon className="size-4" />
|
||||
)}
|
||||
</ClientOnly>
|
||||
|
||||
{theme}
|
||||
</Button>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
<ClientOnly ssr={<Skeleton className="w-16 h-4" />}>
|
||||
<span>{theme}</span>
|
||||
</ClientOnly>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { ChevronDownIcon, ChevronRightIcon } from "lucide-react";
|
||||
import { ChevronRightIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useMemo, useState } from "react";
|
||||
import {
|
||||
@@ -126,6 +126,7 @@ function FullWidthSidebarLayoutInner(props: FullWidthSidebarLayoutProps) {
|
||||
links={footerSidebarLinks}
|
||||
fullPath={props.fullPath}
|
||||
/>
|
||||
<SidebarSeparator />
|
||||
<ThemeToggle />
|
||||
</SidebarFooter>
|
||||
)}
|
||||
@@ -289,7 +290,7 @@ function RenderSidebarSubmenu(props: {
|
||||
<DynamicHeight transition="height 200ms ease">
|
||||
<SidebarMenuItem>
|
||||
<Collapsible
|
||||
className="group/collapsible [&[data-state=open]>button>svg[data-chevron]]:rotate-180"
|
||||
className="group/collapsible [&[data-state=open]>button>svg[data-chevron]]:rotate-90"
|
||||
defaultOpen={open}
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
@@ -307,9 +308,9 @@ function RenderSidebarSubmenu(props: {
|
||||
<span>{props.subMenu.label}</span>
|
||||
|
||||
{sidebar.open && (
|
||||
<ChevronDownIcon
|
||||
<ChevronRightIcon
|
||||
data-chevron
|
||||
className="transition-transform ml-auto"
|
||||
className="transition-transform ml-auto text-foreground"
|
||||
/>
|
||||
)}
|
||||
</SidebarMenuButton>
|
||||
|
||||
@@ -1,63 +1,3 @@
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
defaultVariants: {
|
||||
size: "default",
|
||||
variant: "default",
|
||||
},
|
||||
variants: {
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
icon: "h-10 w-10",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
},
|
||||
variant: {
|
||||
default: "bg-foreground text-background hover:bg-foreground/90",
|
||||
destructive:
|
||||
"bg-destructive hover:bg-destructive/90 text-semibold text-destructive-foreground ",
|
||||
ghost: "hover:bg-accent text-semibold hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline text-semibold",
|
||||
outline:
|
||||
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground text-semibold",
|
||||
primary:
|
||||
"bg-primary hover:bg-primary/90 text-semibold text-primary-foreground ",
|
||||
secondary:
|
||||
"bg-secondary hover:bg-secondary/80 text-semibold text-secondary-foreground ",
|
||||
upsell:
|
||||
"bg-gradient-to-r from-purple-500 to-pink-500 text-white hover:from-purple-600 hover:to-pink-600 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5 transition-all duration-200",
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
const btnOnlyProps =
|
||||
Comp === "button" ? { type: "button" as const } : undefined;
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ className, size, variant }))}
|
||||
ref={ref}
|
||||
{...btnOnlyProps}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button, buttonVariants };
|
||||
"use client";
|
||||
export type { ButtonProps } from "@workspace/ui/components/button";
|
||||
export { Button, buttonVariants } from "@workspace/ui/components/button";
|
||||
|
||||
@@ -27,8 +27,8 @@ import { cn } from "@/lib/utils";
|
||||
|
||||
const SIDEBAR_COOKIE_NAME = "sidebar_state";
|
||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
||||
const SIDEBAR_WIDTH = "19rem";
|
||||
const SIDEBAR_WIDTH_MOBILE = "19rem";
|
||||
const SIDEBAR_WIDTH = "18rem";
|
||||
const SIDEBAR_WIDTH_MOBILE = "18rem";
|
||||
const SIDEBAR_WIDTH_ICON = "3rem";
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
|
||||
|
||||
|
||||
@@ -1,111 +1,13 @@
|
||||
import { fontFamily } from "tailwindcss/defaultTheme";
|
||||
import tailwindConfig from "@workspace/ui/tailwind.config";
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ["./src/**/*.{ts,tsx}"],
|
||||
darkMode: ["class"],
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
prefix: "",
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: {
|
||||
// 16px
|
||||
DEFAULT: "1rem",
|
||||
// 24px
|
||||
lg: "1.5rem",
|
||||
},
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
colors: {
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
"active-border": "hsl(var(--active-border))",
|
||||
background: "hsl(var(--background))",
|
||||
border: "hsl(var(--border))",
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
code: {
|
||||
DEFAULT: "hsl(var(--code))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
text: "hsl(var(--destructive-text))",
|
||||
},
|
||||
foreground: "hsl(var(--foreground))",
|
||||
input: "hsl(var(--input))",
|
||||
inverted: {
|
||||
DEFAULT: "hsl(var(--inverted))",
|
||||
foreground: "hsl(var(--inverted-foreground))",
|
||||
},
|
||||
link: {
|
||||
foreground: "hsl(var(--link-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
ring: "hsl(var(--ring))",
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
sidebar: {
|
||||
accent: "hsl(var(--sidebar-accent))",
|
||||
"accent-foreground": "hsl(var(--sidebar-accent-foreground))",
|
||||
border: "hsl(var(--sidebar-border))",
|
||||
DEFAULT: "hsl(var(--sidebar-background))",
|
||||
foreground: "hsl(var(--sidebar-foreground))",
|
||||
primary: "hsl(var(--sidebar-primary))",
|
||||
"primary-foreground": "hsl(var(--sidebar-primary-foreground))",
|
||||
ring: "hsl(var(--sidebar-ring))",
|
||||
},
|
||||
success: {
|
||||
text: "hsl(var(--success-text))",
|
||||
},
|
||||
warning: {
|
||||
text: "hsl(var(--warning-text))",
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
mono: ["var(--font-mono)", ...fontFamily.mono],
|
||||
sans: ["var(--font-sans)", ...fontFamily.sans],
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: { height: "0" },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: "0" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
const config: Config = {
|
||||
...tailwindConfig,
|
||||
content: [
|
||||
"./src/**/*.{ts,tsx}",
|
||||
// add contents of ui package
|
||||
"../../packages/ui/src/**/*.{ts,tsx}",
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
+9
-15
@@ -2,31 +2,25 @@
|
||||
"$schema": "https://unpkg.com/knip@5/schema.json",
|
||||
"entry": [
|
||||
"next.config.{js,ts,cjs,mjs}",
|
||||
"{instrumentation,middleware}.{js,ts}",
|
||||
"app/global-error.{js,jsx,ts,tsx}",
|
||||
"app/**/{error,layout,loading,not-found,page,template,default}.{js,jsx,ts,tsx,mdx}",
|
||||
"app/**/route.{js,jsx,ts,tsx}",
|
||||
"app/{manifest,sitemap,robots}.{js,ts}",
|
||||
"app/**/{icon,apple-icon}.{js,jsx,ts,tsx}",
|
||||
"app/**/{opengraph,twitter}-image.{js,jsx,ts,tsx}",
|
||||
"pages/**/*.{js,jsx,ts,tsx}",
|
||||
"src/{instrumentation,middleware}.{js,ts}",
|
||||
"src/app/global-error.{js,jsx,ts,tsx}",
|
||||
"src/app/**/{error,layout,loading,not-found,page,template,default}.{js,jsx,ts,tsx,mdx}",
|
||||
"src/app/**/route.{js,jsx,ts,tsx}",
|
||||
"src/app/{manifest,sitemap,robots}.{js,ts}",
|
||||
"src/app/**/{icon,apple-icon}.{js,jsx,ts,tsx}",
|
||||
"src/app/**/{opengraph,twitter}-image.{js,jsx,ts,tsx}",
|
||||
"src/pages/**/*.{js,jsx,ts,tsx}",
|
||||
"scripts/*.ts"
|
||||
],
|
||||
"ignore": ["src/components/ui/**", "src/icons/**", "src/components/others/Banner.tsx"],
|
||||
"ignore": [
|
||||
"src/components/ui/**",
|
||||
"src/icons/**",
|
||||
"src/components/others/Banner.tsx"
|
||||
],
|
||||
"ignoreBinaries": ["only-allow"],
|
||||
"ignoreDependencies": [
|
||||
"@thirdweb-dev/chains",
|
||||
"@thirdweb-dev/wallets",
|
||||
"thirdweb",
|
||||
"@types/flexsearch"
|
||||
"@workspace/ui",
|
||||
"@types/flexsearch",
|
||||
"@radix-ui/react-slot",
|
||||
"tailwindcss-animate"
|
||||
],
|
||||
"next": true,
|
||||
"project": ["src/**"]
|
||||
|
||||
@@ -47,6 +47,7 @@ const withMDX = createMDX({
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
transpilePackages: ["@workspace/ui"],
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
@@ -82,10 +83,6 @@ const nextConfig = {
|
||||
},
|
||||
];
|
||||
},
|
||||
webpack: (config) => {
|
||||
config.externals.push("pino-pretty", "lokijs", "encoding");
|
||||
return config;
|
||||
},
|
||||
};
|
||||
|
||||
export default withMDX(nextConfig);
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
"@types/tryghost__content-api": "^1.3.16",
|
||||
"@typescript-eslint/eslint-plugin": "7.14.1",
|
||||
"@typescript-eslint/parser": "7.14.1",
|
||||
"@workspace/ui": "workspace:*",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-biome": "1.9.4",
|
||||
|
||||
@@ -341,7 +341,7 @@ export function Header() {
|
||||
|
||||
{/* Mobile menu */}
|
||||
{showBurgerMenu && (
|
||||
<div className="fixed inset-0 top-sticky-top-height z-[50] overflow-auto bg-card p-6 xl:hidden">
|
||||
<div className="fixed inset-0 top-sticky-top-height z-50 overflow-auto bg-card p-6 xl:hidden">
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-4">
|
||||
<h3 className="font-semibold text-lg">Products</h3>
|
||||
|
||||
@@ -1,123 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
/* bg - neutral */
|
||||
--background: 0 0% 98%;
|
||||
--popover: 0 0% 100%;
|
||||
--card: 0 0% 100%;
|
||||
--secondary: 0 0% 90%;
|
||||
--muted: 0 0% 97%;
|
||||
--accent: 0 0% 93%;
|
||||
--inverted: 0 0% 4%;
|
||||
|
||||
/* bg - colorful */
|
||||
--primary: 221 83% 54%;
|
||||
--destructive: 360 72% 51%;
|
||||
|
||||
/* Text */
|
||||
--foreground: 0 0% 4%;
|
||||
--card-foreground: 0 0% 4%;
|
||||
--popover-foreground: 240 10% 4%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
--secondary-foreground: 0 0% 4%;
|
||||
--muted-foreground: 0 0% 40%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
--destructive-foreground: 0 0% 100%;
|
||||
--inverted-foreground: 0 0% 100%;
|
||||
--link-foreground: 221.21deg 83.19% 53.33%;
|
||||
--success-text: 142.09 70.56% 35.29%;
|
||||
--warning-text: 38 92% 40%;
|
||||
--destructive-text: 360 72% 60%;
|
||||
|
||||
/* Borders */
|
||||
--border: 0 0% 85%;
|
||||
--active-border: 0 0% 70%;
|
||||
--input: 0 0% 85%;
|
||||
--ring: 0 0% 80%;
|
||||
|
||||
/* Others */
|
||||
--radius: 0.5rem;
|
||||
|
||||
/* Sidebar */
|
||||
--sidebar-background: 0 0% 98%;
|
||||
--sidebar-foreground: 0 0% 4%;
|
||||
--sidebar-primary: 221 83% 54%;
|
||||
--sidebar-primary-foreground: 0 0% 100%;
|
||||
--sidebar-accent: 0 0% 93%;
|
||||
--sidebar-accent-foreground: 0 0% 9%;
|
||||
--sidebar-border: 0 0% 85%;
|
||||
--sidebar-ring: 0 0% 80%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
/* bg - neutral */
|
||||
--background: 0 0% 0%;
|
||||
--card: 0 0% 3.92%;
|
||||
--popover: 0 0% 0%;
|
||||
--secondary: 0 0% 11%;
|
||||
--muted: 0 0% 8%;
|
||||
--accent: 0 0% 11%;
|
||||
--inverted: 0 0% 100%;
|
||||
|
||||
/* bg - colorful */
|
||||
--primary: 221 83% 54%;
|
||||
--destructive: 360 72% 51%;
|
||||
|
||||
/* Text */
|
||||
--foreground: 0 0% 98%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted-foreground: 0 0% 63%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive-foreground: 0 0% 100%;
|
||||
--link-foreground: 215.88 100% 65%;
|
||||
--warning-text: 38 92% 50%;
|
||||
--destructive-text: 360 72% 55%;
|
||||
--success-text: 142 75% 50%;
|
||||
--inverted-foreground: 0 0% 0%;
|
||||
|
||||
/* Borders */
|
||||
--border: 0 0% 15%;
|
||||
--active-border: 0 0% 22%;
|
||||
--ring: 0 0% 30%;
|
||||
--input: 0 0% 15%;
|
||||
|
||||
/* sidebar */
|
||||
--sidebar-background: 0 0% 0%;
|
||||
--sidebar-foreground: 0 0% 98%;
|
||||
--sidebar-primary: 221 83% 54%;
|
||||
--sidebar-primary-foreground: 0 0% 100%;
|
||||
--sidebar-accent: 0 0% 11%;
|
||||
--sidebar-accent-foreground: 0 0% 98%;
|
||||
--sidebar-border: 0 0% 15%;
|
||||
--sidebar-ring: 0 0% 30%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.no-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
code span {
|
||||
color: var(--code-light-color);
|
||||
}
|
||||
@@ -131,43 +11,6 @@ code span {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.dark .shiki,
|
||||
.dark .shiki span {
|
||||
color: var(--shiki-dark) !important;
|
||||
/* Optional, if you also want font styles */
|
||||
font-style: var(--shiki-dark-font-style) !important;
|
||||
font-weight: var(--shiki-dark-font-weight) !important;
|
||||
text-decoration: var(--shiki-dark-text-decoration) !important;
|
||||
}
|
||||
|
||||
/* Fix colors on auto-filled inputs */
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:hover,
|
||||
input:-webkit-autofill:focus,
|
||||
input:-webkit-autofill:active {
|
||||
/* Revert text color */
|
||||
-webkit-text-fill-color: hsl(var(--foreground)) !important;
|
||||
color: hsl(var(--foreground)) !important;
|
||||
caret-color: hsl(var(--foreground)) !important;
|
||||
|
||||
/* Revert background color */
|
||||
transition: background-color 5000s ease-in-out 0s;
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
.no-scrollbar::-webkit-scrollbar,
|
||||
.no-scrollbar *::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
/* Hide scrollbar for IE, Edge and Firefox */
|
||||
.no-scrollbar,
|
||||
.no-scrollbar * {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
}
|
||||
|
||||
.styled-scrollbar::-webkit-scrollbar {
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "@workspace/ui/global.css";
|
||||
import "./globals.css";
|
||||
import { Fira_Code, Inter } from "next/font/google";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
|
||||
@@ -41,6 +41,10 @@ export const sidebar: SideBar = {
|
||||
href: `${paymentsSlug}/sell`,
|
||||
name: "Sell Tokens",
|
||||
},
|
||||
{
|
||||
href: `${paymentsSlug}/swap`,
|
||||
name: "Swap Tokens",
|
||||
},
|
||||
{
|
||||
href: `${paymentsSlug}/tokens`,
|
||||
name: "Get Token Prices",
|
||||
|
||||
@@ -0,0 +1,645 @@
|
||||
import {
|
||||
Callout,
|
||||
createMetadata,
|
||||
Tabs,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
TabsContent,
|
||||
ArticleIconCard,
|
||||
} from "@doc";
|
||||
import {
|
||||
ReactIcon,
|
||||
TypeScriptIcon,
|
||||
} from "@/icons";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
image: {
|
||||
title: "Swap Tokens",
|
||||
icon: "payments",
|
||||
},
|
||||
title: "Swap Tokens",
|
||||
description: "Build a custom token swap interface using thirdweb Payments.",
|
||||
});
|
||||
|
||||
# Swap Tokens
|
||||
|
||||
Build a custom token swap interface that allows users to exchange tokens across chains using thirdweb Payments.
|
||||
|
||||
<Tabs defaultValue="react">
|
||||
<TabsList>
|
||||
<TabsTrigger value="react" className="flex items-center gap-2 [&>p]:mb-0">
|
||||
<ReactIcon className="w-4 h-4 mr-2" />
|
||||
React
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="typescript" className="flex items-center gap-2 [&>p]:mb-0">
|
||||
<TypeScriptIcon className="w-4 h-4 mr-2" />
|
||||
TypeScript
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="react">
|
||||
|
||||
### Simple Swap Component
|
||||
|
||||
Create a basic swap interface with token selection and amount input:
|
||||
|
||||
```tsx
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Bridge } from "thirdweb";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import { createThirdwebClient } from "thirdweb";
|
||||
import { ethereum, polygon } from "thirdweb/chains";
|
||||
|
||||
const client = createThirdwebClient({
|
||||
clientId: "YOUR_CLIENT_ID",
|
||||
});
|
||||
|
||||
function SwapInterface() {
|
||||
const account = useActiveAccount();
|
||||
const [fromToken, setFromToken] = useState("ETH");
|
||||
const [toToken, setToToken] = useState("MATIC");
|
||||
const [amount, setAmount] = useState("");
|
||||
const [quote, setQuote] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const getQuote = async () => {
|
||||
if (!amount || !account) return;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const preparedQuote = await Bridge.Buy.prepare({
|
||||
originChainId: ethereum.id,
|
||||
originTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", // ETH
|
||||
destinationChainId: polygon.id,
|
||||
destinationTokenAddress: "0x0000000000000000000000000000000000001010", // MATIC
|
||||
amount: BigInt(amount * 1e18), // Convert to wei
|
||||
sender: account.address,
|
||||
receiver: account.address,
|
||||
client,
|
||||
});
|
||||
setQuote(preparedQuote);
|
||||
} catch (error) {
|
||||
console.error("Failed to get quote:", error);
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (amount && account) {
|
||||
const timer = setTimeout(getQuote, 500);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [amount, account]);
|
||||
|
||||
const executeSwap = async () => {
|
||||
if (!quote || !account) return;
|
||||
|
||||
try {
|
||||
for (const step of quote.steps) {
|
||||
for (const transaction of step.transactions) {
|
||||
const result = await sendAndConfirmTransaction({
|
||||
transaction,
|
||||
account,
|
||||
});
|
||||
|
||||
// Wait for cross-chain completion if needed
|
||||
if (["buy", "sell", "transfer"].includes(transaction.action)) {
|
||||
let swapStatus;
|
||||
do {
|
||||
swapStatus = await Bridge.status({
|
||||
transactionHash: result.transactionHash,
|
||||
chainId: transaction.chainId,
|
||||
client,
|
||||
});
|
||||
if (swapStatus.status === "PENDING") {
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
}
|
||||
} while (swapStatus.status === "PENDING");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
alert("Swap completed successfully!");
|
||||
setQuote(null);
|
||||
setAmount("");
|
||||
} catch (error) {
|
||||
console.error("Swap failed:", error);
|
||||
alert("Swap failed. Please try again.");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-md mx-auto p-6 bg-white rounded-lg shadow-lg">
|
||||
<h2 className="text-2xl font-bold mb-6 text-center">Token Swap</h2>
|
||||
|
||||
{/* From Token */}
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium mb-2">From</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="number"
|
||||
placeholder="0.0"
|
||||
value={amount}
|
||||
onChange={(e) => setAmount(e.target.value)}
|
||||
className="flex-1 p-3 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
<select
|
||||
value={fromToken}
|
||||
onChange={(e) => setFromToken(e.target.value)}
|
||||
className="p-3 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="ETH">ETH</option>
|
||||
<option value="USDC">USDC</option>
|
||||
<option value="WETH">WETH</option>
|
||||
</select>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mt-1">Ethereum</p>
|
||||
</div>
|
||||
|
||||
{/* Swap Arrow */}
|
||||
<div className="flex justify-center mb-4">
|
||||
<button className="p-2 rounded-full bg-gray-100 hover:bg-gray-200">
|
||||
↓
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* To Token */}
|
||||
<div className="mb-6">
|
||||
<label className="block text-sm font-medium mb-2">To</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="0.0"
|
||||
value={quote ? (Number(quote.destinationAmount) / 1e18).toFixed(6) : ""}
|
||||
readOnly
|
||||
className="flex-1 p-3 border rounded-lg bg-gray-50"
|
||||
/>
|
||||
<select
|
||||
value={toToken}
|
||||
onChange={(e) => setToToken(e.target.value)}
|
||||
className="p-3 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="MATIC">MATIC</option>
|
||||
<option value="USDC">USDC</option>
|
||||
<option value="WETH">WETH</option>
|
||||
</select>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mt-1">Polygon</p>
|
||||
</div>
|
||||
|
||||
{/* Quote Info */}
|
||||
{quote && (
|
||||
<div className="mb-4 p-3 bg-blue-50 rounded-lg">
|
||||
<p className="text-sm">
|
||||
<span className="font-medium">Rate:</span> 1 {fromToken} = {(Number(quote.destinationAmount) / Number(quote.originAmount)).toFixed(6)} {toToken}
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
<span className="font-medium">Fee:</span> {((Number(quote.originAmount) - Number(amount) * 1e18) / 1e18).toFixed(6)} {fromToken}
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
<span className="font-medium">Estimated time:</span> {Math.round(quote.estimatedExecutionTimeMs / 1000)}s
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Swap Button */}
|
||||
<button
|
||||
onClick={executeSwap}
|
||||
disabled={!quote || isLoading || !account}
|
||||
className="w-full py-3 px-4 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed"
|
||||
>
|
||||
{!account ? "Connect Wallet" : isLoading ? "Getting Quote..." : "Swap"}
|
||||
</button>
|
||||
|
||||
{!account && (
|
||||
<p className="text-center text-sm text-gray-500 mt-3">
|
||||
Connect your wallet to start swapping
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced Swap with Route Selection
|
||||
|
||||
Build a more sophisticated swap interface with multiple route options:
|
||||
|
||||
```tsx
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Bridge } from "thirdweb";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
|
||||
function AdvancedSwapInterface() {
|
||||
const account = useActiveAccount();
|
||||
const [routes, setRoutes] = useState([]);
|
||||
const [selectedRoute, setSelectedRoute] = useState(null);
|
||||
const [quotes, setQuotes] = useState([]);
|
||||
|
||||
// Get available routes
|
||||
const getRoutes = async () => {
|
||||
try {
|
||||
const availableRoutes = await Bridge.routes({
|
||||
originChainId: 1, // Ethereum
|
||||
originTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
|
||||
client,
|
||||
});
|
||||
setRoutes(availableRoutes.slice(0, 5)); // Show top 5 routes
|
||||
} catch (error) {
|
||||
console.error("Failed to get routes:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Get quotes for multiple routes
|
||||
const getQuotes = async (amount) => {
|
||||
if (!amount || !account) return;
|
||||
|
||||
const quotePromises = routes.map(async (route) => {
|
||||
try {
|
||||
const quote = await Bridge.Buy.prepare({
|
||||
originChainId: route.originToken.chainId,
|
||||
originTokenAddress: route.originToken.address,
|
||||
destinationChainId: route.destinationToken.chainId,
|
||||
destinationTokenAddress: route.destinationToken.address,
|
||||
amount: BigInt(amount * 1e18),
|
||||
sender: account.address,
|
||||
receiver: account.address,
|
||||
client,
|
||||
});
|
||||
return { route, quote };
|
||||
} catch (error) {
|
||||
return { route, error };
|
||||
}
|
||||
});
|
||||
|
||||
const results = await Promise.all(quotePromises);
|
||||
setQuotes(results.filter(result => result.quote));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getRoutes();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto p-6">
|
||||
<h2 className="text-2xl font-bold mb-6">Advanced Token Swap</h2>
|
||||
|
||||
{/* Amount Input */}
|
||||
<div className="mb-6">
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Enter amount to swap"
|
||||
onChange={(e) => getQuotes(e.target.value)}
|
||||
className="w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Route Options */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-lg font-medium">Available Routes</h3>
|
||||
{quotes.map(({ route, quote }, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="border rounded-lg p-4 hover:bg-gray-50 cursor-pointer"
|
||||
onClick={() => setSelectedRoute({ route, quote })}
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<p className="font-medium">
|
||||
{route.originToken.symbol} → {route.destinationToken.symbol}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
{route.originToken.name} to {route.destinationToken.name}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="font-medium">
|
||||
{(Number(quote.destinationAmount) / Math.pow(10, route.destinationToken.decimals)).toFixed(6)}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
~{Math.round(quote.estimatedExecutionTimeMs / 1000)}s
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{selectedRoute && (
|
||||
<div className="mt-6 p-4 bg-blue-50 rounded-lg">
|
||||
<h4 className="font-medium mb-2">Selected Route</h4>
|
||||
<p>{selectedRoute.route.originToken.symbol} → {selectedRoute.route.destinationToken.symbol}</p>
|
||||
<button
|
||||
onClick={() => executeSwap(selectedRoute.quote)}
|
||||
className="mt-3 w-full py-2 px-4 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
||||
>
|
||||
Execute Swap
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="typescript">
|
||||
|
||||
### Core Swap Logic
|
||||
|
||||
Create the core swap functionality that can be integrated into any UI:
|
||||
|
||||
```typescript
|
||||
import { Bridge, sendAndConfirmTransaction, type Account } from "thirdweb";
|
||||
|
||||
interface SwapParams {
|
||||
fromChainId: number;
|
||||
fromTokenAddress: string;
|
||||
toChainId: number;
|
||||
toTokenAddress: string;
|
||||
amount: bigint;
|
||||
userAddress: string;
|
||||
client: any;
|
||||
}
|
||||
|
||||
interface SwapQuote {
|
||||
originAmount: bigint;
|
||||
destinationAmount: bigint;
|
||||
estimatedExecutionTimeMs: number;
|
||||
steps: any[];
|
||||
}
|
||||
|
||||
export class SwapManager {
|
||||
private client: any;
|
||||
|
||||
constructor(client: any) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
async getQuote(params: SwapParams): Promise<SwapQuote> {
|
||||
try {
|
||||
const quote = await Bridge.Buy.prepare({
|
||||
originChainId: params.fromChainId,
|
||||
originTokenAddress: params.fromTokenAddress,
|
||||
destinationChainId: params.toChainId,
|
||||
destinationTokenAddress: params.toTokenAddress,
|
||||
amount: params.amount,
|
||||
sender: params.userAddress,
|
||||
receiver: params.userAddress,
|
||||
client: this.client,
|
||||
});
|
||||
|
||||
return {
|
||||
originAmount: quote.originAmount,
|
||||
destinationAmount: quote.destinationAmount,
|
||||
estimatedExecutionTimeMs: quote.estimatedExecutionTimeMs,
|
||||
steps: quote.steps,
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get swap quote: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async executeSwap(quote: SwapQuote, account: Account): Promise<string[]> {
|
||||
const transactionHashes: string[] = [];
|
||||
|
||||
try {
|
||||
for (const step of quote.steps) {
|
||||
for (const transaction of step.transactions) {
|
||||
const result = await sendAndConfirmTransaction({
|
||||
transaction,
|
||||
account,
|
||||
});
|
||||
|
||||
transactionHashes.push(result.transactionHash);
|
||||
|
||||
// Wait for cross-chain completion if needed
|
||||
if (["buy", "sell", "transfer"].includes(transaction.action)) {
|
||||
await this.waitForCompletion(result.transactionHash, transaction.chainId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return transactionHashes;
|
||||
} catch (error) {
|
||||
throw new Error(`Swap execution failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async waitForCompletion(transactionHash: string, chainId: number): Promise<void> {
|
||||
let attempts = 0;
|
||||
const maxAttempts = 200; // 10 minutes max wait
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
try {
|
||||
const status = await Bridge.status({
|
||||
transactionHash,
|
||||
chainId,
|
||||
client: this.client,
|
||||
});
|
||||
|
||||
if (status.status === "COMPLETED") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (status.status === "FAILED") {
|
||||
throw new Error("Cross-chain transaction failed");
|
||||
}
|
||||
|
||||
// Wait 3 seconds before checking again
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
attempts++;
|
||||
} catch (error) {
|
||||
attempts++;
|
||||
if (attempts >= maxAttempts) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Transaction timeout - please check status manually");
|
||||
}
|
||||
|
||||
async getAvailableRoutes(params: {
|
||||
originChainId?: number;
|
||||
originTokenAddress?: string;
|
||||
destinationChainId?: number;
|
||||
destinationTokenAddress?: string;
|
||||
limit?: number;
|
||||
}) {
|
||||
try {
|
||||
return await Bridge.routes({
|
||||
...params,
|
||||
client: this.client,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get routes: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Usage Example
|
||||
|
||||
```typescript
|
||||
import { createThirdwebClient } from "thirdweb";
|
||||
import { ethereum, polygon } from "thirdweb/chains";
|
||||
|
||||
const client = createThirdwebClient({
|
||||
clientId: "YOUR_CLIENT_ID",
|
||||
});
|
||||
|
||||
const swapManager = new SwapManager(client);
|
||||
|
||||
// Example: Swap ETH to MATIC
|
||||
async function swapEthToMatic(amount: string, userAccount: Account) {
|
||||
try {
|
||||
// Get quote
|
||||
const quote = await swapManager.getQuote({
|
||||
fromChainId: ethereum.id,
|
||||
fromTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", // ETH
|
||||
toChainId: polygon.id,
|
||||
toTokenAddress: "0x0000000000000000000000000000000000001010", // MATIC
|
||||
amount: BigInt(parseFloat(amount) * 1e18),
|
||||
userAddress: userAccount.address,
|
||||
client,
|
||||
});
|
||||
|
||||
console.log("Quote:", {
|
||||
fromAmount: Number(quote.originAmount) / 1e18,
|
||||
toAmount: Number(quote.destinationAmount) / 1e18,
|
||||
estimatedTime: quote.estimatedExecutionTimeMs / 1000,
|
||||
});
|
||||
|
||||
// Execute swap
|
||||
const txHashes = await swapManager.executeSwap(quote, userAccount);
|
||||
console.log("Swap completed! Transaction hashes:", txHashes);
|
||||
|
||||
return txHashes;
|
||||
} catch (error) {
|
||||
console.error("Swap failed:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced Price Comparison
|
||||
|
||||
Compare prices across multiple routes to find the best deal:
|
||||
|
||||
```typescript
|
||||
interface RouteComparison {
|
||||
route: any;
|
||||
quote: SwapQuote;
|
||||
priceImpact: number;
|
||||
gasEstimate: bigint;
|
||||
}
|
||||
|
||||
export class PriceComparator {
|
||||
private swapManager: SwapManager;
|
||||
|
||||
constructor(swapManager: SwapManager) {
|
||||
this.swapManager = swapManager;
|
||||
}
|
||||
|
||||
async compareRoutes(
|
||||
fromToken: { chainId: number; address: string },
|
||||
amount: bigint,
|
||||
userAddress: string
|
||||
): Promise<RouteComparison[]> {
|
||||
// Get available routes
|
||||
const routes = await this.swapManager.getAvailableRoutes({
|
||||
originChainId: fromToken.chainId,
|
||||
originTokenAddress: fromToken.address,
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
// Get quotes for each route
|
||||
const comparisons: RouteComparison[] = [];
|
||||
|
||||
for (const route of routes) {
|
||||
try {
|
||||
const quote = await this.swapManager.getQuote({
|
||||
fromChainId: route.originToken.chainId,
|
||||
fromTokenAddress: route.originToken.address,
|
||||
toChainId: route.destinationToken.chainId,
|
||||
toTokenAddress: route.destinationToken.address,
|
||||
amount,
|
||||
userAddress,
|
||||
client: this.swapManager.client,
|
||||
});
|
||||
|
||||
const priceImpact = this.calculatePriceImpact(quote, amount);
|
||||
const gasEstimate = this.estimateGasCosts(quote);
|
||||
|
||||
comparisons.push({
|
||||
route,
|
||||
quote,
|
||||
priceImpact,
|
||||
gasEstimate,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn(`Failed to get quote for route ${route.originToken.symbol} -> ${route.destinationToken.symbol}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by best output amount
|
||||
return comparisons.sort((a, b) =>
|
||||
Number(b.quote.destinationAmount) - Number(a.quote.destinationAmount)
|
||||
);
|
||||
}
|
||||
|
||||
private calculatePriceImpact(quote: SwapQuote, inputAmount: bigint): number {
|
||||
// Simplified price impact calculation
|
||||
const ratio = Number(quote.destinationAmount) / Number(quote.originAmount);
|
||||
return Math.abs(1 - ratio) * 100; // Percentage
|
||||
}
|
||||
|
||||
private estimateGasCosts(quote: SwapQuote): bigint {
|
||||
// Sum up gas estimates from all transactions
|
||||
return quote.steps.reduce((total, step) => {
|
||||
return total + step.transactions.reduce((stepTotal: bigint, tx: any) => {
|
||||
return stepTotal + (tx.gasLimit || 200000n); // Default estimate
|
||||
}, 0n);
|
||||
}, 0n);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
## View it in action:
|
||||
|
||||
<ArticleIconCard
|
||||
title="Token Swap Playground"
|
||||
description="Try building and testing swap interfaces with live data"
|
||||
icon={ReactIcon}
|
||||
href="https://playground.thirdweb.com/connect/pay"
|
||||
/>
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Cross-chain swaps** - Exchange tokens across 50+ supported chains
|
||||
- **Real-time quotes** - Get up-to-date pricing and execution estimates
|
||||
- **Route optimization** - Find the best path for any token pair
|
||||
- **Price comparison** - Compare multiple routes to maximize output
|
||||
- **Status tracking** - Monitor cross-chain transaction progress
|
||||
|
||||
<Callout variant="info">
|
||||
**Gas Optimization Tip:** For frequently traded pairs, consider caching route data to reduce API calls and improve UI responsiveness.
|
||||
</Callout>
|
||||
|
||||
## Going Further
|
||||
|
||||
- [Send a Payment](/payments/send) - Learn about peer-to-peer transfers
|
||||
- [Get Routes](/payments/routes) - Explore route discovery APIs
|
||||
- [Token Prices](/payments/tokens) - Access real-time token pricing
|
||||
- [Webhooks](/payments/webhooks) - Get notified when swaps complete
|
||||
|
||||
## API Reference
|
||||
|
||||
- [Bridge.Buy.prepare](/references/typescript/v5/buy/prepare)
|
||||
- [Bridge.routes](/references/typescript/v5/routes)
|
||||
- [Bridge.status](/references/typescript/v5/status)
|
||||
@@ -30,7 +30,7 @@ export function RenderCode(props: {
|
||||
/>
|
||||
</ScrollShadow>
|
||||
<CopyButton
|
||||
className="absolute top-4 right-4 z-[10] border bg-background p-2 opacity-0 transition-opacity duration-300 group-hover:opacity-100"
|
||||
className="absolute top-4 right-4 z-10 border bg-background p-2 opacity-0 transition-opacity duration-300 group-hover:opacity-100"
|
||||
iconClassName="size-3"
|
||||
text={props.code}
|
||||
/>
|
||||
|
||||
@@ -1,62 +1,3 @@
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
defaultVariants: {
|
||||
size: "default",
|
||||
variant: "default",
|
||||
},
|
||||
variants: {
|
||||
size: {
|
||||
default: "h-8 px-4 py-2 text-sm",
|
||||
icon: "size-10",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
sm: "h-7 rounded-md px-3 text-sm",
|
||||
},
|
||||
variant: {
|
||||
default: "bg-foreground text-background hover:bg-foreground/90",
|
||||
destructive:
|
||||
"bg-destructive hover:bg-destructive/90 text-destructive-foreground ",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
outline:
|
||||
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
|
||||
primary: "bg-primary hover:bg-primary/90 text-primary-foreground ",
|
||||
secondary:
|
||||
"bg-secondary hover:bg-secondary/80 text-secondary-foreground ",
|
||||
upsell:
|
||||
"bg-gradient-to-r from-purple-500 to-pink-500 text-white hover:from-purple-600 hover:to-pink-600 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5 transition-all duration-200",
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
const btnOnlyProps =
|
||||
Comp === "button" ? { type: "button" as const } : undefined;
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ className, size, variant }))}
|
||||
ref={ref}
|
||||
{...btnOnlyProps}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button, buttonVariants };
|
||||
"use client";
|
||||
export type { ButtonProps } from "@workspace/ui/components/button";
|
||||
export { Button, buttonVariants } from "@workspace/ui/components/button";
|
||||
|
||||
+28
-114
@@ -1,130 +1,44 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
import tailwindConfig from "@workspace/ui/tailwind.config";
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
const config: Config = {
|
||||
...tailwindConfig,
|
||||
content: [
|
||||
"./components/**/*.{ts,tsx}",
|
||||
"./app/**/*.{ts,tsx,mdx}",
|
||||
"./src/**/*.{ts,tsx,mdx}",
|
||||
// add contents of ui package
|
||||
"../../packages/ui/src/**/*.{ts,tsx}",
|
||||
],
|
||||
darkMode: ["class"],
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "1rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
...tailwindConfig.theme,
|
||||
extend: {
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
"animate-in-slow": "animate-in 0.4s ease",
|
||||
"text-shimmer": "text-shimmer 1.25s linear infinite",
|
||||
},
|
||||
colors: {
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
"active-border": "hsl(var(--active-border))",
|
||||
background: "hsl(var(--background))",
|
||||
border: "hsl(var(--border))",
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
current: "currentColor",
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
// destructive-foreground should only be used on destructive bg
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
// destructive-text can be used on neutral bg - it's red-ish
|
||||
text: "hsl(var(--destructive-text))",
|
||||
},
|
||||
foreground: "hsl(var(--foreground))",
|
||||
input: "hsl(var(--input))",
|
||||
inverted: {
|
||||
DEFAULT: "hsl(var(--inverted))",
|
||||
foreground: "hsl(var(--inverted-foreground))",
|
||||
},
|
||||
link: {
|
||||
foreground: "hsl(var(--link-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
overlay: "hsl(var(--overlay))",
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
ring: "hsl(var(--ring))",
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
success: {
|
||||
// success-text can be used on neutral bg, it's green-ish
|
||||
text: "hsl(var(--success-text))",
|
||||
},
|
||||
transparent: "transparent",
|
||||
warning: {
|
||||
text: "hsl(var(--warning-text))",
|
||||
},
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: { height: 0, opacity: 0 },
|
||||
to: { height: "var(--radix-accordion-content-height)", opacity: 1 },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)", opacity: 1 },
|
||||
to: { height: 0, opacity: 0 },
|
||||
},
|
||||
"text-shimmer": {
|
||||
"0%": { backgroundPosition: "100% 50%" },
|
||||
"100%": { backgroundPosition: "-100% 50%" },
|
||||
},
|
||||
},
|
||||
...tailwindConfig.theme?.extend,
|
||||
spacing: {
|
||||
"offset-top": "calc(var(--sticky-top-height) + 18px)",
|
||||
"offset-top-mobile": "calc(var(--sticky-top-height) + 100px)",
|
||||
"sidebar-height": "calc(100vh - var(--sticky-top-height))",
|
||||
"sticky-top-height": "var(--sticky-top-height)",
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
mono: ["var(--font-mono)", "monospace"],
|
||||
sans: ["var(--font-sans)", "sans-serif"],
|
||||
},
|
||||
|
||||
zIndex: {
|
||||
// base
|
||||
base: 0,
|
||||
//
|
||||
codeToken: 5,
|
||||
codeTokenHighlight: 1,
|
||||
//
|
||||
copyCodeButton: 20,
|
||||
dropdownMenu: 1200,
|
||||
floatingButton: 1000,
|
||||
menu: 1100,
|
||||
modal: 1400,
|
||||
modalOverlay: 1300,
|
||||
stickyMobileSidebar: 500,
|
||||
//
|
||||
stickyTop: 1000,
|
||||
zIndex: {
|
||||
// base
|
||||
base: "0",
|
||||
//
|
||||
codeToken: "5",
|
||||
codeTokenHighlight: "1",
|
||||
//
|
||||
copyCodeButton: "20",
|
||||
dropdownMenu: "1200",
|
||||
floatingButton: "1000",
|
||||
menu: "1100",
|
||||
modal: "1400",
|
||||
modalOverlay: "1300",
|
||||
stickyMobileSidebar: "500",
|
||||
//
|
||||
stickyTop: "1000",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -74,10 +74,6 @@ const nextConfig = {
|
||||
},
|
||||
];
|
||||
},
|
||||
webpack: (config) => {
|
||||
config.externals.push("pino-pretty", "lokijs", "encoding");
|
||||
return config;
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @thirdweb-dev/nebula
|
||||
|
||||
## 0.2.34
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`7f11657`](https://github.com/thirdweb-dev/js/commit/7f116577aba529e21a48163b295b091f9d6008f7), [`467ba50`](https://github.com/thirdweb-dev/js/commit/467ba5090729dd951970b15ef320caf9d058cc0b), [`d227bc3`](https://github.com/thirdweb-dev/js/commit/d227bc389bef4b2f84544a73732a61782e3dca16)]:
|
||||
- thirdweb@5.105.34
|
||||
|
||||
## 0.2.33
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -57,5 +57,5 @@
|
||||
"type": "module",
|
||||
"types": "./dist/types/exports/thirdweb.d.ts",
|
||||
"typings": "./dist/types/exports/thirdweb.d.ts",
|
||||
"version": "0.2.33"
|
||||
"version": "0.2.34"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# thirdweb
|
||||
|
||||
## 5.105.34
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#7858](https://github.com/thirdweb-dev/js/pull/7858) [`7f11657`](https://github.com/thirdweb-dev/js/commit/7f116577aba529e21a48163b295b091f9d6008f7) Thanks [@MananTank](https://github.com/MananTank)! - Fix PayEmbed UI when mode prop is not specified - Default to mode: "fund_wallet" with amount: "0.01" and chain: ethereum
|
||||
|
||||
- [#7865](https://github.com/thirdweb-dev/js/pull/7865) [`467ba50`](https://github.com/thirdweb-dev/js/commit/467ba5090729dd951970b15ef320caf9d058cc0b) Thanks [@MananTank](https://github.com/MananTank)! - Add missing props for details modal on `useWalletDetailsModal` hook
|
||||
|
||||
- [#7321](https://github.com/thirdweb-dev/js/pull/7321) [`d227bc3`](https://github.com/thirdweb-dev/js/commit/d227bc389bef4b2f84544a73732a61782e3dca16) Thanks [@kumaryash90](https://github.com/kumaryash90)! - ERC20 assets
|
||||
|
||||
## 5.105.33
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -195,6 +195,11 @@
|
||||
"import": "./dist/esm/exports/storage.js",
|
||||
"default": "./dist/cjs/exports/storage.js"
|
||||
},
|
||||
"./tokens": {
|
||||
"types": "./dist/types/exports/tokens.d.ts",
|
||||
"import": "./dist/esm/exports/tokens.js",
|
||||
"default": "./dist/cjs/exports/tokens.js"
|
||||
},
|
||||
"./transaction": {
|
||||
"types": "./dist/types/exports/transaction.d.ts",
|
||||
"import": "./dist/esm/exports/transaction.js",
|
||||
@@ -391,6 +396,9 @@
|
||||
"storage": [
|
||||
"./dist/types/exports/storage.d.ts"
|
||||
],
|
||||
"tokens": [
|
||||
"./dist/types/exports/tokens.d.ts"
|
||||
],
|
||||
"transaction": [
|
||||
"./dist/types/exports/transaction.d.ts"
|
||||
],
|
||||
@@ -409,5 +417,5 @@
|
||||
}
|
||||
},
|
||||
"typings": "./dist/types/exports/thirdweb.d.ts",
|
||||
"version": "5.105.33"
|
||||
"version": "5.105.34"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
[
|
||||
"constructor(address owner, address manager)",
|
||||
"function cancelOwnershipHandover() payable",
|
||||
"function completeOwnershipHandover(address pendingOwner) payable",
|
||||
"function computeDeployAddress(uint8 deployType, bytes bytecode, bytes constructorArgs, bytes32 salt) view returns (address)",
|
||||
"function computeProxyAddress(uint8 deployType, address implementation, bytes data, bytes32 salt) view returns (address)",
|
||||
"function deployContract(bytes32 id, uint8 deployType, bytes bytecode, bytes constructorArgs, bytes32 salt, bytes[] postDeployCalls) returns (address deployed)",
|
||||
"function deployContractWithSignature((bytes32 id, uint8 deployType, bytes bytecode, bytes constructorArgs, bytes32 salt, bytes[] postDeployCalls, uint256 nonce, uint256 deadline) request, bytes signature) returns (address deployed)",
|
||||
"function deployProxy(bytes32 id, uint8 deployType, address implementation, bytes data, bytes32 salt, bytes[] postDeployCalls) returns (address deployed)",
|
||||
"function deployProxyWithSignature((bytes32 id, uint8 deployType, address implementation, bytes data, bytes32 salt, bytes[] postDeployCalls, uint256 nonce, uint256 deadline) request, bytes signature) returns (address deployed)",
|
||||
"function getDeployment(bytes32 id, uint256 version) view returns (address)",
|
||||
"function grantRoles(address user, uint256 roles) payable",
|
||||
"function hasAllRoles(address user, uint256 roles) view returns (bool)",
|
||||
"function hasAnyRole(address user, uint256 roles) view returns (bool)",
|
||||
"function owner() view returns (address result)",
|
||||
"function ownershipHandoverExpiresAt(address pendingOwner) view returns (uint256 result)",
|
||||
"function renounceOwnership() payable",
|
||||
"function renounceRoles(uint256 roles) payable",
|
||||
"function requestOwnershipHandover() payable",
|
||||
"function revokeRoles(address user, uint256 roles) payable",
|
||||
"function rolesOf(address user) view returns (uint256 roles)",
|
||||
"function transferOwnership(address newOwner) payable",
|
||||
"event ContractDeployed(bytes32 id, uint256 version, address contractAddress, bytes32 salt)",
|
||||
"event NonceUsed(address account, uint256 nonce)",
|
||||
"event OwnershipHandoverCanceled(address indexed pendingOwner)",
|
||||
"event OwnershipHandoverRequested(address indexed pendingOwner)",
|
||||
"event OwnershipTransferred(address indexed oldOwner, address indexed newOwner)",
|
||||
"event ProxyDeployed(bytes32 id, uint256 version, address proxyAddress, address implementation, bytes32 salt)",
|
||||
"event RolesUpdated(address indexed user, uint256 indexed roles)",
|
||||
"error AlreadyInitialized()",
|
||||
"error DeploymentFailed()",
|
||||
"error InvalidDeploymentType()",
|
||||
"error InvalidImplementation()",
|
||||
"error InvalidNonce()",
|
||||
"error InvalidSignature()",
|
||||
"error NewOwnerIsZeroAddress()",
|
||||
"error NoHandoverRequest()",
|
||||
"error SignatureExpired()",
|
||||
"error Unauthorized()"
|
||||
]
|
||||
@@ -0,0 +1,49 @@
|
||||
[
|
||||
"constructor()",
|
||||
"function DOMAIN_SEPARATOR() view returns (bytes32 result)",
|
||||
"function allowance(address owner, address spender) view returns (uint256 result)",
|
||||
"function approve(address spender, uint256 amount) returns (bool)",
|
||||
"function balanceOf(address owner) view returns (uint256 result)",
|
||||
"function burn(uint256 amount)",
|
||||
"function burnFrom(address from, uint256 amount)",
|
||||
"function cancelOwnershipHandover() payable",
|
||||
"function completeOwnershipHandover(address pendingOwner) payable",
|
||||
"function contractURI() view returns (string)",
|
||||
"function decimals() view returns (uint8)",
|
||||
"function initialize(string name, string symbol, string contractURI, uint256 maxSupply, address owner)",
|
||||
"function name() view returns (string)",
|
||||
"function nonces(address owner) view returns (uint256 result)",
|
||||
"function owner() view returns (address result)",
|
||||
"function ownershipHandoverExpiresAt(address pendingOwner) view returns (uint256 result)",
|
||||
"function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)",
|
||||
"function renounceOwnership() payable",
|
||||
"function requestOwnershipHandover() payable",
|
||||
"function setContractURI(string _contractURI)",
|
||||
"function supportsInterface(bytes4 interfaceId) pure returns (bool)",
|
||||
"function symbol() view returns (string)",
|
||||
"function totalSupply() view returns (uint256 result)",
|
||||
"function transfer(address to, uint256 amount) returns (bool)",
|
||||
"function transferFrom(address from, address to, uint256 amount) returns (bool)",
|
||||
"function transferOwnership(address newOwner) payable",
|
||||
"event Approval(address indexed owner, address indexed spender, uint256 amount)",
|
||||
"event ContractURIUpdated()",
|
||||
"event Initialized(uint64 version)",
|
||||
"event OwnershipHandoverCanceled(address indexed pendingOwner)",
|
||||
"event OwnershipHandoverRequested(address indexed pendingOwner)",
|
||||
"event OwnershipTransferred(address indexed oldOwner, address indexed newOwner)",
|
||||
"event Transfer(address indexed from, address indexed to, uint256 amount)",
|
||||
"error AllowanceOverflow()",
|
||||
"error AllowanceUnderflow()",
|
||||
"error AlreadyInitialized()",
|
||||
"error InsufficientAllowance()",
|
||||
"error InsufficientBalance()",
|
||||
"error InvalidInitialization()",
|
||||
"error InvalidPermit()",
|
||||
"error NewOwnerIsZeroAddress()",
|
||||
"error NoHandoverRequest()",
|
||||
"error NotInitializing()",
|
||||
"error Permit2AllowanceIsFixedAtInfinity()",
|
||||
"error PermitExpired()",
|
||||
"error TotalSupplyOverflow()",
|
||||
"error Unauthorized()"
|
||||
]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user