From dc3df4bc35da98d085554278e0c00cea8badd6dd Mon Sep 17 00:00:00 2001 From: MananTank Date: Thu, 28 Aug 2025 17:02:32 +0000 Subject: [PATCH] [MNY-11] Dashboard: Add ERC20 token selector for starting price in token creation flow (#7933) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ## PR-Codex overview This PR focuses on enhancing the token creation process by modifying the `onLaunchSuccess` function to accept `CreateAssetFormValues`, updating the way initial tick values are calculated, and improving the token selection interface. ### Detailed summary - Updated `onLaunchSuccess` in `launch-token.tsx` and `create-token-page.client.tsx` to accept `formValues` and `contractAddress`. - Modified `getInitialTickValue` to include `tokenAddress`, `chain`, and `client` parameters. - Added token selection functionality in the `PoolConfig` component using `TokenSelector`. - Adjusted form validation and pricing logic for token creation. - Enhanced UI layout in `PoolConfig` for better responsiveness and usability. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` ## Summary by CodeRabbit - New Features - Token selector added to choose paired currency (native or ERC‑20) and a field to specify/select token address for ERC‑20 pools. - Bridging on launch is now performed only for ERC‑20 asset pool launches. - Bug Fixes - Starting-price handling improved to account for token decimals and precompute/validate launch pricing. - Style - Responsive layout and spacing refinements for pricing and allocation inputs. --- .../tokens/create/token/_common/form.ts | 14 +---- .../create/token/create-token-page-impl.tsx | 57 ++++++++++++++----- .../create/token/create-token-page.client.tsx | 15 +++-- .../create/token/distribution/token-sale.tsx | 38 ++++++++++--- .../create/token/launch/launch-token.tsx | 13 ++--- .../create/token/utils/calculate-tick.ts | 31 +++++++++- 6 files changed, 117 insertions(+), 51 deletions(-) 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 050ecc19ae6..218fb1d2b14 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 @@ -1,7 +1,6 @@ import type { UseFormReturn } from "react-hook-form"; import * as z from "zod"; import { addressSchema, socialUrlsSchema } from "../../_common/schema"; -import { getInitialTickValue, isValidTickValue } from "../utils/calculate-tick"; export const tokenInfoFormSchema = z.object({ chain: z.string().min(1, "Chain is required"), @@ -37,17 +36,8 @@ export const tokenDistributionFormSchema = z.object({ airdropEnabled: z.boolean(), // sales --- erc20Asset_poolMode: z.object({ - startingPricePerToken: priceAmountSchema.refine((value) => { - const numValue = Number(value); - if (numValue === 0) { - return false; - } - const tick = getInitialTickValue({ - startingPricePerToken: Number(value), - }); - - return isValidTickValue(tick); - }, "Invalid price"), + startingPricePerToken: priceAmountSchema, + tokenAddress: addressSchema, saleAllocationPercentage: z.string().refine( (value) => { const number = Number(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 6fe18c3a8e6..9ec2825510f 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 @@ -38,7 +38,7 @@ import { pollWithTimeout } from "@/utils/pollWithTimeout"; import { createTokenOnUniversalBridge } from "../_apis/create-token-on-bridge"; import type { CreateAssetFormValues } from "./_common/form"; import { CreateTokenAssetPageUI } from "./create-token-page.client"; -import { getInitialTickValue } from "./utils/calculate-tick"; +import { getInitialTickValue, isValidTickValue } from "./utils/calculate-tick"; export function CreateTokenAssetPage(props: { accountAddress: string; @@ -113,6 +113,25 @@ export function CreateTokenAssetPage(props: { const chain = getChain(Number(params.values.chain)); + let initialTick: number | undefined; + + if (params.values.saleEnabled && saleAmount !== 0) { + initialTick = await getInitialTickValue({ + startingPricePerToken: Number( + params.values.erc20Asset_poolMode.startingPricePerToken, + ), + tokenAddress: params.values.erc20Asset_poolMode.tokenAddress, + chain, + client: props.client, + }); + + if (!isValidTickValue(initialTick)) { + throw new Error( + "Invalid starting price per token. Change price and try again", + ); + } + } + const contractAddress = await createToken({ account, chain: chain, @@ -123,12 +142,13 @@ export function CreateTokenAssetPage(props: { kind: "pool", config: { amount: BigInt(saleAmount), - initialTick: getInitialTickValue({ - startingPricePerToken: Number( - params.values.erc20Asset_poolMode.startingPricePerToken, - ), - }), + initialTick: initialTick, developerRewardBps: 1250, // 12.5% + currency: + getAddress(params.values.erc20Asset_poolMode.tokenAddress) === + getAddress(NATIVE_TOKEN_ADDRESS) + ? undefined + : params.values.erc20Asset_poolMode.tokenAddress, }, } : undefined, @@ -442,14 +462,23 @@ export function CreateTokenAssetPage(props: { setClaimConditions: DropERC20_setClaimConditions, }, }} - onLaunchSuccess={(params) => { - createTokenOnUniversalBridge({ - chainId: params.chainId, - client: props.client, - tokenAddress: params.contractAddress, - // TODO: UPDATE THIS WHEN WE ALLOW CUSTOM CURRENCY PAIRING - pairedTokenAddress: NATIVE_TOKEN_ADDRESS, - }); + onLaunchSuccess={(values, contractAddress) => { + if (values.saleMode === "erc20-asset:pool") { + createTokenOnUniversalBridge({ + chainId: Number(values.chain), + client: props.client, + tokenAddress: contractAddress, + pairedTokenAddress: values.erc20Asset_poolMode.tokenAddress, + }); + } else if (values.saleMode === "drop-erc20:token-drop") { + createTokenOnUniversalBridge({ + chainId: Number(values.chain), + client: props.client, + tokenAddress: contractAddress, + pairedTokenAddress: undefined, + }); + } + revalidatePathAction( `/team/${props.teamSlug}/project/${props.projectId}/tokens`, "page", 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 7bdd7c8738e..97c6649c4cd 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 @@ -59,10 +59,10 @@ export function CreateTokenAssetPageUI(props: { accountAddress: string; client: ThirdwebClient; createTokenFunctions: CreateTokenFunctions; - onLaunchSuccess: (params: { - chainId: number; - contractAddress: string; - }) => void; + onLaunchSuccess: ( + formValues: CreateAssetFormValues, + contractAddress: string, + ) => void; teamSlug: string; projectSlug: string; teamPlan: Team["billingPlan"]; @@ -138,6 +138,7 @@ export function CreateTokenAssetPageUI(props: { erc20Asset_poolMode: { startingPricePerToken: "0.000000001", // 1gwei per token saleAllocationPercentage: "100", + tokenAddress: nativeTokenAddress, }, dropERC20Mode: { pricePerToken: "0.1", @@ -166,11 +167,15 @@ export function CreateTokenAssetPageUI(props: { client={props.client} form={tokenInfoForm} onChainUpdated={() => { - // reset the token address to the native token address on chain change tokenDistributionForm.setValue( "dropERC20Mode.saleTokenAddress", nativeTokenAddress, ); + + tokenDistributionForm.setValue( + "erc20Asset_poolMode.tokenAddress", + nativeTokenAddress, + ); }} onNext={() => { reportAssetCreationStepConfigured({ 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 57e2fa004a7..73b4615ac02 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 @@ -4,6 +4,7 @@ import { DollarSignIcon, XIcon } from "lucide-react"; import type { ThirdwebClient } from "thirdweb"; import { DistributionBarChart } from "@/components/blocks/distribution-chart"; import { FormFieldSetup } from "@/components/blocks/FormFieldSetup"; +import { TokenSelector } from "@/components/blocks/TokenSelector"; import { Badge } from "@/components/ui/badge"; import { DynamicHeight } from "@/components/ui/DynamicHeight"; import { DecimalInput } from "@/components/ui/decimal-input"; @@ -147,9 +148,6 @@ function PoolConfig(props: { chainId: string; client: ThirdwebClient; }) { - const { idToChain } = useAllChainsData(); - const chainMeta = idToChain.get(Number(props.chainId)); - const totalSupply = Number(props.form.watch("supply")); const sellSupply = Math.floor( (totalSupply * @@ -189,7 +187,7 @@ function PoolConfig(props: { -
+
{/* supply % */}
-
+
{ props.form.setValue( "erc20Asset_poolMode.startingPricePerToken", @@ -247,9 +247,29 @@ function PoolConfig(props: { "erc20Asset_poolMode.startingPricePerToken", )} /> - - {chainMeta?.nativeCurrency.symbol || "ETH"} - + + { + props.form.setValue( + "erc20Asset_poolMode.tokenAddress", + value.address, + { + shouldValidate: true, + }, + ); + }} + selectedToken={{ + address: props.form.watch("erc20Asset_poolMode.tokenAddress"), + chainId: Number(props.chainId), + }} + showCheck={true} + />
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 731960b9182..b70ce5fdd92 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 @@ -56,10 +56,10 @@ export function LaunchTokenStatus(props: { values: CreateAssetFormValues; onPrevious: () => void; client: ThirdwebClient; - onLaunchSuccess: (params: { - chainId: number; - contractAddress: string; - }) => void; + onLaunchSuccess: ( + formValues: CreateAssetFormValues, + contractAddress: string, + ) => void; teamSlug: string; projectSlug: string; teamPlan: Team["billingPlan"]; @@ -251,10 +251,7 @@ export function LaunchTokenStatus(props: { }); if (contractAddressRef.current) { - props.onLaunchSuccess({ - chainId: Number(formValues.chain), - contractAddress: contractAddressRef.current, - }); + props.onLaunchSuccess(formValues, contractAddressRef.current); } } diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/utils/calculate-tick.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/utils/calculate-tick.ts index 97e53335146..a67cf4c1eed 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/utils/calculate-tick.ts +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/utils/calculate-tick.ts @@ -1,10 +1,35 @@ +import type { Chain, ThirdwebClient } from "thirdweb"; +import { getContract, NATIVE_TOKEN_ADDRESS } from "thirdweb"; +import { decimals } from "thirdweb/extensions/erc20"; +import { getAddress } from "thirdweb/utils"; + const MIN_TICK = -887200; const MAX_TICK = 887200; const TICK_SPACING = 200; -export function getInitialTickValue(params: { startingPricePerToken: number }) { - const calculatedTick = - Math.log(params.startingPricePerToken) / Math.log(1.0001); +export async function getInitialTickValue(params: { + startingPricePerToken: number; + tokenAddress: string; + chain: Chain; + client: ThirdwebClient; +}) { + const isNativeToken = + getAddress(params.tokenAddress) === getAddress(NATIVE_TOKEN_ADDRESS); + + const pairTokenDecimals = isNativeToken + ? 18 + : await decimals({ + contract: getContract({ + address: params.tokenAddress, + chain: params.chain, + client: params.client, + }), + }); + + const decimalAdjustedPrice = + params.startingPricePerToken * 10 ** (pairTokenDecimals - 18); + + const calculatedTick = Math.log(decimalAdjustedPrice) / Math.log(1.0001); // Round to nearest tick spacing const tick = Math.round(calculatedTick / TICK_SPACING) * TICK_SPACING;