From 8702a49cc7d5b0bde62cdc8b12fad91070ca00ad Mon Sep 17 00:00:00 2001 From: Manan Tank Date: Mon, 16 Jun 2025 20:27:39 +0530 Subject: [PATCH 01/35] [TOOL-4689] Dashboard: Integrate ERC20Asset contract in token creation flow --- .../tables/contract-table.tsx | 1 + apps/dashboard/src/@/components/ui/tabs.tsx | 8 +- .../src/@/hooks/project-contracts.ts | 2 +- .../tokens/create/_common/step-card.tsx | 2 +- .../nft-collection-info-fieldset.tsx | 3 +- .../tokens/create/nft/create-nft-page-ui.tsx | 11 +- .../tokens/create/token/_common/form.ts | 29 +- .../create/token/create-token-page-impl.tsx | 231 +++-------- .../create/token/create-token-page.client.tsx | 31 +- .../token/create-token-page.stories.tsx | 17 +- .../token/distribution/token-airdrop.tsx | 4 +- .../create/token/distribution/token-sale.tsx | 377 ++++++++++++++---- .../create/token/launch/launch-token.tsx | 14 - .../token/token-info/token-info-fieldset.tsx | 3 +- 14 files changed, 418 insertions(+), 315 deletions(-) diff --git a/apps/dashboard/src/@/components/contract-components/tables/contract-table.tsx b/apps/dashboard/src/@/components/contract-components/tables/contract-table.tsx index 27ba135d3fd..e608e879c03 100644 --- a/apps/dashboard/src/@/components/contract-components/tables/contract-table.tsx +++ b/apps/dashboard/src/@/components/contract-components/tables/contract-table.tsx @@ -300,6 +300,7 @@ const contractTypeToAssetTypeRecord: Record = { DropERC20: "Coin", DropERC721: "NFT Collection", DropERC1155: "NFT Collection", + ERC20Asset: "Coin", }; const NetworkFilterCell = React.memo(function NetworkFilterCell({ diff --git a/apps/dashboard/src/@/components/ui/tabs.tsx b/apps/dashboard/src/@/components/ui/tabs.tsx index 47acea12b5d..a2239f181ef 100644 --- a/apps/dashboard/src/@/components/ui/tabs.tsx +++ b/apps/dashboard/src/@/components/ui/tabs.tsx @@ -98,6 +98,7 @@ export function TabButtons(props: { shadowColor?: string; tabIconClassName?: string; hideBottomLine?: boolean; + bottomLineClassName?: string; }) { const { containerRef, lineRef, activeTabRef } = useUnderline(); @@ -106,7 +107,12 @@ export function TabButtons(props: {
{/* Bottom line */} {!props.hideBottomLine && ( -
+
)} { const res = await apiServerProxy({ body: JSON.stringify({ diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/_common/step-card.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/_common/step-card.tsx index fa14752c80f..316ee8b824f 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/_common/step-card.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/_common/step-card.tsx @@ -35,7 +35,7 @@ export function StepCard(props: { {props.children} {(props.prevButton || props.nextButton) && ( -
+
{props.prevButton && ( +
+ ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/claim-rewards/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/claim-rewards/page.tsx new file mode 100644 index 00000000000..5cd5d7de07e --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/claim-rewards/page.tsx @@ -0,0 +1,91 @@ +import { notFound, redirect } from "next/navigation"; +import { getContract } from "thirdweb"; +import { + getDeployedEntrypointERC20, + getRewardLocker, + v3PositionManager as getV3PositionManager, +} from "thirdweb/assets"; +import { getProject } from "@/api/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"; + +export default async function Page(props: { + params: Promise; +}) { + 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 assetContractClient = info.clientContract; + + const entrypointContractClient = await getDeployedEntrypointERC20({ + chain: assetContractClient.chain, + client: assetContractClient.client, + }); + + const reward = await getValidReward({ + assetContract: assetContractClient, + entrypointContract: entrypointContractClient, + }); + + const rewardLocker = await getRewardLocker({ + contract: entrypointContractClient, + }).catch(() => null); + + if (!reward || !rewardLocker) { + redirect( + `/team/${params.team_slug}/${params.project_slug}/contract/${params.chainIdOrSlug}/${params.contractAddress}`, + ); + } + + const rewardLockerContractClient = getContract({ + address: rewardLocker, + chain: assetContractClient.chain, + client: assetContractClient.client, + }); + + const v3PositionManager = await getV3PositionManager({ + contract: rewardLockerContractClient, + }).catch(() => null); + + // const v4PositionManager = await getV4PositionManager({ + // contract: rewardLockerContractClient, + // }).catch(() => null); + + if (!v3PositionManager || v3PositionManager !== reward.positionManager) { + redirect( + `/team/${params.team_slug}/${params.project_slug}/contract/${params.chainIdOrSlug}/${params.contractAddress}`, + ); + } + + const v3PositionManagerContract = getContract({ + address: reward.positionManager, + chain: assetContractClient.chain, + client: assetContractClient.client, + }); + + return ( + + ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/claim-rewards/utils/rewards.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/claim-rewards/utils/rewards.ts new file mode 100644 index 00000000000..7c6283d0100 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/claim-rewards/utils/rewards.ts @@ -0,0 +1,28 @@ +import { type ThirdwebContract, ZERO_ADDRESS } from "thirdweb"; +import { getReward } from "thirdweb/assets"; + +export async function getValidReward(params: { + assetContract: ThirdwebContract; + entrypointContract: ThirdwebContract; +}) { + try { + const reward = await getReward({ + contract: params.entrypointContract, + asset: params.assetContract.address, + }); + + if ( + reward.positionManager === ZERO_ADDRESS || + reward.recipient === ZERO_ADDRESS || + reward.referrer === ZERO_ADDRESS || + reward.referrerBps === 0 || + reward.tokenId === BigInt(0) + ) { + return null; + } + + return reward; + } catch { + return null; + } +} diff --git a/packages/thirdweb/src/exports/assets.ts b/packages/thirdweb/src/exports/assets.ts index a2cbef1ae7a..71967483f99 100644 --- a/packages/thirdweb/src/exports/assets.ts +++ b/packages/thirdweb/src/exports/assets.ts @@ -18,5 +18,10 @@ export type { PoolConfig, TokenParams, } from "../assets/types.js"; -export { getInitBytecodeWithSalt } from "../utils/any-evm/get-init-bytecode-with-salt.js"; export { getReward } from "../extensions/assets/__generated__/ERC20AssetEntrypoint/read/getReward.js"; +export { getRewardLocker } from "../extensions/assets/__generated__/ERC20AssetEntrypoint/read/getRewardLocker.js"; +export { claimReward } from "../extensions/assets/__generated__/ERC20AssetEntrypoint/write/claimReward.js"; +export { positions } from "../extensions/assets/__generated__/RewardLocker/read/positions.js"; +export { v3PositionManager } from "../extensions/assets/__generated__/RewardLocker/read/v3PositionManager.js"; +export { v4PositionManager } from "../extensions/assets/__generated__/RewardLocker/read/v4PositionManager.js"; +export { getInitBytecodeWithSalt } from "../utils/any-evm/get-init-bytecode-with-salt.js"; From 6bfce15daeb3c13d82c83b11b087dbc9a3c12229 Mon Sep 17 00:00:00 2001 From: Manan Tank Date: Wed, 16 Jul 2025 20:49:32 +0530 Subject: [PATCH 10/35] load amounts --- .../components/claim-rewards-page.tsx | 85 +++++++++++-------- .../claim-rewards/utils/unclaimed-fees.ts | 63 ++++++++++++++ 2 files changed, 114 insertions(+), 34 deletions(-) create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/claim-rewards/utils/unclaimed-fees.ts diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/claim-rewards/components/claim-rewards-page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/claim-rewards/components/claim-rewards-page.tsx index 43e7f9984b2..7eb9be51c7e 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/claim-rewards/components/claim-rewards-page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/claim-rewards/components/claim-rewards-page.tsx @@ -2,15 +2,17 @@ import { useQuery } from "@tanstack/react-query"; import { ArrowRightIcon } from "lucide-react"; -import { readContract, type ThirdwebContract } from "thirdweb"; +import { toast } from "sonner"; +import type { ThirdwebContract } from "thirdweb"; import { claimReward } from "thirdweb/assets"; import { useSendAndConfirmTransaction } from "thirdweb/react"; +import { WalletAddress } from "@/components/blocks/wallet-address"; import { Button } from "@/components/ui/button"; import { Spinner } from "@/components/ui/Spinner/Spinner"; import { parseError } from "@/utils/errorParser"; +import { tryCatch } from "@/utils/try-catch"; import type { getValidReward } from "../utils/rewards"; - -const maxInt128 = 2n ** (128n - 1n) - 1n; +import { getUnclaimedFees } from "../utils/unclaimed-fees"; export function ClaimRewardsPage(props: { assetContractClient: ThirdwebContract; @@ -22,44 +24,47 @@ export function ClaimRewardsPage(props: { const sendAndConfirmTransaction = useSendAndConfirmTransaction(); async function handleClaim() { - const tx = claimReward({ + const claimRewardsTx = claimReward({ asset: props.assetContractClient.address, contract: props.rewardLockerContractClient, }); - await sendAndConfirmTransaction.mutateAsync(tx); + const claimRewardsResult = await tryCatch( + sendAndConfirmTransaction.mutateAsync(claimRewardsTx), + ); + + if (claimRewardsResult.error) { + toast.error("Failed to claim rewards", { + description: parseError(claimRewardsResult.error), + }); + } else { + toast.success("Rewards claimed successfully"); + } } - const collectionQuery = useQuery({ + const unclaimedFeesQuery = useQuery({ queryKey: [ - "unclaimed-fees", + "get-unclaimed-fees", { - ...props, + positionManager: props.v3PositionManagerContractClient.address, reward: { - ...props.reward, - tokenId: props.reward?.tokenId.toString(), + tokenId: props.reward.tokenId.toString(), + recipient: props.reward.recipient, }, }, ], - queryFn: async () => { - const result = await readContract({ - contract: props.v3PositionManagerContractClient, - method: - "function collect((uint256 tokenId,address recipient,uint128 amount0Max,uint128 amount1Max)) returns (uint256,uint256)", - params: [ - { - tokenId: props.reward.tokenId, - recipient: props.reward.recipient, - amount0Max: maxInt128, - amount1Max: maxInt128, - }, - ], - }); - - return result; - }, + queryFn: async () => + getUnclaimedFees({ + positionManager: props.v3PositionManagerContractClient, + reward: { + tokenId: props.reward.tokenId, + recipient: props.reward.recipient, + }, + }), }); + // TODO: add proper UI + return (

@@ -67,24 +72,36 @@ export function ClaimRewardsPage(props: {

- {collectionQuery.isPending && ( + {unclaimedFeesQuery.isPending && (
Loading unclaimed fees
)} - {collectionQuery.error && ( + {unclaimedFeesQuery.error && (
- Failed to load unclaimed fees {parseError(collectionQuery.error)} + Failed to load unclaimed fees {parseError(unclaimedFeesQuery.error)}
)} - {collectionQuery.data && ( -
-

Amount0: {collectionQuery.data[0]}

-

Amount1: {collectionQuery.data[1]}

+ {unclaimedFeesQuery.data && ( +
+

+ Amount0: {unclaimedFeesQuery.data.token0.amount}{" "} + {unclaimedFeesQuery.data.token0.address} +

+

+ Amount1: {unclaimedFeesQuery.data.token1.amount}{" "} + {unclaimedFeesQuery.data.token1.address} +

)} + +

Recipient

+
-
- ); -} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/claim-rewards/components/claim-rewards-page.stories.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.stories.tsx similarity index 73% rename from apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/claim-rewards/components/claim-rewards-page.stories.tsx rename to apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.stories.tsx index 7e937c9e62a..e324f36bf65 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/claim-rewards/components/claim-rewards-page.stories.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.stories.tsx @@ -1,5 +1,6 @@ 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"; @@ -10,7 +11,7 @@ const meta = { decorators: [ (Story) => ( -
+
@@ -28,10 +29,12 @@ function unclaimedFeesStub(token0Amount: bigint, token1Amount: bigint) { token0: { address: "0x1234567890123456789012345678901234567890", amount: token0Amount, + symbol: "FOO", }, token1: { address: "0x0987654321098765432109876543210987654321", amount: token1Amount, + symbol: "BAR", }, }; } @@ -41,6 +44,7 @@ export function LargeAmounts() { ); } @@ -50,24 +54,42 @@ export function SmallAmounts() { ); } -export function ZeroAmounts() { +export function ZeroAmount() { + return ( + + ); +} + +export function ZeroAmountNoChainExplorer() { return ( ); } -function Variant(props: { token0Amount: bigint; token1Amount: bigint }) { +function Variant(props: { + token0Amount: bigint; + token1Amount: bigint; + includeChainExplorer?: boolean; +}) { return ( {}} isClaimPending={false} + chain={base} unclaimedFees={unclaimedFeesStub(props.token0Amount, props.token1Amount)} /> ); diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.tsx new file mode 100644 index 00000000000..dafeb0de9ea --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.tsx @@ -0,0 +1,304 @@ +"use client"; + +import { ArrowRightIcon, ExternalLinkIcon } from "lucide-react"; +import Link from "next/link"; +import { toast } from "sonner"; +import { + type Chain, + type ThirdwebClient, + type ThirdwebContract, + toTokens, +} from "thirdweb"; +import { claimReward } from "thirdweb/assets"; +import { TokenIcon, TokenProvider, useSendTransaction } from "thirdweb/react"; +import { DistributionBarChart } from "@/components/blocks/distribution-chart"; +import { WalletAddress } from "@/components/blocks/wallet-address"; +import { Button } from "@/components/ui/button"; +import { Spinner } from "@/components/ui/Spinner/Spinner"; +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>>; + 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 = claimReward({ + asset: props.assetContractClient.address, + contract: props.entrypointContractClient, + }); + + const claimRewardsResult = await tryCatch( + sendTx.mutateAsync(claimRewardsTx), + ); + + if (claimRewardsResult.error) { + toast.error("Failed to claim rewards", { + description: parseError(claimRewardsResult.error), + }); + } else { + toast.success("Rewards claimed successfully"); + router.refresh(); + } + } + + return ( + + ); +} + +function calculateFees(referrerBps: number) { + // 20% of is protocol fees + // remaining is split between referrer and recipient + + const protocolFees = 20; + const remaining = 100 - protocolFees; + + const referrerPercentageFinal = (remaining * referrerBps) / 10000; + + return { + protocolFees, + referrerPercentage: referrerPercentageFinal, + recipientPercentage: 100 - protocolFees - referrerPercentageFinal, + }; +} + +export function ClaimRewardsPageUI(props: { + unclaimedFees: { + token0: { + address: string; + amount: bigint; + symbol: string; + }; + token1: { + address: string; + amount: bigint; + symbol: string; + }; + }; + recipient: string; + referrer: string; + referrerBps: number; + handleClaim: () => void; + isClaimPending: boolean; + client: ThirdwebClient; + chain: Chain; + chainSlug: string; +}) { + const fees = calculateFees(props.referrerBps); + + const recipientColor = `hsl(var(--chart-1))`; + const referrerColor = `hsl(var(--chart-2))`; + const protocolFeesColor = `hsl(var(--chart-10))`; + + const hasUnclaimedRewards = + props.unclaimedFees.token0.amount > 0 || + props.unclaimedFees.token1.amount > 0; + + return ( +
+
+
+

+ Uniswap LP Rewards +

+

+ Earnings received by Liquidity Providers (LPs) in exchange for + depositing tokens into {props.unclaimedFees.token0.symbol} /{" "} + {props.unclaimedFees.token1.symbol} Uniswap liquidity pool +

+
+ +
+
+

Unclaimed Rewards

+
+
+ + +
+
+ +
+ + +
+ +
+
+

Recipient

+ + } + /> +
+ +
+

Referrer

+ + } + /> +
+
+
+ +
+ {hasUnclaimedRewards && ( +

+ Click on "Distribute Rewards" to distribute unclaimed rewards +

+ )} + + {!hasUnclaimedRewards && ( +

+ No unclaimed rewards to distribute +

+ )} + +
+
+
+ ); +} + +function TokenReward(props: { + token: { + address: string; + amount: bigint; + symbol: string; + }; + client: ThirdwebClient; + chain: Chain; + chainSlug: string; +}) { + const fallbackIcon = ( +
+ {props.token.symbol[0]} +
+ ); + + return ( +
+
+ + + +
+
+

+ {toTokens(props.token.amount, 18)} {props.token.symbol} +

+ + + {props.token.address.slice(0, 6)}... + {props.token.address.slice(-4)} + + + +
+
+ ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/claim-rewards/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/page.tsx similarity index 98% rename from apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/claim-rewards/page.tsx rename to apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/page.tsx index 3d53487686f..eecf4a3d445 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/claim-rewards/page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/page.tsx @@ -90,6 +90,7 @@ export default async function Page(props: { entrypointContractClient={entrypointContractClient} reward={reward} unclaimedFees={unclaimedFees} + chainSlug={info.chainMetadata.slug} /> ); } diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/claim-rewards/utils/rewards.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/utils/rewards.ts similarity index 100% rename from apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/claim-rewards/utils/rewards.ts rename to apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/utils/rewards.ts diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/claim-rewards/utils/unclaimed-fees.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/utils/unclaimed-fees.ts similarity index 66% rename from apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/claim-rewards/utils/unclaimed-fees.ts rename to apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/utils/unclaimed-fees.ts index 2306481692d..16c1284602e 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/claim-rewards/utils/unclaimed-fees.ts +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/utils/unclaimed-fees.ts @@ -1,5 +1,6 @@ import type { ThirdwebContract } from "thirdweb"; -import { readContract } from "thirdweb"; +import { getContract, readContract } from "thirdweb"; +import { symbol } from "thirdweb/extensions/common"; export const maxUint128 = 2n ** 128n - 1n; @@ -36,7 +37,6 @@ export async function getUnclaimedFees(params: { positionsResultPromise, ]); - // positionsResult // 0- nonce // 1- owner // 2- token0 @@ -50,14 +50,39 @@ export async function getUnclaimedFees(params: { // 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: positionsResult[2], // token0 + address: token0Address, amount: collectResult[0], + symbol: token0Symbol, }, token1: { - address: positionsResult[3], // token1 + address: token1Address, amount: collectResult[1], + symbol: token1Symbol, }, }; } From e946458827e7d309e01e9610ff8034bc62ffb753 Mon Sep 17 00:00:00 2001 From: Manan Tank Date: Thu, 17 Jul 2025 00:54:50 +0530 Subject: [PATCH 13/35] copy changes --- .../rewards/components/claim-rewards-page.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.tsx index dafeb0de9ea..708e1638cb3 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.tsx @@ -52,11 +52,11 @@ export function ClaimRewardsPage(props: { ); if (claimRewardsResult.error) { - toast.error("Failed to claim rewards", { + toast.error("Failed to distribute rewards", { description: parseError(claimRewardsResult.error), }); } else { - toast.success("Rewards claimed successfully"); + toast.success("Rewards distributed successfully"); router.refresh(); } } @@ -128,7 +128,7 @@ export function ClaimRewardsPageUI(props: {
-

+

Uniswap LP Rewards

@@ -221,7 +221,7 @@ export function ClaimRewardsPageUI(props: {

-
+
{hasUnclaimedRewards && (

Click on "Distribute Rewards" to distribute unclaimed rewards @@ -230,7 +230,7 @@ export function ClaimRewardsPageUI(props: { {!hasUnclaimedRewards && (

- No unclaimed rewards to distribute + There are no unclaimed rewards available for distribution

)} + + + + +
))} @@ -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 ( +
+ + {tokenAmountFormatter.format( + Number(toTokens(BigInt(props.amount), props.decimals)), + )} + + {props.symbol} +
+ ); +} + function SkeletonRow() { return ( @@ -199,7 +266,7 @@ function SkeletonRow() { - + ); From 1366058d963195b9ea29be95b0af20b1e8792030 Mon Sep 17 00:00:00 2001 From: Manan Tank Date: Fri, 18 Jul 2025 04:08:54 +0530 Subject: [PATCH 15/35] update creation UI --- .../create/token/create-token-page.client.tsx | 10 ++-- .../token/distribution/token-distribution.tsx | 9 +++- .../create/token/distribution/token-sale.tsx | 53 +++++++++++++------ 3 files changed, 51 insertions(+), 21 deletions(-) diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page.client.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page.client.tsx index 3187173a3c0..cec518d549b 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page.client.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page.client.tsx @@ -49,7 +49,7 @@ export function CreateTokenAssetPageUI(props: { const tokenInfoForm = useForm({ defaultValues: { - chain: activeChain?.id.toString() || "1", + chain: activeChain?.id.toString() || "8453", description: "", image: undefined, name: "", @@ -75,12 +75,12 @@ export function CreateTokenAssetPageUI(props: { // airdrop airdropEnabled: false, pool: { - startingPricePerToken: "0.01", + startingPricePerToken: "0.000000001", // 1gwei per token }, // sale fieldset - saleAllocationPercentage: "0", - saleMode: "disabled", - supply: "1000000", + saleAllocationPercentage: "100", + saleMode: "pool", + supply: "1000000000", // 1 billion }, mode: "onChange", resolver: zodResolver(tokenDistributionFormSchema), diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-distribution.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-distribution.tsx index b15dba1adb1..0627a5bc5d5 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-distribution.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-distribution.tsx @@ -17,6 +17,11 @@ import type { 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; @@ -49,6 +54,7 @@ export function TokenDistributionFieldset(props: { @@ -73,12 +79,13 @@ export function TokenDistributionFieldset(props: {
- + +
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-sale.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-sale.tsx index d14fbc83822..f72476d643e 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-sale.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-sale.tsx @@ -2,14 +2,14 @@ import { useQuery } from "@tanstack/react-query"; import { XIcon } from "lucide-react"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import type { ThirdwebClient } from "thirdweb"; import { defineChain } from "thirdweb"; import { isRouterEnabled } from "thirdweb/assets"; import { FormFieldSetup } from "@/components/blocks/FormFieldSetup"; import { DynamicHeight } from "@/components/ui/DynamicHeight"; import { DecimalInput } from "@/components/ui/decimal-input"; -import { Skeleton } from "@/components/ui/skeleton"; +import { Spinner } from "@/components/ui/Spinner/Spinner"; import { Switch } from "@/components/ui/switch"; import { useAllChainsData } from "@/hooks/chains/allChains"; import type { TokenDistributionForm } from "../_common/form"; @@ -22,27 +22,48 @@ export function TokenSaleSection(props: { const saleMode = props.form.watch("saleMode"); const { idToChain } = useAllChainsData(); const chainMeta = idToChain.get(Number(props.chainId)); + const [hasUserUpdatedSaleMode, setHasUserUpdatedSaleMode] = useState(false); const isRouterEnabledQuery = useQuery({ - queryFn: () => - isRouterEnabled({ - // eslint-disable-next-line no-restricted-syntax - chain: defineChain(Number(props.chainId)), - client: props.client, - }), + queryFn: async () => { + try { + return await isRouterEnabled({ + // eslint-disable-next-line no-restricted-syntax + chain: defineChain(Number(props.chainId)), + client: props.client, + }); + } catch { + return false; + } + }, queryKey: ["isRouterEnabled", props.chainId], }); + const isRouterEnabledValue = isRouterEnabledQuery.data === true; + const isSaleEnabled = saleMode !== "disabled"; // eslint-disable-next-line no-restricted-syntax useEffect(() => { - if (isRouterEnabledQuery.data === false && isSaleEnabled) { + if (isRouterEnabledValue === false && isSaleEnabled) { props.form.setValue("saleMode", "disabled", { shouldValidate: true, }); } - }, [isRouterEnabledQuery.data, isSaleEnabled, props.form]); + }, [isRouterEnabledValue, isSaleEnabled, props.form]); + + // eslint-disable-next-line no-restricted-syntax + useEffect(() => { + if ( + isRouterEnabledValue === true && + !hasUserUpdatedSaleMode && + !isSaleEnabled + ) { + props.form.setValue("saleMode", "pool", { + shouldValidate: true, + }); + } + }, [isRouterEnabledValue, props.form, hasUserUpdatedSaleMode, isSaleEnabled]); return ( @@ -52,23 +73,25 @@ export function TokenSaleSection(props: {

Sale

- List and add liquidity for your coin on a decentralized exchange - for purchase at fluctuating market price + List your coin on a decentralized exchange and earn rewards on + every trade

{isRouterEnabledQuery.isPending ? ( - + ) : ( { - if (isRouterEnabledQuery.data !== true) { + if (!isRouterEnabledValue) { return; } + setHasUserUpdatedSaleMode(true); + props.form.setValue( "saleMode", checked ? "pool" : "disabled", From 3117a3bf7cf041ea9712f846e633a30e002594ca Mon Sep 17 00:00:00 2001 From: Manan Tank Date: Sat, 19 Jul 2025 03:57:35 +0530 Subject: [PATCH 16/35] updates --- .../components/blocks/distribution-chart.tsx | 43 +++++++++++------ .../create/token/create-token-page-impl.tsx | 2 +- .../token/distribution/token-airdrop.tsx | 2 +- .../token/distribution/token-distribution.tsx | 3 ++ .../create/token/distribution/token-sale.tsx | 48 ++++++++++++++++++- .../rewards/components/claim-rewards-page.tsx | 22 +++++---- 6 files changed, 94 insertions(+), 26 deletions(-) diff --git a/apps/dashboard/src/@/components/blocks/distribution-chart.tsx b/apps/dashboard/src/@/components/blocks/distribution-chart.tsx index 8abb8373679..b34094aadda 100644 --- a/apps/dashboard/src/@/components/blocks/distribution-chart.tsx +++ b/apps/dashboard/src/@/components/blocks/distribution-chart.tsx @@ -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 (
-
-

{props.title}

-
- Total: {totalPercentage}% + {props.title && ( +
+

+ {props.title} +

+
+ Total: {totalPercentage}% +
-
+ )} {/* Bar */} -
+
{props.segments.map((segment) => { return (
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}

); diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page-impl.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page-impl.tsx index c291f038bb8..8230c955cb0 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page-impl.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page-impl.tsx @@ -113,7 +113,7 @@ export function CreateTokenAssetPage(props: { params.values.pool.startingPricePerToken, ), }), - referrerRewardBps: 5000, // 50% + referrerRewardBps: 1250, // 12.5% }, } : undefined, diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-airdrop.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-airdrop.tsx index 133e8bd0295..99631b957d7 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-airdrop.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-airdrop.tsx @@ -67,7 +67,7 @@ export function TokenAirdropSection(props: { return (
-
+

Airdrop

diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-distribution.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-distribution.tsx index 0627a5bc5d5..075eacd6e79 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-distribution.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-distribution.tsx @@ -144,16 +144,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}%`, }, ]; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-sale.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-sale.tsx index f72476d643e..22b0c3eb10d 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-sale.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-sale.tsx @@ -1,11 +1,12 @@ "use client"; import { useQuery } from "@tanstack/react-query"; -import { XIcon } from "lucide-react"; +import { DollarSignIcon, XIcon } from "lucide-react"; import { useEffect, useState } from "react"; import type { ThirdwebClient } from "thirdweb"; import { defineChain } from "thirdweb"; import { isRouterEnabled } from "thirdweb/assets"; +import { DistributionBarChart } from "@/components/blocks/distribution-chart"; import { FormFieldSetup } from "@/components/blocks/FormFieldSetup"; import { DynamicHeight } from "@/components/ui/DynamicHeight"; import { DecimalInput } from "@/components/ui/decimal-input"; @@ -65,6 +66,11 @@ export function TokenSaleSection(props: { } }, [isRouterEnabledValue, props.form, hasUserUpdatedSaleMode, isSaleEnabled]); + const protocolFee = 20; + const leftOverFee = 100 - protocolFee; + const convenienceFee = (12.5 * leftOverFee) / 100; + const deployerFee = leftOverFee - convenienceFee; + return (

@@ -131,6 +137,46 @@ export function TokenSaleSection(props: { client={props.client} form={props.form} /> + +
+
+
+ +
+
+

Sale Rewards

+

+ All trades on the market are subjected to{" "} + 1% {" "} + fee distributed as: +

+ +
+
Total: 1%
+ +
+
)}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.tsx index 708e1638cb3..51da24045f5 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.tsx @@ -128,8 +128,8 @@ export function ClaimRewardsPageUI(props: {
-

- Uniswap LP Rewards +

+ Rewards

Earnings received by Liquidity Providers (LPs) in exchange for @@ -140,7 +140,7 @@ export function ClaimRewardsPageUI(props: {

-

Unclaimed Rewards

+

Unclaimed Rewards

@@ -289,7 +293,7 @@ function TokenReward(props: { From 23a9a62a6a2af9741e8a80b914ee6d1b4bb06ca2 Mon Sep 17 00:00:00 2001 From: Manan Tank Date: Sat, 19 Jul 2025 04:30:08 +0530 Subject: [PATCH 17/35] add pricing strategy selector --- .../create/token/distribution/token-sale.tsx | 126 ++++++++++++------ .../rewards/utils/unclaimed-fees.ts | 2 +- 2 files changed, 83 insertions(+), 45 deletions(-) diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-sale.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-sale.tsx index 22b0c3eb10d..61cb85ed3fa 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-sale.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-sale.tsx @@ -8,9 +8,17 @@ import { defineChain } from "thirdweb"; import { isRouterEnabled } from "thirdweb/assets"; import { DistributionBarChart } from "@/components/blocks/distribution-chart"; import { FormFieldSetup } from "@/components/blocks/FormFieldSetup"; +import { Badge } from "@/components/ui/badge"; import { DynamicHeight } from "@/components/ui/DynamicHeight"; import { DecimalInput } from "@/components/ui/decimal-input"; import { Spinner } from "@/components/ui/Spinner/Spinner"; +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"; @@ -198,55 +206,85 @@ function PoolConfig(props: { ); return ( -
- {/* supply % */} +
+ {/* Pricing Strategy */} -
- { - props.form.setValue("saleAllocationPercentage", value, { - shouldValidate: true, - }); - }} - value={props.form.watch("saleAllocationPercentage")} - /> - - % - -
+
- {/* starting price */} - -
- { - props.form.setValue("pool.startingPricePerToken", value, { - shouldValidate: true, - }); - }} - value={props.form.watch("pool.startingPricePerToken")} - /> - - {chainMeta?.nativeCurrency.symbol || "ETH"} - -
-
+
+ {/* supply % */} + +
+ { + props.form.setValue("saleAllocationPercentage", value, { + shouldValidate: true, + }); + }} + value={props.form.watch("saleAllocationPercentage")} + /> + + % + +
+
+ + {/* starting price */} + +
+ { + props.form.setValue("pool.startingPricePerToken", value, { + shouldValidate: true, + }); + }} + value={props.form.watch("pool.startingPricePerToken")} + /> + + {chainMeta?.nativeCurrency.symbol || "ETH"} + +
+
+
); } diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/utils/unclaimed-fees.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/utils/unclaimed-fees.ts index 16c1284602e..3e0adcb6e9e 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/utils/unclaimed-fees.ts +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/utils/unclaimed-fees.ts @@ -2,7 +2,7 @@ import type { ThirdwebContract } from "thirdweb"; import { getContract, readContract } from "thirdweb"; import { symbol } from "thirdweb/extensions/common"; -export const maxUint128 = 2n ** 128n - 1n; +const maxUint128 = 2n ** 128n - 1n; export async function getUnclaimedFees(params: { positionManager: ThirdwebContract; From f07f9b44a9cec16727c224244d038539d8788b44 Mon Sep 17 00:00:00 2001 From: Manan Tank Date: Sat, 19 Jul 2025 04:59:49 +0530 Subject: [PATCH 18/35] distribute page ui updates --- .../rewards/components/claim-rewards-page.tsx | 45 +++++++++++++------ 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.tsx index 51da24045f5..e8856bbe586 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.tsx @@ -1,6 +1,6 @@ "use client"; -import { ArrowRightIcon, ExternalLinkIcon } from "lucide-react"; +import { DollarSignIcon, ExternalLinkIcon, SplitIcon } from "lucide-react"; import Link from "next/link"; import { toast } from "sonner"; import { @@ -118,7 +118,7 @@ export function ClaimRewardsPageUI(props: { const recipientColor = `hsl(var(--chart-1))`; const referrerColor = `hsl(var(--chart-2))`; - const protocolFeesColor = `hsl(var(--chart-10))`; + const protocolFeesColor = `hsl(var(--chart-3))`; const hasUnclaimedRewards = props.unclaimedFees.token0.amount > 0 || @@ -127,8 +127,8 @@ export function ClaimRewardsPageUI(props: { return (
-
-

+
+

Rewards

@@ -138,9 +138,17 @@ export function ClaimRewardsPageUI(props: {

-
-
-

Unclaimed Rewards

+
+
+
+
+ +
+
+

Unclaimed Rewards

+

+ The rewards that are earned but haven't been distributed yet +

-
+
+
+
+ +
+
+ +

Reward Distribution

+

+ The unclaimed rewards will be distributed as: +

+
-
+
{hasUnclaimedRewards && (

Click on "Distribute Rewards" to distribute unclaimed rewards @@ -239,14 +256,14 @@ export function ClaimRewardsPageUI(props: { )}

@@ -272,7 +289,7 @@ function TokenReward(props: { ); return ( -
+
Date: Sat, 19 Jul 2025 05:16:34 +0530 Subject: [PATCH 19/35] ui tweaks --- .../rewards/components/claim-rewards-page.tsx | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.tsx index e8856bbe586..9e476bf9713 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.tsx @@ -127,8 +127,14 @@ export function ClaimRewardsPageUI(props: { return (
-
-

+
+
+
+ +
+
+ +

Rewards

@@ -140,11 +146,6 @@ export function ClaimRewardsPageUI(props: {

-
-
- -
-

Unclaimed Rewards

The rewards that are earned but haven't been distributed yet @@ -167,12 +168,6 @@ export function ClaimRewardsPageUI(props: {

-
-
- -
-
-

Reward Distribution

The unclaimed rewards will be distributed as: From db4a397b5d4e14115211324242db57da124da2ba Mon Sep 17 00:00:00 2001 From: Manan Tank Date: Wed, 23 Jul 2025 21:37:54 +0530 Subject: [PATCH 20/35] update imports --- .../_utils/getContractPageMetadataSetup.ts | 2 +- .../tokens/create/token/create-token-page-impl.tsx | 6 +++--- .../tokens/create/token/distribution/token-sale.tsx | 2 +- .../rewards/components/claim-rewards-page.tsx | 2 +- .../[chainIdOrSlug]/[contractAddress]/rewards/page.tsx | 2 +- .../[contractAddress]/rewards/utils/rewards.ts | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_utils/getContractPageMetadataSetup.ts b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_utils/getContractPageMetadataSetup.ts index ed173c54984..74d8176807d 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_utils/getContractPageMetadataSetup.ts +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_utils/getContractPageMetadataSetup.ts @@ -1,6 +1,6 @@ import type { ThirdwebContract } from "thirdweb"; -import { getDeployedEntrypointERC20 } from "thirdweb/assets"; 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 { diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page-impl.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page-impl.tsx index 8230c955cb0..cfd226e94e7 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page-impl.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page-impl.tsx @@ -7,13 +7,13 @@ import { type ThirdwebClient, toWei, } from "thirdweb"; +import { approve } from "thirdweb/extensions/erc20"; +import { useActiveAccount } from "thirdweb/react"; import { createToken, distributeToken, getDeployedEntrypointERC20, -} from "thirdweb/assets"; -import { approve } from "thirdweb/extensions/erc20"; -import { useActiveAccount } from "thirdweb/react"; +} from "thirdweb/tokens"; import { create7702MinimalAccount } from "thirdweb/wallets/smart"; import { revalidatePathAction } from "@/actions/revalidate"; import { diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-sale.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-sale.tsx index 61cb85ed3fa..6232596d3ba 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-sale.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-sale.tsx @@ -5,7 +5,7 @@ import { DollarSignIcon, XIcon } from "lucide-react"; import { useEffect, useState } from "react"; import type { ThirdwebClient } from "thirdweb"; import { defineChain } from "thirdweb"; -import { isRouterEnabled } from "thirdweb/assets"; +import { isRouterEnabled } from "thirdweb/tokens"; import { DistributionBarChart } from "@/components/blocks/distribution-chart"; import { FormFieldSetup } from "@/components/blocks/FormFieldSetup"; import { Badge } from "@/components/ui/badge"; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.tsx index 9e476bf9713..8cce45eb0f3 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.tsx @@ -9,8 +9,8 @@ import { type ThirdwebContract, toTokens, } from "thirdweb"; -import { claimReward } from "thirdweb/assets"; import { TokenIcon, TokenProvider, useSendTransaction } from "thirdweb/react"; +import { claimReward } from "thirdweb/tokens"; import { DistributionBarChart } from "@/components/blocks/distribution-chart"; import { WalletAddress } from "@/components/blocks/wallet-address"; import { Button } from "@/components/ui/button"; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/page.tsx index eecf4a3d445..3923bb75966 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/page.tsx @@ -4,7 +4,7 @@ import { getDeployedEntrypointERC20, getRewardLocker, v3PositionManager as getV3PositionManager, -} from "thirdweb/assets"; +} from "thirdweb/tokens"; import { getProject } from "@/api/projects"; import { getContractPageParamsInfo } from "../../../../../../../(dashboard)/(chain)/[chain_id]/[contractAddress]/_utils/getContractFromParams"; import type { ProjectContractPageParams } from "../types"; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/utils/rewards.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/utils/rewards.ts index 7c6283d0100..7a0431a9bc0 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/utils/rewards.ts +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/utils/rewards.ts @@ -1,5 +1,5 @@ import { type ThirdwebContract, ZERO_ADDRESS } from "thirdweb"; -import { getReward } from "thirdweb/assets"; +import { getReward } from "thirdweb/tokens"; export async function getValidReward(params: { assetContract: ThirdwebContract; From 0d7728e177a192f6b38a6387dfb682c2e5218e84 Mon Sep 17 00:00:00 2001 From: Manan Tank Date: Thu, 24 Jul 2025 01:43:42 +0530 Subject: [PATCH 21/35] show 2 steps for airdrop --- apps/dashboard/src/@/analytics/report.ts | 3 +- .../create/token/create-token-page-impl.tsx | 64 +++++++++++-------- .../create/token/create-token-page.client.tsx | 1 + .../token/create-token-page.stories.tsx | 3 + .../create/token/launch/launch-token.tsx | 9 +++ 5 files changed, 51 insertions(+), 29 deletions(-) diff --git a/apps/dashboard/src/@/analytics/report.ts b/apps/dashboard/src/@/analytics/report.ts index 4516a4fcaff..e2ab11d02e9 100644 --- a/apps/dashboard/src/@/analytics/report.ts +++ b/apps/dashboard/src/@/analytics/report.ts @@ -374,7 +374,8 @@ export function reportAssetCreationFailed( | "deploy-contract" | "set-claim-conditions" | "mint-tokens" - | "airdrop-tokens"; + | "airdrop-tokens" + | "approve-airdrop-tokens"; } ), ) { diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page-impl.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page-impl.tsx index cfd226e94e7..c8a19c1842b 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page-impl.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page-impl.tsx @@ -176,34 +176,23 @@ export function CreateTokenAssetPage(props: { const account = getAccount(gasless); const contract = getDeployedContract({ chain: values.chain }); - const totalAmountToAirdrop = values.airdropAddresses.reduce( - (acc, recipient) => acc + BigInt(recipient.quantity), - 0n, - ); - - // approve entrypoint to spend tokens - try { - const entrypoint = await getDeployedEntrypointERC20({ + const airdropTx = await distributeToken({ 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, + contents: values.airdropAddresses.map((recipient) => ({ + amount: BigInt(recipient.quantity), + recipient: recipient.address, + })), + tokenAddress: contract.address, }); await sendAndConfirmTransaction({ account, - transaction: approvalTx, + transaction: airdropTx, }); } catch (e) { + console.error(e); const errorMessage = parseError(e); reportAssetCreationFailed({ @@ -212,27 +201,44 @@ export function CreateTokenAssetPage(props: { error: errorMessage, step: "airdrop-tokens", }); - throw e; } + } + + async function 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 + BigInt(recipient.quantity), + 0n, + ); try { - const airdropTx = await distributeToken({ + const entrypoint = await getDeployedEntrypointERC20({ chain: contract.chain, client: props.client, - contents: values.airdropAddresses.map((recipient) => ({ - amount: BigInt(recipient.quantity), - recipient: recipient.address, - })), - tokenAddress: contract.address, + }); + + if (!entrypoint) { + throw new Error("Entrypoint not found"); + } + + const approvalTx = approve({ + amountWei: toWei(totalAmountToAirdrop.toString()), + contract: contract, + spender: entrypoint.address, }); await sendAndConfirmTransaction({ account, - transaction: airdropTx, + transaction: approvalTx, }); } catch (e) { - console.error(e); const errorMessage = parseError(e); reportAssetCreationFailed({ @@ -241,6 +247,7 @@ export function CreateTokenAssetPage(props: { error: errorMessage, step: "airdrop-tokens", }); + throw e; } } @@ -252,6 +259,7 @@ export function CreateTokenAssetPage(props: { createTokenFunctions={{ airdropTokens, deployContract, + approveAirdropTokens, }} onLaunchSuccess={(params) => { createTokenOnUniversalBridge({ diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page.client.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page.client.tsx index cec518d549b..db42f9e9dc3 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page.client.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page.client.tsx @@ -28,6 +28,7 @@ export type CreateTokenFunctions = { contractAddress: string; }>; airdropTokens: (values: CreateTokenFunctionsParams) => Promise; + approveAirdropTokens: (values: CreateTokenFunctionsParams) => Promise; }; export function CreateTokenAssetPageUI(props: { diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page.stories.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page.stories.tsx index bb401ac64a1..d1bb81761bd 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page.stories.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page.stories.tsx @@ -29,6 +29,9 @@ const mockCreateTokenFunctions: CreateTokenFunctions = { 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" }; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/launch/launch-token.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/launch/launch-token.tsx index d04088ae4c6..bd408cf9575 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/launch/launch-token.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/launch/launch-token.tsx @@ -39,6 +39,7 @@ import { TokenDistributionBarChart } from "../distribution/token-distribution"; const stepIds = { "airdrop-tokens": "airdrop-tokens", + "approve-airdrop-tokens": "approve-airdrop-tokens", "deploy-contract": "deploy-contract", "mint-tokens": "mint-tokens", "set-claim-conditions": "set-claim-conditions", @@ -97,6 +98,12 @@ export function LaunchTokenStatus(props: { ]; if (formValues.airdropEnabled && formValues.airdropAddresses.length > 0) { + initialSteps.push({ + id: stepIds["approve-airdrop-tokens"], + label: "Approve spending tokens for airdrop", + status: { type: "idle" }, + }); + initialSteps.push({ id: stepIds["airdrop-tokens"], label: "Airdrop tokens", @@ -123,6 +130,8 @@ export function LaunchTokenStatus(props: { setContractAddress(result.contractAddress); } else if (stepId === "airdrop-tokens") { await createTokenFunctions.airdropTokens(params); + } else if (stepId === "approve-airdrop-tokens") { + await createTokenFunctions.approveAirdropTokens(params); } } From 07a879d9431fb93744ceb1243129da8cdab63a0e Mon Sep 17 00:00:00 2001 From: Manan Tank Date: Thu, 24 Jul 2025 23:13:17 +0530 Subject: [PATCH 22/35] update chain selectors --- .../@/components/blocks/NetworkSelectors.tsx | 50 +++++++++++++++---- .../token/distribution/token-distribution.tsx | 26 +++++----- .../token/token-info/token-info-fieldset.tsx | 12 +++++ 3 files changed, 66 insertions(+), 22 deletions(-) diff --git a/apps/dashboard/src/@/components/blocks/NetworkSelectors.tsx b/apps/dashboard/src/@/components/blocks/NetworkSelectors.tsx index e6507f45d6d..c3dd895db25 100644 --- a/apps/dashboard/src/@/components/blocks/NetworkSelectors.tsx +++ b/apps/dashboard/src/@/components/blocks/NetworkSelectors.tsx @@ -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(() => { diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-distribution.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-distribution.tsx index 075eacd6e79..cdf228aaeaf 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-distribution.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-distribution.tsx @@ -50,7 +50,7 @@ export function TokenDistributionFieldset(props: { title="Coin Distribution" >

-
+
- -
- - - {distributionError && ( -
- {distributionError} -
- )} -
+ +
+ + + {distributionError && ( +
+ {distributionError} +
+ )} +
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/token-info/token-info-fieldset.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/token-info/token-info-fieldset.tsx index 9ab208ad627..e0f685ec73a 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/token-info/token-info-fieldset.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/token-info/token-info-fieldset.tsx @@ -97,6 +97,18 @@ export function TokenInfoFieldset(props: { > }> Date: Thu, 24 Jul 2025 23:27:18 +0530 Subject: [PATCH 23/35] padding fix --- .../(sidebar)/tokens/create/token/distribution/token-sale.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-sale.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-sale.tsx index 6232596d3ba..bd859ddf804 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-sale.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-sale.tsx @@ -235,7 +235,7 @@ function PoolConfig(props: { -
+
{/* supply % */} Date: Thu, 24 Jul 2025 23:54:14 +0530 Subject: [PATCH 24/35] UI tweaks --- .../(sidebar)/tokens/create/_common/SocialUrls.tsx | 2 +- .../tokens/create/token/distribution/token-airdrop.tsx | 8 ++++---- .../create/token/distribution/token-distribution.tsx | 4 ++-- .../tokens/create/token/distribution/token-sale.tsx | 8 ++++---- .../create/token/token-info/token-info-fieldset.tsx | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/_common/SocialUrls.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/_common/SocialUrls.tsx index 9990f1718c7..cb9e8c18494 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/_common/SocialUrls.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/_common/SocialUrls.tsx @@ -34,7 +34,7 @@ export function SocialUrlsFieldset(props: {

Social URLs

{fields.length > 0 && ( -
+
{fields.map((field, index) => (
-
-
+
+
-

Airdrop

+

Airdrop

Airdrop tokens to a list of addresses with each address receiving a specific quantity @@ -88,7 +88,7 @@ export function TokenAirdropSection(props: {

{isEnabled && ( -
+
{airdropAddresses.length > 0 ? (
{/* left */} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-distribution.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-distribution.tsx index cdf228aaeaf..3b340f75c95 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-distribution.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-distribution.tsx @@ -58,7 +58,7 @@ export function TokenDistributionFieldset(props: { isRequired label="Total Supply" > -
+
{props.tokenSymbol || "Tokens"} @@ -75,7 +75,7 @@ export function TokenDistributionFieldset(props: { -
+
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-sale.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-sale.tsx index bd859ddf804..55fa5e3da72 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-sale.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-sale.tsx @@ -81,11 +81,11 @@ export function TokenSaleSection(props: { return ( -
+
-

Sale

+

Sale

List your coin on a decentralized exchange and earn rewards on every trade @@ -206,7 +206,7 @@ function PoolConfig(props: { ); return ( -

+
{/* Pricing Strategy */} -
+
{/* supply % */} Date: Sat, 26 Jul 2025 07:29:29 +0000 Subject: [PATCH 25/35] Update tokens SDK --- .../create/token/distribution/token-sale.tsx | 4 +-- .../rewards/components/claim-rewards-page.tsx | 4 +-- .../[contractAddress]/rewards/page.tsx | 26 ++----------------- .../rewards/utils/rewards.ts | 17 +++++------- 4 files changed, 12 insertions(+), 39 deletions(-) diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-sale.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-sale.tsx index 55fa5e3da72..1c9cd420cde 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-sale.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-sale.tsx @@ -5,7 +5,7 @@ import { DollarSignIcon, XIcon } from "lucide-react"; import { useEffect, useState } from "react"; import type { ThirdwebClient } from "thirdweb"; import { defineChain } from "thirdweb"; -import { isRouterEnabled } from "thirdweb/tokens"; +import { isPoolRouterEnabled } from "thirdweb/tokens"; import { DistributionBarChart } from "@/components/blocks/distribution-chart"; import { FormFieldSetup } from "@/components/blocks/FormFieldSetup"; import { Badge } from "@/components/ui/badge"; @@ -36,7 +36,7 @@ export function TokenSaleSection(props: { const isRouterEnabledQuery = useQuery({ queryFn: async () => { try { - return await isRouterEnabled({ + return await isPoolRouterEnabled({ // eslint-disable-next-line no-restricted-syntax chain: defineChain(Number(props.chainId)), client: props.client, diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.tsx index 8cce45eb0f3..173b6834822 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.tsx @@ -10,7 +10,7 @@ import { toTokens, } from "thirdweb"; import { TokenIcon, TokenProvider, useSendTransaction } from "thirdweb/react"; -import { claimReward } from "thirdweb/tokens"; +import { claimRewards } from "thirdweb/tokens"; import { DistributionBarChart } from "@/components/blocks/distribution-chart"; import { WalletAddress } from "@/components/blocks/wallet-address"; import { Button } from "@/components/ui/button"; @@ -42,7 +42,7 @@ export function ClaimRewardsPage(props: { const router = useDashboardRouter(); async function handleClaim() { - const claimRewardsTx = claimReward({ + const claimRewardsTx = claimRewards({ asset: props.assetContractClient.address, contract: props.entrypointContractClient, }); diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/page.tsx index 3923bb75966..367dc97932f 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/page.tsx @@ -2,8 +2,6 @@ import { notFound, redirect } from "next/navigation"; import { getContract } from "thirdweb"; import { getDeployedEntrypointERC20, - getRewardLocker, - v3PositionManager as getV3PositionManager, } from "thirdweb/tokens"; import { getProject } from "@/api/projects"; import { getContractPageParamsInfo } from "../../../../../../../(dashboard)/(chain)/[chain_id]/[contractAddress]/_utils/getContractFromParams"; @@ -44,27 +42,7 @@ export default async function Page(props: { entrypointContract: entrypointContractClient, }); - const rewardLocker = await getRewardLocker({ - contract: entrypointContractClient, - }).catch(() => null); - - if (!reward || !rewardLocker) { - redirect( - `/team/${params.team_slug}/${params.project_slug}/contract/${params.chainIdOrSlug}/${params.contractAddress}`, - ); - } - - const rewardLockerContractClient = getContract({ - address: rewardLocker, - chain: assetContractClient.chain, - client: assetContractClient.client, - }); - - const v3PositionManager = await getV3PositionManager({ - contract: rewardLockerContractClient, - }).catch(() => null); - - if (!v3PositionManager || v3PositionManager !== reward.positionManager) { + if (!reward) { redirect( `/team/${params.team_slug}/${params.project_slug}/contract/${params.chainIdOrSlug}/${params.contractAddress}`, ); @@ -79,7 +57,7 @@ export default async function Page(props: { const unclaimedFees = await getUnclaimedFees({ positionManager: v3PositionManagerContract, reward: { - tokenId: reward.tokenId, + tokenId: reward.positionId, recipient: reward.recipient, }, }); diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/utils/rewards.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/utils/rewards.ts index 7a0431a9bc0..5774ac9fc5d 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/utils/rewards.ts +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/utils/rewards.ts @@ -1,27 +1,22 @@ -import { type ThirdwebContract, ZERO_ADDRESS } from "thirdweb"; -import { getReward } from "thirdweb/tokens"; +import { type ThirdwebContract } from "thirdweb"; +import { getRewards } from "thirdweb/tokens"; export async function getValidReward(params: { assetContract: ThirdwebContract; entrypointContract: ThirdwebContract; }) { try { - const reward = await getReward({ + const rewards = await getRewards({ contract: params.entrypointContract, asset: params.assetContract.address, }); - if ( - reward.positionManager === ZERO_ADDRESS || - reward.recipient === ZERO_ADDRESS || - reward.referrer === ZERO_ADDRESS || - reward.referrerBps === 0 || - reward.tokenId === BigInt(0) - ) { + if (rewards.length === 0) { return null; } - return reward; + // It's potentially possible to have multiple rewards locked up, but it's not the default use case. + return rewards[0]; } catch { return null; } From 5f8c5035d9f71651442d824759192596500110f0 Mon Sep 17 00:00:00 2001 From: Jake Loo <2171134+jakeloo@users.noreply.github.com> Date: Tue, 12 Aug 2025 06:07:55 +0000 Subject: [PATCH 26/35] Update referrer -> developer --- .../create/token/create-token-page-impl.tsx | 4 +-- .../components/claim-rewards-page.stories.tsx | 6 ++-- .../rewards/components/claim-rewards-page.tsx | 36 +++++++++---------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page-impl.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page-impl.tsx index c8a19c1842b..c866df74a17 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page-impl.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page-impl.tsx @@ -113,7 +113,7 @@ export function CreateTokenAssetPage(props: { params.values.pool.startingPricePerToken, ), }), - referrerRewardBps: 1250, // 12.5% + developerRewardBps: 1250, // 12.5% }, } : undefined, @@ -125,7 +125,7 @@ export function CreateTokenAssetPage(props: { social_urls: socialUrls, symbol: params.values.symbol, }, - referrerAddress: "0x1Af20C6B23373350aD464700B5965CE4B0D2aD94", + developerAddress: "0x1Af20C6B23373350aD464700B5965CE4B0D2aD94", }); // add contract to project in background diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.stories.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.stories.tsx index e324f36bf65..529d53ec22a 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.stories.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.stories.tsx @@ -22,7 +22,7 @@ const meta = { export default meta; const recipient = "0x8C00f3F231c88CcAc2382AaC6e09A78D4F42129d"; -const referrer = "0x1Af20C6B23373350aD464700B5965CE4B0D2aD94"; +const developer = "0x1Af20C6B23373350aD464700B5965CE4B0D2aD94"; function unclaimedFeesStub(token0Amount: bigint, token1Amount: bigint) { return { @@ -82,10 +82,10 @@ function Variant(props: { }) { return ( {}} isClaimPending={false} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.tsx index 173b6834822..36afbf4e753 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.tsx @@ -65,30 +65,30 @@ export function ClaimRewardsPage(props: { ); } -function calculateFees(referrerBps: number) { +function calculateFees(developerBps: number) { // 20% of is protocol fees - // remaining is split between referrer and recipient + // remaining is split between developer and recipient const protocolFees = 20; const remaining = 100 - protocolFees; - const referrerPercentageFinal = (remaining * referrerBps) / 10000; + const developerPercentageFinal = (remaining * developerBps) / 10000; return { protocolFees, - referrerPercentage: referrerPercentageFinal, - recipientPercentage: 100 - protocolFees - referrerPercentageFinal, + developerPercentage: developerPercentageFinal, + recipientPercentage: 100 - protocolFees - developerPercentageFinal, }; } @@ -106,18 +106,18 @@ export function ClaimRewardsPageUI(props: { }; }; recipient: string; - referrer: string; - referrerBps: number; + developer: string; + developerBps: number; handleClaim: () => void; isClaimPending: boolean; client: ThirdwebClient; chain: Chain; chainSlug: string; }) { - const fees = calculateFees(props.referrerBps); + const fees = calculateFees(props.developerBps); const recipientColor = `hsl(var(--chart-1))`; - const referrerColor = `hsl(var(--chart-2))`; + const developerColor = `hsl(var(--chart-2))`; const protocolFeesColor = `hsl(var(--chart-3))`; const hasUnclaimedRewards = @@ -188,10 +188,10 @@ export function ClaimRewardsPageUI(props: { value: `${fees.protocolFees}%`, }, { - label: "Referrer", - color: referrerColor, - percent: fees.referrerPercentage, - value: `${fees.referrerPercentage}%`, + label: "Developer", + color: developerColor, + percent: fees.developerPercentage, + value: `${fees.developerPercentage}%`, }, ]} /> @@ -218,9 +218,9 @@ export function ClaimRewardsPageUI(props: {
-

Referrer

+

Developer

} From a37c2967a38bc9cda3e55f361930c9624613e123 Mon Sep 17 00:00:00 2001 From: Manan Tank Date: Tue, 12 Aug 2025 15:15:40 +0530 Subject: [PATCH 27/35] fix import --- .../[chainIdOrSlug]/[contractAddress]/rewards/page.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/page.tsx index 367dc97932f..ea2692b21ec 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/page.tsx @@ -1,9 +1,7 @@ import { notFound, redirect } from "next/navigation"; import { getContract } from "thirdweb"; -import { - getDeployedEntrypointERC20, -} from "thirdweb/tokens"; -import { getProject } from "@/api/projects"; +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"; From 3048bd49d496217d070ae641bb4acf71868032d6 Mon Sep 17 00:00:00 2001 From: Manan Tank Date: Wed, 13 Aug 2025 17:21:58 +0530 Subject: [PATCH 28/35] fix lint warning --- .../[chainIdOrSlug]/[contractAddress]/rewards/utils/rewards.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/utils/rewards.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/utils/rewards.ts index 5774ac9fc5d..8834c6572df 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/utils/rewards.ts +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/utils/rewards.ts @@ -1,4 +1,4 @@ -import { type ThirdwebContract } from "thirdweb"; +import type { ThirdwebContract } from "thirdweb"; import { getRewards } from "thirdweb/tokens"; export async function getValidReward(params: { From 484113b5e439c783e129c8ed16d6dc6ecbcecfc9 Mon Sep 17 00:00:00 2001 From: Manan Tank Date: Thu, 14 Aug 2025 21:49:21 +0530 Subject: [PATCH 29/35] update --- .../[chainIdOrSlug]/[contractAddress]/rewards/page.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/page.tsx index ea2692b21ec..a5d313d31d8 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/page.tsx @@ -30,7 +30,13 @@ export default async function Page(props: { const assetContractClient = info.clientContract; - const entrypointContractClient = await getDeployedEntrypointERC20({ + const { address } = await getDeployedEntrypointERC20({ + chain: assetContractClient.chain, + client: assetContractClient.client, + }); + + const entrypointContractClient = getContract({ + address, chain: assetContractClient.chain, client: assetContractClient.client, }); From 49103bfec88fae039bbd157886e5492ca10d852d Mon Sep 17 00:00:00 2001 From: Manan Tank Date: Thu, 14 Aug 2025 22:00:17 +0530 Subject: [PATCH 30/35] debug crash --- .../[chainIdOrSlug]/[contractAddress]/rewards/page.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/page.tsx index a5d313d31d8..6c41102a5c8 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/page.tsx @@ -66,6 +66,14 @@ export default async function Page(props: { }, }); + console.log("DEBUG", { + assetContractClient, + entrypointContractClient, + reward, + unclaimedFees, + chainSlug: info.chainMetadata.slug, + }); + return ( Date: Thu, 14 Aug 2025 22:16:49 +0530 Subject: [PATCH 31/35] test --- .../contract/[chainIdOrSlug]/[contractAddress]/rewards/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/page.tsx index 6c41102a5c8..5a707b6186a 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/page.tsx @@ -66,7 +66,7 @@ export default async function Page(props: { }, }); - console.log("DEBUG", { + console.error("DEBUG", { assetContractClient, entrypointContractClient, reward, From 61edd0e1bf8034e75bd32037bda9f6752f7d9689 Mon Sep 17 00:00:00 2001 From: Manan Tank Date: Thu, 14 Aug 2025 23:12:25 +0530 Subject: [PATCH 32/35] fix --- .../src/@/contexts/error-handler.tsx | 1 - .../[contractAddress]/rewards/page.tsx | 57 +++++++++---------- 2 files changed, 27 insertions(+), 31 deletions(-) diff --git a/apps/dashboard/src/@/contexts/error-handler.tsx b/apps/dashboard/src/@/contexts/error-handler.tsx index 71015e3307f..4bea625871c 100644 --- a/apps/dashboard/src/@/contexts/error-handler.tsx +++ b/apps/dashboard/src/@/contexts/error-handler.tsx @@ -1,5 +1,4 @@ "use client"; - import { CircleAlertIcon, InfoIcon } from "lucide-react"; import Link from "next/link"; import { createContext, useCallback, useContext, useState } from "react"; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/page.tsx index 5a707b6186a..74cc783bf3f 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/page.tsx @@ -28,22 +28,24 @@ export default async function Page(props: { notFound(); } - const assetContractClient = info.clientContract; + const chain = info.clientContract.chain; + const assetContractServer = info.serverContract; + const serverClient = assetContractServer.client; - const { address } = await getDeployedEntrypointERC20({ - chain: assetContractClient.chain, - client: assetContractClient.client, - }); - - const entrypointContractClient = getContract({ - address, - chain: assetContractClient.chain, - client: assetContractClient.client, - }); + const { address: entrypointContractAddress } = + await getDeployedEntrypointERC20({ + chain, + client: serverClient, + }); + // Note: must use server contract/client here const reward = await getValidReward({ - assetContract: assetContractClient, - entrypointContract: entrypointContractClient, + assetContract: assetContractServer, + entrypointContract: getContract({ + address: entrypointContractAddress, + chain, + client: serverClient, + }), }); if (!reward) { @@ -52,32 +54,27 @@ export default async function Page(props: { ); } - const v3PositionManagerContract = getContract({ - address: reward.positionManager, - chain: assetContractClient.chain, - client: assetContractClient.client, - }); - + // Note: must use server contract/client here const unclaimedFees = await getUnclaimedFees({ - positionManager: v3PositionManagerContract, + positionManager: getContract({ + address: reward.positionManager, + chain, + client: serverClient, + }), reward: { tokenId: reward.positionId, recipient: reward.recipient, }, }); - console.error("DEBUG", { - assetContractClient, - entrypointContractClient, - reward, - unclaimedFees, - chainSlug: info.chainMetadata.slug, - }); - return ( Date: Sat, 16 Aug 2025 00:49:39 +0530 Subject: [PATCH 33/35] Add tokendrop as fallback --- apps/dashboard/src/@/analytics/report.ts | 22 +- .../src/@/components/blocks/TokenSelector.tsx | 2 +- .../src/@/hooks/project-contracts.ts | 7 +- .../tokens/create/token/_common/form.ts | 51 +- .../create/token/create-token-page-impl.tsx | 451 ++++++++++++------ .../create/token/create-token-page.client.tsx | 149 ++++-- .../token/create-token-page.stories.tsx | 55 ++- .../distribution/drop-erc20-token-sale.tsx | 134 ++++++ .../token/distribution/token-airdrop.tsx | 2 +- .../token/distribution/token-distribution.tsx | 36 +- .../create/token/distribution/token-sale.tsx | 156 +++--- .../create/token/launch/launch-token.tsx | 129 +++-- .../rewards/components/claim-rewards-page.tsx | 54 ++- packages/thirdweb/src/exports/tokens.ts | 5 +- .../src/tokens/get-entrypoint-erc20.ts | 2 +- 15 files changed, 878 insertions(+), 377 deletions(-) create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/drop-erc20-token-sale.tsx diff --git a/apps/dashboard/src/@/analytics/report.ts b/apps/dashboard/src/@/analytics/report.ts index f8c628ca5e0..21dd3a67e57 100644 --- a/apps/dashboard/src/@/analytics/report.ts +++ b/apps/dashboard/src/@/analytics/report.ts @@ -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,12 +368,7 @@ export function reportAssetCreationFailed( } | { assetType: "coin"; - step: - | "deploy-contract" - | "set-claim-conditions" - | "mint-tokens" - | "airdrop-tokens" - | "approve-airdrop-tokens"; + step: CoinCreationStep; } ), ) { diff --git a/apps/dashboard/src/@/components/blocks/TokenSelector.tsx b/apps/dashboard/src/@/components/blocks/TokenSelector.tsx index a85978aec36..e6145bf8660 100644 --- a/apps/dashboard/src/@/components/blocks/TokenSelector.tsx +++ b/apps/dashboard/src/@/components/blocks/TokenSelector.tsx @@ -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} /> ); } diff --git a/apps/dashboard/src/@/hooks/project-contracts.ts b/apps/dashboard/src/@/hooks/project-contracts.ts index 984e5a821bc..736b18ab0d6 100644 --- a/apps/dashboard/src/@/hooks/project-contracts.ts +++ b/apps/dashboard/src/@/hooks/project-contracts.ts @@ -11,7 +11,12 @@ export function useAddContractToProject() { contractAddress: string; chainId: string; deploymentType: "asset" | undefined; - contractType: "ERC20Asset" | "DropERC721" | "DropERC1155" | undefined; + contractType: + | "ERC20Asset" + | "DropERC721" + | "DropERC1155" + | "DropERC20" + | undefined; }) => { const res = await apiServerProxy({ body: JSON.stringify({ diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/_common/form.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/_common/form.ts index de3daecc7eb..050ecc19ae6 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/_common/form.ts +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/_common/form.ts @@ -22,20 +22,21 @@ const priceAmountSchema = z.string().refine( return !Number.isNaN(number) && number >= 0; }, { - message: "Must be number larger than or equal to 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(), - pool: z.object({ + // sales --- + erc20Asset_poolMode: z.object({ startingPricePerToken: priceAmountSchema.refine((value) => { const numValue = Number(value); if (numValue === 0) { @@ -47,21 +48,37 @@ export const tokenDistributionFormSchema = z.object({ 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", + }, + ), }), - 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", - }, - ), - - saleMode: z.enum(["pool", "disabled"]), + 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(), + saleMode: z.enum(["erc20-asset:pool", "drop-erc20:token-drop"]), supply: z.string().min(1, "Supply is required"), }); diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page-impl.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page-impl.tsx index 0eb1fcdca23..58fc8352f71 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page-impl.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page-impl.tsx @@ -2,12 +2,23 @@ import { useRef } from "react"; import { defineChain, + getAddress, getContract, + NATIVE_TOKEN_ADDRESS, sendAndConfirmTransaction, type ThirdwebClient, + toUnits, toWei, } from "thirdweb"; -import { approve } from "thirdweb/extensions/erc20"; +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, @@ -16,13 +27,14 @@ import { } from "thirdweb/tokens"; 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 { useAddContractToProject } from "@/hooks/project-contracts"; -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"; @@ -73,7 +85,9 @@ export function CreateTokenAssetPage(props: { }); } - async function deployContract(params: { + // ERC20Asset --- + + async function Erc20Asset_deployContract(params: { values: CreateAssetFormValues; gasless: boolean; }) { @@ -89,86 +103,71 @@ export function CreateTokenAssetPage(props: { {} as Record, ); - try { - const salePercent = - params.values.saleMode === "pool" - ? Number(params.values.saleAllocationPercentage) - : 0; + const salePercent = params.values.saleEnabled + ? Number(params.values.erc20Asset_poolMode.saleAllocationPercentage) + : 0; - const saleAmount = Number(params.values.supply) * (salePercent / 100); + 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.saleMode === "pool" && saleAmount !== 0 - ? { - kind: "pool", - config: { - amount: BigInt(saleAmount), - initialTick: getInitialTickValue({ - startingPricePerToken: Number( - params.values.pool.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", - }); + 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, - }); + // 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, - }); + reportContractDeployed({ + address: contractAddress, + chainId: Number(params.values.chain), + contractName: "DropERC20", + deploymentType: "asset", + publisher: account.address, + }); - contractAddressRef.current = contractAddress; - - return { - contractAddress: contractAddress, - }; - } catch (e) { - const parsedError = parseError(e); - const errorMessage = - typeof parsedError === "string" ? parsedError : "Unknown error"; - - reportAssetCreationFailed({ - assetType: "coin", - contractType: "DropERC20", - error: errorMessage, - step: "deploy-contract", - }); + contractAddressRef.current = contractAddress; - console.error(errorMessage); - throw e; - } + return { + contractAddress: contractAddress, + }; } - async function airdropTokens(params: { + async function ERC20Asset_airdropTokens(params: { values: CreateAssetFormValues; gasless: boolean; }) { @@ -176,36 +175,23 @@ export function CreateTokenAssetPage(props: { const account = getAccount(gasless); const contract = getDeployedContract({ chain: values.chain }); - try { - const airdropTx = await distributeToken({ - chain: contract.chain, - client: props.client, - contents: values.airdropAddresses.map((recipient) => ({ - amount: BigInt(recipient.quantity), - recipient: recipient.address, - })), - tokenAddress: contract.address, - }); + const airdropTx = await distributeToken({ + chain: contract.chain, + client: props.client, + contents: values.airdropAddresses.map((recipient) => ({ + amount: BigInt(recipient.quantity), + recipient: recipient.address, + })), + tokenAddress: contract.address, + }); - 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 approveAirdropTokens(params: { + async function ERC20Asset_approveAirdropTokens(params: { values: CreateAssetFormValues; gasless: boolean; }) { @@ -218,38 +204,223 @@ export function CreateTokenAssetPage(props: { 0n, ); - try { - const entrypoint = await getDeployedEntrypointERC20({ - chain: contract.chain, - client: props.client, - }); + const entrypoint = await getDeployedEntrypointERC20({ + chain: contract.chain, + client: props.client, + }); - if (!entrypoint) { - throw new Error("Entrypoint not found"); - } + if (!entrypoint) { + throw new Error("Entrypoint not found"); + } - const approvalTx = approve({ - amountWei: toWei(totalAmountToAirdrop.toString()), - contract: contract, - spender: entrypoint.address, - }); + const approvalTx = approve({ + amountWei: toWei(totalAmountToAirdrop.toString()), + contract: contract, + spender: entrypoint.address, + }); - await sendAndConfirmTransaction({ - account, - transaction: approvalTx, - }); - } catch (e) { - const errorMessage = parseError(e); - - reportAssetCreationFailed({ - assetType: "coin", - contractType: "DropERC20", - error: errorMessage, - step: "airdrop-tokens", - }); + await sendAndConfirmTransaction({ + account, + transaction: approvalTx, + }); + } + + // DropERC20 ---- + + async function DropERC20_deployContract(params: { + values: CreateAssetFormValues; + gasless: boolean; + }) { + const { values, gasless } = params; + + const account = getAccount(gasless); - throw e; + const socialUrls = values.socialUrls.reduce( + (acc, url) => { + if (url.url && url.platform) { + acc[url.platform] = url.url; + } + return acc; + }, + {} as Record, + ); + + 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; + + // 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", + }); + + return { + contractAddress: contractAddress, + }; + } + + async function DropERC20_airdropTokens(params: { + values: CreateAssetFormValues; + gasless: boolean; + }) { + const { values, gasless } = params; + + const contract = getDeployedContract({ + chain: values.chain, + }); + + const account = getAccount(gasless); + + if (!account) { + throw new Error("No connected account"); } + + const airdropTx = transferBatch({ + batch: values.airdropAddresses.map((recipient) => ({ + amount: recipient.quantity, + to: recipient.address, + })), + contract, + }); + + await sendAndConfirmTransaction({ + account, + transaction: airdropTx, + }); + } + + async function DropERC20_mintTokens(params: { + values: CreateAssetFormValues; + gasless: boolean; + }) { + const { values, gasless } = params; + + const contract = getDeployedContract({ + chain: values.chain, + }); + + const account = getAccount(gasless); + + // poll until claim conditions are set before moving on to minting + await pollWithTimeout({ + shouldStop: async () => { + const claimConditions = await getActiveClaimCondition({ + contract, + }); + return !!claimConditions; + }, + timeoutMs: 30000, + }); + + const totalSupply = Number(values.supply); + const salePercent = values.saleEnabled + ? Number(values.dropERC20Mode.saleAllocationPercentage) + : 0; + + const ownerAndAirdropPercent = 100 - salePercent; + const ownerSupplyTokens = (totalSupply * ownerAndAirdropPercent) / 100; + + const claimTx = claimTo({ + contract, + quantity: ownerSupplyTokens.toString(), + to: account.address, + }); + + await sendAndConfirmTransaction({ + account, + transaction: claimTx, + }); + } + + async function DropERC20_setClaimConditions(params: { + values: CreateAssetFormValues; + gasless: boolean; + }) { + const { values, gasless } = params; + const contract = getDeployedContract({ + chain: values.chain, + }); + + const account = getAccount(gasless); + + const salePercent = values.saleEnabled + ? Number(values.dropERC20Mode.saleAllocationPercentage) + : 0; + + const totalSupply = Number(values.supply); + const totalSupplyWei = toUnits(totalSupply.toString(), 18); + + const phases: ClaimConditionsInput[] = [ + { + currencyAddress: + getAddress(values.dropERC20Mode.saleTokenAddress) === + getAddress(NATIVE_TOKEN_ADDRESS) + ? undefined + : values.dropERC20Mode.saleTokenAddress, + maxClaimablePerWallet: values.saleEnabled ? undefined : 0n, + maxClaimableSupply: totalSupplyWei, + metadata: { + name: + values.saleEnabled && salePercent > 0 + ? "Coin Sale phase" + : "Only Owner phase", + }, + overrideList: [ + { + address: account.address, + maxClaimable: "unlimited", + price: "0", + }, + ], + price: + values.saleEnabled && salePercent > 0 + ? values.dropERC20Mode.pricePerToken + : "0", + startTime: new Date(), + }, + ]; + + const preparedTx = setClaimConditionsExtension({ + contract, + phases, + }); + + await sendAndConfirmTransaction({ + account, + transaction: preparedTx, + }); } return ( @@ -257,9 +428,17 @@ export function CreateTokenAssetPage(props: { accountAddress={props.accountAddress} client={props.client} createTokenFunctions={{ - airdropTokens, - deployContract, - approveAirdropTokens, + ERC20Asset: { + airdropTokens: ERC20Asset_airdropTokens, + deployContract: Erc20Asset_deployContract, + approveAirdropTokens: ERC20Asset_approveAirdropTokens, + }, + DropERC20: { + deployContract: DropERC20_deployContract, + airdropTokens: DropERC20_airdropTokens, + mintTokens: DropERC20_mintTokens, + setClaimConditions: DropERC20_setClaimConditions, + }, }} onLaunchSuccess={(params) => { createTokenOnUniversalBridge({ diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page.client.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page.client.tsx index 2972bdcc1b1..ef213988ab4 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page.client.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page.client.tsx @@ -1,12 +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 type { ThirdwebClient } from "thirdweb"; +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, @@ -24,13 +36,25 @@ type CreateTokenFunctionsParams = { }; export type CreateTokenFunctions = { - deployContract: (params: CreateTokenFunctionsParams) => Promise<{ - contractAddress: string; - }>; - airdropTokens: (values: CreateTokenFunctionsParams) => Promise; - approveAirdropTokens: (values: CreateTokenFunctionsParams) => Promise; + ERC20Asset: { + deployContract: (params: CreateTokenFunctionsParams) => Promise<{ + contractAddress: string; + }>; + airdropTokens: (values: CreateTokenFunctionsParams) => Promise; + approveAirdropTokens: (values: CreateTokenFunctionsParams) => Promise; + }; + DropERC20: { + deployContract: (params: CreateTokenFunctionsParams) => Promise<{ + contractAddress: string; + }>; + setClaimConditions: (params: CreateTokenFunctionsParams) => Promise; + mintTokens: (params: CreateTokenFunctionsParams) => Promise; + airdropTokens: (params: CreateTokenFunctionsParams) => Promise; + }; }; +const nativeTokenAddress = getAddress(NATIVE_TOKEN_ADDRESS); + export function CreateTokenAssetPageUI(props: { accountAddress: string; client: ThirdwebClient; @@ -70,24 +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({ - defaultValues: { + values: { airdropAddresses: [], // airdrop airdropEnabled: false, - pool: { + erc20Asset_poolMode: { startingPricePerToken: "0.000000001", // 1gwei per token + saleAllocationPercentage: "100", + }, + dropERC20Mode: { + pricePerToken: "0.1", + saleAllocationPercentage: "100", + saleTokenAddress: nativeTokenAddress, }, - // sale fieldset - saleAllocationPercentage: "100", - saleMode: "pool", 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 (
{step === "token-info" && ( @@ -107,25 +177,44 @@ export function CreateTokenAssetPageUI(props: { /> )} - {step === "distribution" && ( - { - reportAssetCreationStepConfigured({ - assetType: "coin", - step: "token-distribution", - }); - setStep("launch"); - }} - onPrevious={() => { - setStep("token-info"); - }} - tokenSymbol={tokenInfoForm.watch("symbol")} - /> - )} + {step === "distribution" && + (distributionFieldIsPending ? ( + { + setStep("token-info"); + }, + }} + title="Coin Distribution" + > +
+ +
+
+ ) : ( + { + reportAssetCreationStepConfigured({ + assetType: "coin", + step: "token-distribution", + }); + setStep("launch"); + }} + onPrevious={() => { + setStep("token-info"); + }} + tokenSymbol={tokenInfoForm.watch("symbol")} + /> + ))} {step === "launch" && ( ; const mockCreateTokenFunctions: CreateTokenFunctions = { - airdropTokens: async () => { - await new Promise((resolve) => setTimeout(resolve, 1000)); - }, - approveAirdropTokens: async () => { - await new Promise((resolve) => setTimeout(resolve, 1000)); + 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" }; + 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: () => {}, diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/drop-erc20-token-sale.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/drop-erc20-token-sale.tsx new file mode 100644 index 00000000000..77261de2fb7 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/drop-erc20-token-sale.tsx @@ -0,0 +1,134 @@ +"use client"; + +import type { ThirdwebClient } from "thirdweb"; +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"); + return ( + +
+
+
+

Sale

+

+ Make your coin available for purchase by setting a price +

+
+ + { + 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, + }, + ); + } + }} + /> +
+ + {isEnabled && ( +
+ +
+ { + props.form.setValue( + "dropERC20Mode.saleAllocationPercentage", + value, + ); + }} + value={props.form.watch( + "dropERC20Mode.saleAllocationPercentage", + )} + /> + + % + +
+
+ + +
+ { + props.form.setValue("dropERC20Mode.pricePerToken", value); + }} + value={props.form.watch("dropERC20Mode.pricePerToken")} + className="rounded-r-none w-42" + /> + + { + props.form.setValue( + "dropERC20Mode.saleTokenAddress", + value.address, + ); + }} + selectedToken={{ + address: props.form.watch("dropERC20Mode.saleTokenAddress"), + chainId: Number(props.chainId), + }} + showCheck={true} + /> +
+
+
+ )} +
+
+ ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-airdrop.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-airdrop.tsx index 6c9cc527067..b36129add14 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-airdrop.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-airdrop.tsx @@ -88,7 +88,7 @@ export function TokenAirdropSection(props: {
{isEnabled && ( -
+
{airdropAddresses.length > 0 ? (
{/* left */} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-distribution.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-distribution.tsx index 3b340f75c95..e184db1c4ba 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-distribution.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-distribution.tsx @@ -14,6 +14,7 @@ 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"; @@ -26,14 +27,15 @@ 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 ( @@ -67,11 +69,20 @@ export function TokenDistributionFieldset(props: {
- + {form.watch("saleMode") === "drop-erc20:token-drop" ? ( + + ) : ( + + )} @@ -104,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; @@ -134,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); diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-sale.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-sale.tsx index 1c9cd420cde..57e2fa004a7 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-sale.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/distribution/token-sale.tsx @@ -1,17 +1,12 @@ "use client"; -import { useQuery } from "@tanstack/react-query"; import { DollarSignIcon, XIcon } from "lucide-react"; -import { useEffect, useState } from "react"; import type { ThirdwebClient } from "thirdweb"; -import { defineChain } from "thirdweb"; -import { isPoolRouterEnabled } from "thirdweb/tokens"; import { DistributionBarChart } from "@/components/blocks/distribution-chart"; import { FormFieldSetup } from "@/components/blocks/FormFieldSetup"; import { Badge } from "@/components/ui/badge"; import { DynamicHeight } from "@/components/ui/DynamicHeight"; import { DecimalInput } from "@/components/ui/decimal-input"; -import { Spinner } from "@/components/ui/Spinner/Spinner"; import { Select, SelectContent, @@ -27,53 +22,11 @@ export function TokenSaleSection(props: { form: TokenDistributionForm; chainId: string; client: ThirdwebClient; + isRouterEnabled: boolean; }) { - const saleMode = props.form.watch("saleMode"); const { idToChain } = useAllChainsData(); const chainMeta = idToChain.get(Number(props.chainId)); - const [hasUserUpdatedSaleMode, setHasUserUpdatedSaleMode] = useState(false); - - const isRouterEnabledQuery = useQuery({ - queryFn: async () => { - try { - return await isPoolRouterEnabled({ - // eslint-disable-next-line no-restricted-syntax - chain: defineChain(Number(props.chainId)), - client: props.client, - }); - } catch { - return false; - } - }, - queryKey: ["isRouterEnabled", props.chainId], - }); - - const isRouterEnabledValue = isRouterEnabledQuery.data === true; - - const isSaleEnabled = saleMode !== "disabled"; - - // eslint-disable-next-line no-restricted-syntax - useEffect(() => { - if (isRouterEnabledValue === false && isSaleEnabled) { - props.form.setValue("saleMode", "disabled", { - shouldValidate: true, - }); - } - }, [isRouterEnabledValue, isSaleEnabled, props.form]); - - // eslint-disable-next-line no-restricted-syntax - useEffect(() => { - if ( - isRouterEnabledValue === true && - !hasUserUpdatedSaleMode && - !isSaleEnabled - ) { - props.form.setValue("saleMode", "pool", { - shouldValidate: true, - }); - } - }, [isRouterEnabledValue, props.form, hasUserUpdatedSaleMode, isSaleEnabled]); - + const isSaleEnabled = props.form.watch("saleEnabled"); const protocolFee = 20; const leftOverFee = 100 - protocolFee; const convenienceFee = (12.5 * leftOverFee) / 100; @@ -92,42 +45,39 @@ export function TokenSaleSection(props: {

-
- {isRouterEnabledQuery.isPending ? ( - - ) : ( - { - if (!isRouterEnabledValue) { - return; - } - - setHasUserUpdatedSaleMode(true); + { + if (!props.isRouterEnabled) { + return; + } - props.form.setValue( - "saleMode", - checked ? "pool" : "disabled", - ); + props.form.setValue("saleEnabled", checked); - if (checked && !props.form.getValues("airdropEnabled")) { - props.form.setValue("saleAllocationPercentage", "100", { - shouldValidate: true, - }); - } else { - props.form.setValue("saleAllocationPercentage", "0", { - shouldValidate: true, - }); - } - }} - /> - )} -
+ 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, + }, + ); + } + }} + />
- {isRouterEnabledQuery.data === false && ( -
+ {props.isRouterEnabled === false && ( +
@@ -138,8 +88,8 @@ export function TokenSaleSection(props: { )}
- {saleMode === "pool" && isRouterEnabledQuery.data === true && ( -
+ {isSaleEnabled && props.isRouterEnabled === true && ( +
{ - props.form.setValue("saleAllocationPercentage", value, { - shouldValidate: true, - }); + props.form.setValue( + "erc20Asset_poolMode.saleAllocationPercentage", + value, + { + shouldValidate: true, + }, + ); }} - value={props.form.watch("saleAllocationPercentage")} + value={props.form.watch( + "erc20Asset_poolMode.saleAllocationPercentage", + )} /> % @@ -264,7 +225,8 @@ function PoolConfig(props: { {/* starting price */} { - props.form.setValue("pool.startingPricePerToken", value, { - shouldValidate: true, - }); + props.form.setValue( + "erc20Asset_poolMode.startingPricePerToken", + value, + { + shouldValidate: true, + }, + ); }} - value={props.form.watch("pool.startingPricePerToken")} + value={props.form.watch( + "erc20Asset_poolMode.startingPricePerToken", + )} /> {chainMeta?.nativeCurrency.symbol || "ETH"} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/launch/launch-token.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/launch/launch-token.tsx index 63534e80fee..0442270b701 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/launch/launch-token.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/launch/launch-token.tsx @@ -38,11 +38,15 @@ import type { CreateTokenFunctions } from "../create-token-page.client"; import { TokenDistributionBarChart } from "../distribution/token-distribution"; const stepIds = { - "airdrop-tokens": "airdrop-tokens", - "approve-airdrop-tokens": "approve-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; @@ -89,31 +93,63 @@ export function LaunchTokenStatus(props: { } async function handleSubmitClick() { - const initialSteps: MultiStepState[] = [ - { - id: stepIds["deploy-contract"], - label: "Deploy contract", - status: { type: "idle" }, - }, - ]; - - if (formValues.airdropEnabled && formValues.airdropAddresses.length > 0) { - initialSteps.push({ - id: stepIds["approve-airdrop-tokens"], - label: "Approve spending tokens for airdrop", - status: { type: "idle" }, - }); + if (formValues.saleMode === "erc20-asset:pool") { + const initialSteps: MultiStepState[] = [ + { + id: stepIds["erc20-asset:deploy-contract"], + label: "Deploy contract", + 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["airdrop-tokens"], - label: "Airdrop tokens", - 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); + setSteps(initialSteps); + setIsModalOpen(true); + executeSteps(initialSteps, 0, isGasless); + } else { + const initialSteps: MultiStepState[] = [ + { + 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" }, + }, + { + 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); + } } const isComplete = steps.every((step) => step.status.type === "completed"); @@ -125,13 +161,28 @@ export function LaunchTokenStatus(props: { values: formValues, }; - if (stepId === "deploy-contract") { - const result = await createTokenFunctions.deployContract(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 === "airdrop-tokens") { - await createTokenFunctions.airdropTokens(params); - } else if (stepId === "approve-airdrop-tokens") { - await createTokenFunctions.approveAirdropTokens(params); + } 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); } } @@ -161,7 +212,10 @@ export function LaunchTokenStatus(props: { reportAssetCreationFailed({ assetType: "coin", - contractType: "DropERC20", + contractType: + formValues.saleMode === "drop-erc20:token-drop" + ? "DropERC20" + : "ERC20Asset", error: errorMessage, step: currentStep.id, }); @@ -177,7 +231,10 @@ export function LaunchTokenStatus(props: { reportAssetCreationSuccessful({ assetType: "coin", - contractType: "DropERC20", + contractType: + formValues.saleMode === "drop-erc20:token-drop" + ? "DropERC20" + : "ERC20Asset", }); if (contractAddress) { diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.tsx index 36afbf4e753..7cdce2c410b 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/components/claim-rewards-page.tsx @@ -13,8 +13,7 @@ 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 { Button } from "@/components/ui/button"; -import { Spinner } from "@/components/ui/Spinner/Spinner"; +import { TransactionButton } from "@/components/tx-button"; import { useDashboardRouter } from "@/lib/DashboardRouter"; import { parseError } from "@/utils/errorParser"; import { tryCatch } from "@/utils/try-catch"; @@ -127,14 +126,14 @@ export function ClaimRewardsPageUI(props: { return (
-
+
-

+

Rewards

@@ -144,14 +143,14 @@ export function ClaimRewardsPageUI(props: {

-
-
-

Unclaimed Rewards

+
+
+

Unclaimed Rewards

The rewards that are earned but haven't been distributed yet

-
+
-
-

Reward Distribution

-

+

+

Reward Distribution

+

The unclaimed rewards will be distributed as:

@@ -196,7 +195,7 @@ export function ClaimRewardsPageUI(props: { ]} /> -
+
@@ -205,7 +204,7 @@ export function ClaimRewardsPageUI(props: { address={props.recipient} client={props.client} iconClassName="size-3" - className="h-auto py-1 text-sm" + className="h-auto py-1 text-xs" fallbackIcon={
-
+
{hasUnclaimedRewards && (

Click on "Distribute Rewards" to distribute unclaimed rewards @@ -249,18 +248,21 @@ export function ClaimRewardsPageUI(props: { There are no unclaimed rewards available for distribution

)} - +
@@ -278,13 +280,13 @@ function TokenReward(props: { chainSlug: string; }) { const fallbackIcon = ( -
+
{props.token.symbol[0]}
); return ( -
+
diff --git a/packages/thirdweb/src/exports/tokens.ts b/packages/thirdweb/src/exports/tokens.ts index b8e4186626f..d6f3dd9acfe 100644 --- a/packages/thirdweb/src/exports/tokens.ts +++ b/packages/thirdweb/src/exports/tokens.ts @@ -8,7 +8,10 @@ export { } from "../tokens/constants.js"; export { createToken } from "../tokens/create-token.js"; export { distributeToken } from "../tokens/distribute-token.js"; -export { getDeployedEntrypointERC20 } from "../tokens/get-entrypoint-erc20.js"; +export { + getDeployedContractFactory, + getDeployedEntrypointERC20, +} from "../tokens/get-entrypoint-erc20.js"; export { isPoolRouterEnabled } from "../tokens/is-router-enabled.js"; export { generateSalt, diff --git a/packages/thirdweb/src/tokens/get-entrypoint-erc20.ts b/packages/thirdweb/src/tokens/get-entrypoint-erc20.ts index 867812e970a..bfbdadae5d2 100644 --- a/packages/thirdweb/src/tokens/get-entrypoint-erc20.ts +++ b/packages/thirdweb/src/tokens/get-entrypoint-erc20.ts @@ -93,7 +93,7 @@ export async function getDeployedEntrypointERC20(options: ClientAndChain) { ); } -async function getDeployedContractFactory(options: ClientAndChain) { +export async function getDeployedContractFactory(options: ClientAndChain) { const cacheKey = `${options.chain.id}-${CONTRACT_FACTORY_DEPLOY_URL}-${JSON.stringify(options.client)}`; return withCache( async () => { From f5c875a21b1e1e82b8b76127edfbd27e7a2e4745 Mon Sep 17 00:00:00 2001 From: Manan Tank Date: Sat, 16 Aug 2025 02:06:11 +0530 Subject: [PATCH 34/35] reset token on chain change --- .../tokens/create/token/create-token-page.client.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page.client.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page.client.tsx index ef213988ab4..c4821bdbf2f 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page.client.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page.client.tsx @@ -165,7 +165,11 @@ export function CreateTokenAssetPageUI(props: { client={props.client} form={tokenInfoForm} onChainUpdated={() => { - // no op + // reset the token address to the native token address on chain change + tokenDistributionForm.setValue( + "dropERC20Mode.saleTokenAddress", + nativeTokenAddress, + ); }} onNext={() => { reportAssetCreationStepConfigured({ From 2247a73406fd171e195de7250b68f7062cf697fd Mon Sep 17 00:00:00 2001 From: Manan Tank Date: Sat, 16 Aug 2025 03:14:59 +0530 Subject: [PATCH 35/35] changes --- .../multi-step-status/multi-step-status.tsx | 6 +- .../distribution/drop-erc20-token-sale.tsx | 173 +++++++++++------- .../create/token/launch/launch-token.tsx | 12 +- 3 files changed, 119 insertions(+), 72 deletions(-) diff --git a/apps/dashboard/src/@/components/blocks/multi-step-status/multi-step-status.tsx b/apps/dashboard/src/@/components/blocks/multi-step-status/multi-step-status.tsx index 33b6002e809..4ca36385f1e 100644 --- a/apps/dashboard/src/@/components/blocks/multi-step-status/multi-step-status.tsx +++ b/apps/dashboard/src/@/components/blocks/multi-step-status/multi-step-status.tsx @@ -34,7 +34,7 @@ export function MultiStepStatus(props: { }) { return ( -
+
{props.steps.map((step) => (
{step.status.type === "completed" ? ( @@ -46,7 +46,7 @@ export function MultiStepStatus(props: { ) : ( )} -
+

(props: { {step.status.type === "error" ? props.renderError?.(step, step.status.message) || (

-

+

{step.status.message}