Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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"),
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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") {
Comment on lines +465 to +473
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Double-check callers updated to new onLaunchSuccess signature

Guard against any remaining old call sites.


🏁 Script executed:

#!/bin/bash
# Find old-style signatures/usages
rg -nP -C2 --type=ts --type=tsx '\bonLaunchSuccess\s*:\s*\(\s*params\s*:' | sed -n '1,120p'
rg -nP -C2 --type=ts --type=tsx 'onLaunchSuccess\(\s*\{' | sed -n '1,120p'

Length of output: 222


🏁 Script executed:

#!/bin/bash
# Search for all call sites and prop definitions of onLaunchSuccess
rg -n -C2 'onLaunchSuccess\s*=' -g '*.ts' -g '*.tsx'
rg -n -C2 'onLaunchSuccess\s*:' -g '*.ts' -g '*.tsx'

Length of output: 8467


Update all onLaunchSuccess call sites to new signature

  • Stories in create-token-page.stories.tsx still use onLaunchSuccess: () => {}; change to (formValues: CreateAssetFormValues, contractAddress: string) => void
  • Verify any other consumers of onLaunchSuccess (e.g. other token-page story args) accept the two-argument signature
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/token/create-token-page-impl.tsx
around lines 465 to 473, the onLaunchSuccess prop now expects two arguments
(formValues: CreateAssetFormValues, contractAddress: string) but some call sites
(notably stories in create-token-page.stories.tsx) still use a zero-arg handler;
update all onLaunchSuccess call sites to the new signature by changing handlers
like () => {} to (formValues, contractAddress) => { /* use formValues and
contractAddress or forward them */ }, and verify any other consumers (other
story args or tests) accept and forward both parameters when invoked (adjust
usages that only passed/used contractAddress to accept the form values as first
arg and update forwarding into createTokenOnUniversalBridge accordingly).

createTokenOnUniversalBridge({
chainId: Number(values.chain),
client: props.client,
tokenAddress: contractAddress,
pairedTokenAddress: undefined,
});
}

revalidatePathAction(
`/team/${props.teamSlug}/project/${props.projectId}/tokens`,
"page",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"];
Expand Down Expand Up @@ -138,6 +138,7 @@ export function CreateTokenAssetPageUI(props: {
erc20Asset_poolMode: {
startingPricePerToken: "0.000000001", // 1gwei per token
saleAllocationPercentage: "100",
tokenAddress: nativeTokenAddress,
},
dropERC20Mode: {
pricePerToken: "0.1",
Expand Down Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 *
Expand Down Expand Up @@ -189,7 +187,7 @@ function PoolConfig(props: {
</Select>
</FormFieldSetup>

<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 lg:max-w-[536px]">
<div className="flex flex-col lg:flex-row gap-4">
{/* supply % */}
<FormFieldSetup
errorMessage={
Expand All @@ -199,6 +197,7 @@ function PoolConfig(props: {
helperText={`${compactNumberFormatter.format(sellSupply)} tokens`}
isRequired
label="Sell % of Total Supply"
className="lg:max-w-[220px] grow"
>
<div className="relative">
<DecimalInput
Expand Down Expand Up @@ -230,10 +229,11 @@ function PoolConfig(props: {
}
isRequired
label="Starting price per token"
className="grow lg:max-w-sm"
>
<div className="relative">
<div className="relative grid grid-cols-2">
<DecimalInput
className="pr-10"
className="pr-10 rounded-r-none"
onChange={(value) => {
props.form.setValue(
"erc20Asset_poolMode.startingPricePerToken",
Expand All @@ -247,9 +247,29 @@ function PoolConfig(props: {
"erc20Asset_poolMode.startingPricePerToken",
)}
/>
<span className="-translate-y-1/2 absolute top-1/2 right-3 text-sm text-muted-foreground">
{chainMeta?.nativeCurrency.symbol || "ETH"}
</span>

<TokenSelector
addNativeTokenIfMissing={true}
chainId={Number(props.chainId)}
className="bg-background border-l-0 rounded-l-none"
client={props.client}
disableAddress={true}
popoverContentClassName="!w-[320px]"
onChange={(value) => {
props.form.setValue(
"erc20Asset_poolMode.tokenAddress",
value.address,
{
shouldValidate: true,
},
);
}}
selectedToken={{
address: props.form.watch("erc20Asset_poolMode.tokenAddress"),
chainId: Number(props.chainId),
}}
showCheck={true}
/>
</div>
</FormFieldSetup>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"];
Expand Down Expand Up @@ -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);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Loading