From 33da8bf22c8cea7ae0e17aae9ba256f07bcbf13e Mon Sep 17 00:00:00 2001
From: MarcoMandar
Date: Fri, 9 May 2025 13:31:37 +0300
Subject: [PATCH 1/3] initial
Signed-off-by: MarcoMandar
---
.../src/components/token-sections/admin.tsx | 2 +-
packages/client/src/hooks/use-create-token.ts | 12 +-
packages/client/src/pages/create.tsx | 3495 +++--------------
packages/server/src/routes/token.ts | 62 +-
packages/server/src/util.ts | 68 +-
5 files changed, 723 insertions(+), 2916 deletions(-)
diff --git a/packages/client/src/components/token-sections/admin.tsx b/packages/client/src/components/token-sections/admin.tsx
index 7c1e8b5ce..346ccd7b6 100644
--- a/packages/client/src/components/token-sections/admin.tsx
+++ b/packages/client/src/components/token-sections/admin.tsx
@@ -1,4 +1,4 @@
-import { FormInput } from "@/pages/create";
+import { FormInput } from "@/create/forms/FormInput";
import { isFromDomain } from "@/utils";
import { fetcher } from "@/utils/api";
import { env } from "@/utils/env";
diff --git a/packages/client/src/hooks/use-create-token.ts b/packages/client/src/hooks/use-create-token.ts
index 9341dccc6..a5d89b3b0 100644
--- a/packages/client/src/hooks/use-create-token.ts
+++ b/packages/client/src/hooks/use-create-token.ts
@@ -1,6 +1,8 @@
-import { TokenMetadata } from "@/types/form.type";
+import { TokenMetadata } from "@/create/types";
import { env } from "@/utils/env";
-import { Autofun, SEED_CONFIG, useProgram } from "@/utils/program";
+import { SEED_CONFIG, useProgram } from "@/utils/program";
+import { Autofun } from "@autodotfun/types/types/autofun.ts";
+
import { launchAndSwapTx } from "@/utils/swapUtils";
import { BN, Program } from "@coral-xyz/anchor";
import { useConnection, useWallet } from "@solana/wallet-adapter-react";
@@ -127,7 +129,11 @@ const useCreateTokenMutation = () => {
"confirmed",
);
- return { mintPublicKey: mintKeypair.publicKey, userPublicKey };
+ return {
+ mintPublicKey: mintKeypair.publicKey,
+ userPublicKey,
+ signature: txId
+ };
},
});
};
diff --git a/packages/client/src/pages/create.tsx b/packages/client/src/pages/create.tsx
index c7ce4cb4a..56c0f8ca0 100644
--- a/packages/client/src/pages/create.tsx
+++ b/packages/client/src/pages/create.tsx
@@ -1,694 +1,37 @@
-import { EmptyState } from "@/components/empty-state";
+import { AutoTabContent } from "@/create/components/AutoTabContent";
+import { CreationLoadingModal } from "@/create/components/CreationLoadingModal";
+import { FormSection } from "@/create/components/FormSection";
+import { ImportTabContent } from "@/create/components/ImportTabContent";
+import { TabNavigation } from "@/create/components/TabNavigation";
+import { MAX_INITIAL_SOL, TAB_STATE_KEY } from "@/create/consts";
+import { useWallet } from "@/create/hooks/useWallet";
+import {
+ FormTab,
+ PreGeneratedTokenResponse,
+ TokenMetadata,
+ TokenSearchData,
+ UploadImportImageResponse,
+} from "@/create/types";
+import { isValidTokenAddress } from "@/create/validators";
+
+import { useImageUpload } from "@/create/hooks/useImageUpload";
+import { useTokenCreation } from "@/create/hooks/useTokenCreation";
+import { useTokenForm } from "@/create/hooks/useTokenForm";
+import { useTokenGeneration } from "@/create/hooks/useTokenGeneration";
+import { useVanityAddress } from "@/create/hooks/useVanityAddress";
import useAuthentication from "@/hooks/use-authentication";
-import { useCreateToken } from "@/hooks/use-create-token";
import { useSolBalance } from "@/hooks/use-token-balance";
-import { HomepageTokenSchema } from "@/hooks/use-tokens";
import { getAuthToken } from "@/utils/auth";
-import { env, isDevnet } from "@/utils/env";
-import { getSocket } from "@/utils/socket";
-import { useConnection, useWallet } from "@solana/wallet-adapter-react";
-import { Keypair, LAMPORTS_PER_SOL } from "@solana/web3.js";
+import { env } from "@/utils/env";
+import { useConnection } from "@solana/wallet-adapter-react";
+import { LAMPORTS_PER_SOL } from "@solana/web3.js";
import { useCallback, useEffect, useRef, useState } from "react";
-import { useNavigate } from "react-router";
+import { useNavigate } from "react-router-dom";
import { toast } from "react-toastify";
-import { Icons } from "../components/icons";
-import { TokenMetadata } from "../types/form.type";
-// Import the worker using Vite's ?worker syntax
-import InlineVanityWorker from "@/workers/vanityWorker?worker&inline"; // Added import
-
-const MAX_INITIAL_SOL = isDevnet ? 2.8 : 28;
-// Use the token supply and virtual reserves from environment or fallback to defaults
-const TOKEN_SUPPLY = Number(env.tokenSupply) || 1000000000;
-const VIRTUAL_RESERVES = Number(env.virtualReserves) || 100;
-
-// Tab types
-enum FormTab {
- AUTO = "auto",
- MANUAL = "manual",
- IMPORT = "import",
-}
-
-// LocalStorage key for tab state
-const TAB_STATE_KEY = "auto_fun_active_tab";
-
-interface UploadResponse {
- success: boolean;
- imageUrl: string;
- metadataUrl: string;
-}
-
-interface GenerateImageResponse {
- success: boolean;
- mediaUrl: string;
- remainingGenerations: number;
- resetTime: string;
-}
-
-interface PreGeneratedTokenResponse {
- success: boolean;
- token: {
- id: string;
- name: string;
- ticker: string;
- description: string;
- prompt: string;
- image?: string;
- createdAt: string;
- used: number;
- };
-}
-
-interface GenerateMetadataResponse {
- success: boolean;
- metadata: {
- name: string;
- symbol: string;
- description: string;
- prompt: string;
- };
-}
-
-interface UploadImportImageResponse {
- success: boolean;
- imageUrl: string;
-}
-
-// Define tokenData interface
-interface TokenSearchData {
- name?: string;
- symbol?: string;
- description?: string;
- creator?: string;
- creators?: string[];
- image?: string;
- mint: string;
- twitter?: string;
- telegram?: string;
- website?: string;
- discord?: string;
- metadataUri?: string;
- isCreator?: boolean;
- updateAuthority?: string;
-}
-
-// Vanity Generator Types (Copied from testing.tsx)
-type VanityResult = {
- publicKey: string;
- secretKey: Keypair; // Store the Keypair object directly
-};
-type WorkerMessage =
- | {
- type: "found";
- workerId: number;
- publicKey: string;
- secretKey: number[]; // Worker sends array
- validated: boolean;
- }
- | { type: "progress"; workerId: number; count: number }
- | { type: "error"; workerId: number; error: string };
-
-// Base58 characters
-const BASE58_REGEX = /^[1-9A-HJ-NP-Za-km-z]+$/;
-
-// Form Components
-export const FormInput = ({
- label,
- isOptional,
- error,
- leftIndicator,
- rightIndicator,
- inputTag,
- onClick,
- isLoading,
- ...props
-}: {
- label?: string;
- isOptional?: boolean;
- error?: string;
- leftIndicator?: React.ReactNode;
- rightIndicator?: React.ReactNode;
- inputTag?: React.ReactNode;
- onClick?: () => void;
- isLoading?: boolean;
- [key: string]: any;
-}) => {
- return (
-
-
- {label && (
-
- {label}
-
- )}
-
-
- {inputTag && (
-
- {inputTag}
-
- )}
- {leftIndicator && (
-
{leftIndicator}
- )}
-
- {rightIndicator && (
-
- {rightIndicator}
-
- )}
-
- {error &&
{error}
}
-
- );
-};
-
-const FormTextArea = ({
- label,
- rightIndicator,
- minRows = 3,
- maxLength,
- onClick,
- isLoading,
- ...props
-}: {
- label?: string;
- rightIndicator?: React.ReactNode;
- minRows?: number;
- maxLength?: number;
- onClick?: () => void;
- isLoading?: boolean;
- [key: string]: any;
-}) => {
- return (
-
-
- {isLoading && (
-
- )}
-
-
-
- );
-};
-
-const FormImageInput = ({
- onChange,
- onPromptChange,
- isGenerating,
- setIsGenerating,
- setGeneratingField,
- onPromptFunctionsChange,
- onPreviewChange,
- imageUrl,
- onDirectPreviewSet,
- activeTab,
- nameValue,
- onNameChange,
- tickerValue,
- onTickerChange,
-}: {
- onChange: (file: File | null) => void;
- onPromptChange: (prompt: string) => void;
- isGenerating: boolean;
- setIsGenerating: (value: boolean) => void;
- setGeneratingField: (value: string | null) => void;
- onPromptFunctionsChange: (
- setPrompt: (prompt: string) => void,
- onPromptChange: (prompt: string) => void,
- ) => void;
- onPreviewChange?: (previewUrl: string | null) => void;
- imageUrl?: string | null;
- onDirectPreviewSet?: (setter: (preview: string | null) => void) => void;
- activeTab: FormTab;
- nameValue?: string;
- onNameChange?: (value: string) => void;
- tickerValue?: string;
- onTickerChange?: (value: string) => void;
-}) => {
- const [preview, setPreview] = useState(null);
- const [prompt, setPrompt] = useState("");
- const [lastGeneratedImage, setLastGeneratedImage] = useState(
- null,
- );
- const promptDebounceRef = useRef(null);
- const hasDirectlySetPreview = useRef(false);
- const fileInputRef = useRef(null);
- const [nameInputFocused, setNameInputFocused] = useState(false);
- const [tickerInputFocused, setTickerInputFocused] = useState(false);
-
- // Expose the setPreview function to the parent component
- useEffect(() => {
- if (onDirectPreviewSet) {
- onDirectPreviewSet((preview) => {
- hasDirectlySetPreview.current = true;
- setPreview(preview);
- });
- }
- }, [onDirectPreviewSet]);
-
- // Update preview from imageUrl prop if provided
- useEffect(() => {
- if (imageUrl && !preview && !hasDirectlySetPreview.current) {
- setPreview(imageUrl);
- }
- }, [imageUrl, preview]);
-
- // Debounced prompt change handler
- const debouncedPromptChange = useCallback(
- (value: string) => {
- if (promptDebounceRef.current) {
- window.clearTimeout(promptDebounceRef.current);
- }
- promptDebounceRef.current = window.setTimeout(() => {
- onPromptChange(value);
- }, 500);
- },
- [onPromptChange],
- );
-
- // Update lastGeneratedImage only when preview changes
- useEffect(() => {
- if (preview) {
- setLastGeneratedImage(preview);
- if (onPreviewChange) {
- onPreviewChange(preview);
- }
- } else if (onPreviewChange) {
- onPreviewChange(null);
- }
- }, [preview, onPreviewChange]);
-
- // Pass prompt functions to parent only once on mount
- useEffect(() => {
- onPromptFunctionsChange(setPrompt, onPromptChange);
- }, []); // Empty dependency array since we only want this to run once
-
- const handlePromptChange = useCallback(
- (e: React.ChangeEvent) => {
- const value = e.target.value;
- setPrompt(value);
- debouncedPromptChange(value);
- },
- [debouncedPromptChange],
- );
-
- const handleCancel = useCallback(() => {
- setIsGenerating(false);
- setGeneratingField(null);
- setPreview(lastGeneratedImage);
- onChange(null);
- }, [lastGeneratedImage, onChange, setIsGenerating, setGeneratingField]);
-
- // Handle file selection
- const handleFileChange = useCallback(
- (e: React.ChangeEvent) => {
- const files = e.target.files;
- if (files && files.length > 0) {
- const file = files[0];
-
- // Check if file is an image
- if (!file.type.startsWith("image/")) {
- toast.error("Please select an image file");
- return;
- }
-
- // Check file size (limit to 5MB)
- if (file.size > 5 * 1024 * 1024) {
- toast.error(
- "File is too large. Please select an image less than 5MB.",
- );
- return;
- }
-
- // Create a preview URL
- const previewUrl = URL.createObjectURL(file);
- setPreview(previewUrl);
-
- // Pass the file to parent
- onChange(file);
- }
- },
- [onChange],
- );
-
- // Handle drag & drop
- const handleDrop = useCallback(
- (e: React.DragEvent) => {
- e.preventDefault();
- e.stopPropagation();
-
- if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
- const file = e.dataTransfer.files[0];
-
- // Check if file is an image
- if (!file.type.startsWith("image/")) {
- toast.error("Please drop an image file");
- return;
- }
-
- // Check file size (limit to 5MB)
- if (file.size > 5 * 1024 * 1024) {
- toast.error(
- "File is too large. Please select an image less than 5MB.",
- );
- return;
- }
-
- // Create a preview URL
- const previewUrl = URL.createObjectURL(file);
- setPreview(previewUrl);
-
- // Pass the file to parent
- onChange(file);
- }
- },
- [onChange],
- );
-
- const handleDragOver = useCallback((e: React.DragEvent) => {
- e.preventDefault();
- e.stopPropagation();
- }, []);
-
- // Trigger file input click
- const triggerFileInput = useCallback(() => {
- if (fileInputRef.current) {
- fileInputRef.current.click();
- }
- }, []);
-
- // Remove image
- const handleRemoveImage = useCallback(() => {
- // Only allow removing images in Manual mode
- if (activeTab === FormTab.MANUAL) {
- setPreview(null);
- onChange(null);
- if (fileInputRef.current) {
- fileInputRef.current.value = "";
- }
- }
- }, [activeTab, onChange]);
-
- // Cleanup timeout on unmount
- useEffect(() => {
- return () => {
- if (promptDebounceRef.current) {
- window.clearTimeout(promptDebounceRef.current);
- }
- };
- }, []);
-
- // Don't render anything for IMPORT tab
- if (activeTab === FormTab.IMPORT && !preview && !imageUrl) {
- return null;
- }
-
- return (
-
- {/* Image Preview Area - Square */}
-
- {isGenerating ? (
-
-
-
Generating your image...
-
- Cancel
-
-
- ) : preview || imageUrl ? (
-
-
-
- {/* Image hover overlay with X button - only for Manual mode */}
- {activeTab === FormTab.MANUAL && (
-
-
-
-
-
-
- )}
-
- {/* Gradient overlays for better text contrast */}
-
-
-
- {/* Name overlay - top left */}
- {
-
- {activeTab === FormTab.IMPORT && (
-
- {nameValue}
-
- )}
- {activeTab !== FormTab.IMPORT && (
-
- onNameChange && onNameChange(e.target.value)
- }
- placeholder="Token Name"
- maxLength={128}
- onFocus={() => setNameInputFocused(true)}
- onBlur={() => setNameInputFocused(false)}
- className={`bg-transparent text-white text-xl font-bold border-b-2 ${
- nameInputFocused ? "border-white" : "border-gray-500"
- } focus:outline-none px-1 py-0.5 w-[280px] max-w-[95%]`}
- />
- )}
-
- }
-
- {/* Ticker overlay - bottom left */}
- {onTickerChange && (
-
-
- $
- {activeTab === FormTab.IMPORT && (
-
- {tickerValue}
-
- )}
- {activeTab !== FormTab.IMPORT && (
- onTickerChange(e.target.value)}
- placeholder="TICKER"
- maxLength={16}
- onFocus={() => setTickerInputFocused(true)}
- onBlur={() => setTickerInputFocused(false)}
- className={`bg-transparent text-white text-lg font-semibold border-b-2 ${
- tickerInputFocused ? "border-white" : "border-gray-500"
- } focus:outline-none px-1 py-0.5 max-w-[60%]`}
- />
- )}
-
-
- )}
-
- ) : (
-
- {activeTab === FormTab.MANUAL ? (
- // Manual mode - File upload UI
-
-
-
- {/* Placeholder logo when empty */}
-
-
-
- ) : (
- // Auto mode - Prompt text area
-
- )}
-
- )}
-
-
- );
-};
-
-// Image upload function
-const uploadImage = async (metadata: TokenMetadata) => {
- // Determine a safe filename based on token metadata
- const safeName = metadata.name.toLowerCase().replace(/[^a-z0-9]/g, "_");
-
- // Get the image type from the data URL
- const contentType =
- metadata.imageBase64?.match(/^data:([A-Za-z-+/]+);base64,/)?.[1] || "";
-
- // Determine file extension from content type
- let extension = ".jpg"; // Default
- if (contentType.includes("png")) extension = ".png";
- else if (contentType.includes("gif")) extension = ".gif";
- else if (contentType.includes("svg")) extension = ".svg";
- else if (contentType.includes("webp")) extension = ".webp";
-
- const filename = `${safeName}${extension}`;
-
- console.log(
- `Uploading image as ${filename} with content type ${contentType}`,
- );
-
- // Get auth token from localStorage with quote handling
- const authToken = getAuthToken();
-
- // Prepare headers
- const headers: Record = {
- "Content-Type": "application/json",
- };
-
- if (authToken) {
- headers["Authorization"] = `Bearer ${authToken}`;
- }
-
- const response = await fetch(env.apiUrl + "/api/upload", {
- method: "POST",
- headers,
- credentials: "include",
- body: JSON.stringify({
- image: metadata.imageBase64,
- metadata: {
- name: metadata.name,
- symbol: metadata.symbol,
- description: metadata.description,
- twitter: metadata.links.twitter,
- telegram: metadata.links.telegram,
- website: metadata.links.website,
- discord: metadata.links.discord,
- },
- }),
- });
-
- if (!response.ok) {
- // Specifically handle authentication errors
- if (response.status === 401) {
- throw new Error(
- "Authentication required. Please connect your wallet and try again.",
- );
- }
- throw new Error("Failed to upload image: " + (await response.text()));
- }
-
- const result = (await response.json()) as UploadResponse;
-
- // Verify metadata URL exists, if not create a fallback
- if (!result.metadataUrl || result.metadataUrl === "undefined") {
- console.warn("No metadata URL returned from server, using fallback URL");
-
- // Generate a fallback URL using the mint address or a UUID
- result.metadataUrl = env.getMetadataUrl(
- metadata.tokenMint || crypto.randomUUID(),
- );
- }
-
- return result;
-};
-
-const waitForTokenCreation = async (mint: string, timeout = 80_000) => {
- return new Promise((resolve, reject) => {
- const socket = getSocket();
-
- const newTokenListener = (token: unknown) => {
- const { mint: newMint } = HomepageTokenSchema.parse(token);
- if (newMint === mint) {
- clearTimeout(timerId);
- socket.off("newToken", newTokenListener);
- resolve();
- }
- };
-
- socket.emit("subscribeGlobal");
- socket.on("newToken", newTokenListener);
-
- const timerId = setTimeout(() => {
- socket.off("newToken", newTokenListener);
- reject(new Error("Token creation timed out"));
- }, timeout);
- });
-};
-// Main Form Component
export default function Create() {
- // Define things for our page
- const navigate = useNavigate();
const { isAuthenticated } = useAuthentication();
- const { publicKey, signTransaction } = useWallet();
+ const { publicKey, signTransaction, error: walletError } = useWallet();
const { connection } = useConnection();
const [solBalance, setSolBalance] = useState(0);
@@ -702,20 +45,17 @@ export default function Create() {
checkBalance();
}, [publicKey, connection]);
- // State for image upload
+ const navigate = useNavigate();
+
const [imageFile, setImageFile] = useState(null);
- const [showCoinDrop, setShowCoinDrop] = useState(false);
const [coinDropImageUrl, setCoinDropImageUrl] = useState(null);
- const [isSubmitting, setIsSubmitting] = useState(false);
const [isGenerating, setIsGenerating] = useState(false);
const [generatingField, setGeneratingField] = useState(null);
const [promptFunctions, setPromptFunctions] = useState<{
setPrompt: ((prompt: string) => void) | null;
onPromptChange: ((prompt: string) => void) | null;
}>({ setPrompt: null, onPromptChange: null });
- const { mutateAsync: createTokenOnChainAsync } = useCreateToken();
- // Import-related state
const [isImporting, setIsImporting] = useState(false);
const [importStatus, setImportStatus] = useState<{
type: "success" | "error" | "warning";
@@ -723,7 +63,6 @@ export default function Create() {
} | null>(null);
const [hasStoredToken, setHasStoredToken] = useState(false);
- // Tab state - initialize from localStorage or default to AUTO
const [activeTab, setActiveTab] = useState(() => {
const savedTab = localStorage.getItem(TAB_STATE_KEY);
if (savedTab && Object.values(FormTab).includes(savedTab as FormTab)) {
@@ -734,70 +73,44 @@ export default function Create() {
const [userPrompt, setUserPrompt] = useState("");
const [isProcessingPrompt, setIsProcessingPrompt] = useState(false);
- // --- Vanity Generator State --- (Copied and adapted)
- const [vanitySuffix, setVanitySuffix] = useState("FUN"); // Default FUN
- const [isGeneratingVanity, setIsGeneratingVanity] = useState(false);
- const [vanityResult, setVanityResult] = useState(null);
- const [displayedPublicKey, setDisplayedPublicKey] = useState(
- "--- Generate a vanity address ---",
- ); // Placeholder
- const [suffixError, setSuffixError] = useState(null);
- const workersRef = useRef([]);
- const startTimeRef = useRef(null);
- const displayUpdateIntervalRef = useRef(null);
- // Optional: For displaying stats
- // const [totalAttempts, setTotalAttempts] = useState(0);
- // const [generationRate, setGenerationRate] = useState(0);
- // const attemptBatchRef = useRef(0);
-
- // Effect to clear import token data if not in import tab
+ const {
+ vanitySuffix,
+ setVanitySuffix,
+ isGeneratingVanity,
+ vanityResult,
+ displayedPublicKey,
+ suffixError,
+ startVanityGeneration,
+ stopVanityGeneration,
+ } = useVanityAddress();
+
useEffect(() => {
- if (activeTab !== FormTab.IMPORT) {
- localStorage.removeItem("import_token_data");
- setHasStoredToken(false);
+ if (
+ activeTab !== FormTab.IMPORT &&
+ !isGeneratingVanity &&
+ !vanityResult &&
+ vanitySuffix.trim() &&
+ !suffixError
+ ) {
+ const timeoutId = setTimeout(() => {
+ startVanityGeneration();
+ }, 500);
+
+ return () => clearTimeout(timeoutId);
}
- }, [activeTab]);
+ }, [
+ activeTab,
+ isGeneratingVanity,
+ vanityResult,
+ vanitySuffix,
+ suffixError,
+ startVanityGeneration,
+ ]);
- // Effect to check imported token data and wallet authorization when wallet changes
useEffect(() => {
- if (activeTab === FormTab.IMPORT && publicKey) {
- const storedTokenData = localStorage.getItem("import_token_data");
- if (storedTokenData) {
- try {
- // const tokenData = JSON.parse(storedTokenData) as TokenSearchData;
- // // Check if the current wallet is authorized to create this token
- // // In dev mode, always allow any wallet to register
- // const isCreatorWallet =
- // tokenData.isCreator !== undefined
- // ? tokenData.isCreator
- // : (tokenData.updateAuthority &&
- // tokenData.updateAuthority === publicKey.toString()) ||
- // (tokenData.creators &&
- // tokenData.creators.includes(publicKey.toString()));
- // // Update import status based on wallet authorization
- // if (!isCreatorWallet) {
- // setImportStatus({
- // type: "warning",
- // message:
- // "Please connect with the token's creator wallet to register it.",
- // });
- // } else {
- // // Success message - different in dev mode if not the creator
- // const message =
- // "Successfully loaded token data for " + tokenData.name;
- // setImportStatus({
- // type: "success",
- // message,
- // });
- // }
- } catch (error) {
- console.error("Error parsing stored token data:", error);
- }
- }
- }
- }, [activeTab, publicKey]);
+ stopVanityGeneration();
+ }, [activeTab, stopVanityGeneration]);
- // Effect to populate form with token data if it exists
useEffect(() => {
if (activeTab === FormTab.IMPORT) {
const storedTokenData = localStorage.getItem("import_token_data");
@@ -806,7 +119,6 @@ export default function Create() {
const tokenData = JSON.parse(storedTokenData) as TokenSearchData;
setHasStoredToken(true);
- // Populate the form with token data
setForm((prev) => ({
...prev,
name: tokenData.name || tokenData.mint.slice(0, 8),
@@ -821,12 +133,9 @@ export default function Create() {
},
}));
- // Set the image preview if available - use a small timeout to ensure the ref is set
if (tokenData.image) {
- // Set image URL directly to handle refresh cases
setCoinDropImageUrl(tokenData.image || null);
- // Use a small timeout to ensure the ref is available after render
setTimeout(() => {
if (previewSetterRef.current) {
previewSetterRef.current(tokenData.image || null);
@@ -840,24 +149,35 @@ export default function Create() {
}
}, [activeTab]);
- // Simple form state
- const [form, setForm] = useState({
- name: "",
- symbol: "",
- description: "",
- prompt: "",
- initialSol: "0",
- links: {
- twitter: "",
- telegram: "",
- website: "",
- discord: "",
- farcaster: "",
+ const {
+ form,
+ errors: formErrors,
+ handleChange: handleFormChange,
+ validateForm,
+ isFormValid: checkFormValid,
+ setForm,
+ setErrors: setFormErrors,
+ } = useTokenForm({
+ onFormChange: (newForm) => {
+ if (activeTab === FormTab.AUTO) {
+ setAutoForm((prev) => ({
+ ...prev,
+ name: newForm.name,
+ symbol: newForm.symbol,
+ description: newForm.description,
+ prompt: newForm.prompt,
+ }));
+ } else if (activeTab === FormTab.MANUAL) {
+ setManualForm((prev) => ({
+ ...prev,
+ name: newForm.name,
+ symbol: newForm.symbol,
+ description: newForm.description,
+ }));
+ }
},
- importAddress: "",
});
- // Separate state for Auto and Manual modes
const [autoForm, setAutoForm] = useState({
name: "",
symbol: "",
@@ -874,17 +194,15 @@ export default function Create() {
imageFile: null as File | null,
});
- // Add state to track token ID for deletion when creating
const [currentPreGeneratedTokenId, setCurrentPreGeneratedTokenId] = useState<
string | null
>(null);
- const [buyValue, setBuyValue] = useState(form.initialSol || 0);
+ const [buyValue, setBuyValue] = useState(form.initialSol || "0");
const balance = useSolBalance();
const maxUserSol = balance ? Math.max(0, Number(balance) - 0.025) : 0;
- // Use the smaller of MAX_INITIAL_SOL or the user's max available SOL
const maxInputSol = Math.min(MAX_INITIAL_SOL, maxUserSol);
const insufficientBalance =
@@ -892,30 +210,11 @@ export default function Create() {
? false
: Number(buyValue) > Number(balance || 0) - 0.05;
- // Error state
- const [errors, setErrors] = useState({
- name: "",
- symbol: "",
- description: "",
- prompt: "",
- initialSol: "",
- userPrompt: "",
- importAddress: "",
- percentage: "",
- });
-
- // Store a reference to the FormImageInput's setPreview function
const previewSetterRef = useRef<((preview: string | null) => void) | null>(
null,
);
- // Create ref to track image URL creation to prevent infinite loops
- const hasCreatedUrlFromImage = useRef(false);
-
- // Add state to track if token has been generated in AUTO mode
const [hasGeneratedToken, setHasGeneratedToken] = useState(false);
-
- // Update the form from the appropriate mode-specific form when switching tabs
useEffect(() => {
if (activeTab === FormTab.AUTO) {
setForm((prev) => ({
@@ -926,7 +225,6 @@ export default function Create() {
prompt: autoForm.prompt,
}));
- // Set the image from auto form if available
if (autoForm.imageUrl && previewSetterRef.current) {
previewSetterRef.current(autoForm.imageUrl);
setCoinDropImageUrl(autoForm.imageUrl);
@@ -939,43 +237,13 @@ export default function Create() {
description: manualForm.description,
}));
- // Set the image file if available
if (manualForm.imageFile) {
setImageFile(manualForm.imageFile);
}
}
- // Reset vanity state when switching tabs
stopVanityGeneration();
- setVanityResult(null);
- setDisplayedPublicKey("--- Generate a vanity address ---");
- setSuffixError(null);
- }, [activeTab]); // Added stopVanityGeneration
-
- // Automatically start vanity generation when in non-import tab
- useEffect(() => {
- // Only auto-start if:
- // 1. Not on Import tab
- // 2. Not already generating
- // 3. Don't have a result yet
- // 4. Have a valid suffix (default or user-entered)
- if (
- activeTab !== FormTab.IMPORT &&
- !isGeneratingVanity &&
- !vanityResult &&
- vanitySuffix.trim() &&
- !suffixError
- ) {
- // Short delay to allow UI to render first
- const timeoutId = setTimeout(() => {
- startVanityGeneration();
- }, 500);
+ }, [activeTab, stopVanityGeneration]);
- return () => clearTimeout(timeoutId);
- }
- // Note: We're ignoring the linter warnings about dependencies here
- }, [activeTab, isGeneratingVanity, vanityResult, vanitySuffix, suffixError]);
-
- // Update mode-specific state when main form changes
useEffect(() => {
if (activeTab === FormTab.AUTO) {
setAutoForm((prev) => ({
@@ -995,17 +263,13 @@ export default function Create() {
}
}, [form, activeTab]);
- // Keep SOL and percentage values in sync
useEffect(() => {
- // Update buyValue when form.initialSol changes
- if (form.initialSol !== buyValue.toString()) {
+ if (form.initialSol !== buyValue) {
setBuyValue(form.initialSol);
}
}, [form.initialSol]);
- // Handle tab switching
const handleTabChange = (tab: FormTab) => {
- // Save current form values to appropriate mode-specific state
if (activeTab === FormTab.AUTO && tab !== FormTab.AUTO) {
setAutoForm((prev) => ({
...prev,
@@ -1024,17 +288,44 @@ export default function Create() {
}));
}
- // When switching to AUTO or MANUAL, clear any imported token data
if (tab === FormTab.AUTO || tab === FormTab.MANUAL) {
localStorage.removeItem("import_token_data");
setHasStoredToken(false);
+ } else if (tab === FormTab.IMPORT) {
+ const storedTokenData = localStorage.getItem("import_token_data");
+ if (storedTokenData) {
+ try {
+ const tokenData = JSON.parse(storedTokenData) as TokenSearchData;
+ setHasStoredToken(true);
+ setForm((prev) => ({
+ ...prev,
+ name: tokenData.name || tokenData.mint.slice(0, 8),
+ symbol: tokenData.symbol || "TOKEN",
+ description: tokenData.description || "Imported token",
+ links: {
+ ...prev.links,
+ twitter: tokenData.twitter || "",
+ telegram: tokenData.telegram || "",
+ website: tokenData.website || "",
+ discord: tokenData.discord || "",
+ },
+ }));
+
+ if (tokenData.image) {
+ setCoinDropImageUrl(tokenData.image);
+ if (previewSetterRef.current) {
+ previewSetterRef.current(tokenData.image);
+ }
+ }
+ } catch (error) {
+ console.error("Error parsing stored token data:", error);
+ setHasStoredToken(false);
+ }
+ }
}
- // When switching to Manual mode, clear the image regardless of previous tab
if (tab === FormTab.MANUAL) {
- // Clear the imageFile state
setImageFile(null);
- // Clear the preview in FormImageInput
if (previewSetterRef.current) {
previewSetterRef.current(null);
}
@@ -1042,17 +333,13 @@ export default function Create() {
}
setActiveTab(tab);
-
- // Save tab to localStorage
localStorage.setItem(TAB_STATE_KEY, tab);
- // Reset token generation status when switching away from AUTO
if (tab !== FormTab.AUTO) {
setHasGeneratedToken(false);
}
- // Clear errors
- setErrors({
+ setFormErrors({
name: "",
symbol: "",
description: "",
@@ -1064,371 +351,157 @@ export default function Create() {
});
};
- // Handle input changes
- const handleChange = (field: string, value: string) => {
- // Handle nested fields (for links)
- if (field.includes(".")) {
- const [parent, child] = field.split(".");
- setForm((prev) => {
- if (parent === "links") {
- return {
- ...prev,
- links: {
- ...prev.links,
- [child]: value,
- },
- };
- }
- return prev;
- });
- } else {
- setForm((prev) => ({
+ const handlePromptChange = (prompt: string) => {
+ setForm((prev) => ({
+ ...prev,
+ prompt: prompt,
+ }));
+
+ if (prompt) {
+ setFormErrors((prev) => ({
...prev,
- [field]: value,
+ prompt: "",
}));
}
+ };
- // Clear errors immediately when field has a value
- if (field === "name" || field === "symbol" || field === "description") {
- if (value) {
- setErrors((prev) => ({
- ...prev,
- [field]: "",
- }));
- } else {
- setErrors((prev) => ({
- ...prev,
- [field]: `${field.charAt(0) + field.slice(1)} is required`,
- }));
- }
- }
-
- // Validate initialSol
- if (field === "initialSol" && value) {
- const numValue = parseFloat(value);
- if (numValue < 0 || numValue > MAX_INITIAL_SOL) {
- setErrors((prev) => ({
- ...prev,
- initialSol: `Max initial SOL is ${MAX_INITIAL_SOL}`,
- }));
- } else {
- setErrors((prev) => ({
- ...prev,
- initialSol: "",
- }));
- }
- }
- };
-
- // Update the handleChange function to handle prompt changes specially
- const handlePromptChange = (prompt: string) => {
- setForm((prev) => ({
- ...prev,
- prompt: prompt,
- }));
-
- // Clear errors immediately when field has a value
- if (prompt) {
- setErrors((prev) => ({
+ const {
+ isGenerating: tokenGenerationIsGenerating,
+ generationProgress,
+ generatedMetadata,
+ currentImageUrl,
+ currentImageFile,
+ generateToken,
+ resetGeneration,
+ } = useTokenGeneration({
+ onGenerationComplete: (metadata) => {
+ setAutoForm((prev) => ({
...prev,
- prompt: "",
+ name: metadata.name,
+ symbol: metadata.symbol,
+ description: metadata.description,
+ prompt: userPrompt,
}));
- }
- };
-
- // Create token on-chain
- const createTokenOnChain = async (
- tokenMetadata: TokenMetadata,
- mintKeypair: Keypair,
- metadataUrl: string,
- ) => {
- try {
- if (!publicKey) {
- throw new Error("Wallet not connected");
- }
-
- if (!signTransaction) {
- throw new Error("Wallet doesn't support signing");
- }
-
- // Ensure we have a valid metadata URL
- if (!metadataUrl || metadataUrl === "undefined" || metadataUrl === "") {
- console.warn(
- "No metadata URL provided, generating minimal metadata...",
- );
-
- // Upload minimal metadata
- const uploadResult = await uploadImage(tokenMetadata);
- metadataUrl = uploadResult.metadataUrl;
- }
-
- console.log("Creating token on-chain with parameters:", {
- name: tokenMetadata.name,
- symbol: tokenMetadata.symbol,
- metadataUrl: metadataUrl,
- mintKeypair: {
- publicKey: mintKeypair.publicKey.toString(),
- secretKeyLength: mintKeypair.secretKey.length,
- },
- });
-
- // Use the useCreateToken hook to create the token on-chain
- const tx = await createTokenOnChainAsync({
- tokenMetadata,
- metadataUrl,
- mintKeypair,
- });
-
- // Handle transaction cancellation
- if (!tx) {
- // Trigger the flush animation
- if (window.flushCoins) {
- window.flushCoins();
- }
- return;
- }
-
- const txId = mintKeypair.publicKey.toString();
- return txId;
- } catch (error) {
- console.error("Error creating token on-chain:", error);
-
- // Check if it's a deserialization error (0x66 / 102)
- if (
- error instanceof Error &&
- (error.message.includes("custom program error: 0x66") ||
- error.message.includes("InstructionDidNotDeserialize") ||
- error.message.includes("Error Number: 102"))
- ) {
- console.error(
- "Transaction failed due to instruction deserialization error.",
- );
- console.error(
- "This is likely due to parameter mismatch with the on-chain program.",
- );
-
- // Trigger the flush animation on error
- if (window.flushCoins) {
- window.flushCoins();
- }
-
- throw new Error(
- "Failed to create token: instruction format mismatch with on-chain program. Please try again or contact support.",
- );
+ },
+ onError: (error) => {
+ toast.error(error);
+ },
+ onImageUrlUpdate: (imageUrl, imageFile) => {
+ setAutoForm((prev) => ({
+ ...prev,
+ imageUrl,
+ }));
+ if (previewSetterRef.current) {
+ previewSetterRef.current(imageUrl);
}
-
- // Trigger the flush animation on error
- if (window.flushCoins) {
- window.flushCoins();
+ setCoinDropImageUrl(imageUrl);
+ if (imageFile) {
+ setImageFile(imageFile);
}
+ },
+ });
- throw new Error("Failed to create token on-chain");
- }
- };
-
- // Generate token based on user prompt
const generateFromPrompt = useCallback(async () => {
if (!userPrompt.trim()) {
- setErrors((prev) => ({
+ setFormErrors((prev) => ({
...prev,
userPrompt: "Please enter a prompt",
}));
return;
}
- setErrors((prev) => ({
+ setFormErrors((prev) => ({
...prev,
userPrompt: "",
}));
setIsProcessingPrompt(true);
+ setIsGenerating(true);
+ setGeneratingField("name,symbol,description,prompt");
try {
- // Get auth token from localStorage with quote handling
- const authToken = getAuthToken();
-
- // Prepare headers
- const headers: Record = {
- "Content-Type": "application/json",
- };
-
- if (authToken) {
- headers["Authorization"] = `Bearer ${authToken}`;
- }
-
- const response = await fetch(
- env.apiUrl + "/api/generation/generate-metadata",
- {
- method: "POST",
- headers,
- credentials: "include",
- body: JSON.stringify({
- prompt: userPrompt,
- fields: ["name", "symbol", "description", "prompt"],
- }),
- },
- );
-
- if (!response.ok) {
- throw new Error("Failed to generate metadata from prompt");
- }
-
- const data = (await response.json()) as GenerateMetadataResponse;
-
- if (!data.success || !data.metadata) {
- throw new Error("Invalid response from the metadata generation API");
- }
-
- setForm((prev) => ({
- ...prev,
- name: data.metadata.name,
- symbol: data.metadata.symbol,
- description: data.metadata.description,
- prompt: data.metadata.prompt,
- }));
-
- // Also update autoForm
- setAutoForm((prev) => ({
- ...prev,
- name: data.metadata.name,
- symbol: data.metadata.symbol,
- description: data.metadata.description,
- prompt: data.metadata.prompt,
- concept: userPrompt,
- }));
-
- // Set the prompt text so it can be reused
- if (promptFunctions.setPrompt) {
- promptFunctions.setPrompt(data.metadata.prompt);
- } else {
- console.warn("promptFunctions.setPrompt is not available");
- }
-
- if (promptFunctions.onPromptChange) {
- promptFunctions.onPromptChange(data.metadata.prompt);
- } else {
- console.warn("promptFunctions.onPromptChange is not available");
+ // Reset any existing image state
+ setImageFile(null);
+ setCoinDropImageUrl(null);
+ if (previewSetterRef.current) {
+ previewSetterRef.current(null);
}
- // Temporarily set the generating state
- setIsGenerating(true);
- setGeneratingField("prompt");
-
- const imageResponse = await fetch(
- env.apiUrl + "/api/generation/generate",
- {
- method: "POST",
- headers,
- credentials: "include",
- body: JSON.stringify({
- prompt: data.metadata.prompt,
- type: "image",
- }),
- },
+ await generateToken(userPrompt);
+ } catch (error) {
+ console.error("Error generating from prompt:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Failed to generate token from prompt. Please try again.",
);
+ } finally {
+ setIsProcessingPrompt(false);
+ setIsGenerating(false);
+ setGeneratingField(null);
+ }
+ }, [userPrompt, generateToken]);
- if (!imageResponse.ok) {
- const errorText = await imageResponse.text();
- console.error("Image generation API returned an error:", errorText);
- const backendError = JSON.parse(errorText).error;
-
- let userErrorMessage = "Failed to generate image for token.";
+ const generateAll = useCallback(
+ async (
+ setPrompt?: ((prompt: string) => void) | null,
+ onPromptChange?: ((prompt: string) => void) | null,
+ ) => {
+ try {
+ setIsGenerating(true);
+ setGeneratingField("name,symbol,description,prompt");
- if (backendError.includes("NSFW")) {
- userErrorMessage =
- "Your input contains inappropriate content. Please modify and try again.";
+ // Reset any existing image state
+ setImageFile(null);
+ setCoinDropImageUrl(null);
+ if (previewSetterRef.current) {
+ previewSetterRef.current(null);
}
- throw new Error(userErrorMessage);
- }
- const imageData = (await imageResponse.json()) as GenerateImageResponse;
+ const randomConcept = "Generate a unique and creative token";
+ await generateToken(randomConcept);
- if (!imageData.success || !imageData.mediaUrl) {
- console.error("Invalid image data:", imageData);
- throw new Error("Image generation API returned invalid data");
- }
-
- try {
- const imageBlob = await fetch(imageData.mediaUrl).then((r) => {
- if (!r.ok)
- throw new Error(
- `Failed to fetch image: ${r.status} ${r.statusText}`,
- );
- return r.blob();
- });
- const imageFile = new File([imageBlob], "generated-image.png", {
- type: "image/png",
- });
-
- // Reset the flag before setting the new image file
- hasCreatedUrlFromImage.current = false;
- setImageFile(imageFile);
- const previewUrl = URL.createObjectURL(imageBlob);
- setCoinDropImageUrl(previewUrl);
-
- // Update autoForm with the image URL
- setAutoForm((prev) => ({
- ...prev,
- imageUrl: previewUrl,
- }));
+ if (setPrompt) setPrompt(randomConcept);
+ if (onPromptChange) onPromptChange(randomConcept);
- // Directly update the preview in FormImageInput
- if (previewSetterRef.current) {
- previewSetterRef.current(previewUrl);
- } else {
- console.warn("previewSetterRef.current is not available");
- }
- } catch (imageError) {
- console.error("Error processing generated image:", imageError);
- throw new Error("Failed to process the generated image");
+ setUserPrompt(randomConcept);
+ } catch (error) {
+ console.error("Error generating metadata:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Failed to generate metadata. Please try again.",
+ );
} finally {
setIsGenerating(false);
setGeneratingField(null);
}
+ },
+ [generateToken],
+ );
- // Set hasGeneratedToken to true after successful generation
- setHasGeneratedToken(true);
- } catch (error) {
- console.error("Error generating from prompt:", error);
- // Reset generating state in case of error
- setIsGenerating(false);
- setGeneratingField(null);
- toast.error(
- error instanceof Error
- ? error.message
- : "Failed to generate token from prompt. Please try again.",
- );
- } finally {
- setIsProcessingPrompt(false);
+ const base64ToBlob = (base64: string, type: string) => {
+ const byteString = atob(base64.split(",")[1]);
+ const ab = new ArrayBuffer(byteString.length);
+ const ia = new Uint8Array(ab);
+
+ for (let i = 0; i < byteString.length; i++) {
+ ia[i] = byteString.charCodeAt(i);
}
- }, [
- userPrompt,
- setErrors,
- setIsProcessingPrompt,
- setForm,
- setAutoForm,
- promptFunctions,
- setImageFile,
- setCoinDropImageUrl,
- setIsGenerating,
- setGeneratingField,
- previewSetterRef,
- hasCreatedUrlFromImage,
- createTokenOnChainAsync,
- ]);
- // Import token from address
- const importTokenFromAddress = async () => {
- // Validate the address
+ return new Blob([ab], { type });
+ };
+
+ const loadTokenData = async () => {
if (!isValidTokenAddress(form.importAddress)) {
- setErrors((prev) => ({
+ setFormErrors((prev) => ({
...prev,
importAddress: "Please enter a valid token address",
}));
return;
}
- setErrors((prev) => ({
+ setFormErrors((prev) => ({
...prev,
importAddress: "",
}));
@@ -1437,15 +510,12 @@ export default function Create() {
setImportStatus(null);
try {
- // Ensure wallet is connected
if (!publicKey) {
throw new Error("Wallet not connected");
}
- // Get auth token from localStorage with quote handling
const authToken = getAuthToken();
- // Prepare headers
const headers: Record = {
"Content-Type": "application/json",
};
@@ -1455,7 +525,6 @@ export default function Create() {
}
try {
- // Fetch token data from a special search endpoint that can find any token
const response = await fetch(`${env.apiUrl}/api/search-token`, {
method: "POST",
headers,
@@ -1487,9 +556,8 @@ export default function Create() {
const tokenData = (await response.json()) as TokenSearchData & {
isToken2022?: boolean;
- }; // Add isToken2022 to type assertion
+ };
- // If token has an image URL, fetch and convert to base64
if (tokenData.image) {
try {
const imageResponse = await fetch(tokenData.image);
@@ -1500,11 +568,9 @@ export default function Create() {
});
setImageFile(imageFile);
- // Create preview URL
const previewUrl = URL.createObjectURL(imageBlob);
setCoinDropImageUrl(previewUrl);
- // Upload the image to our storage
const uploadResponse = await fetch(
`${env.apiUrl}/api/upload-import-image`,
{
@@ -1526,7 +592,6 @@ export default function Create() {
(await uploadResponse.json()) as UploadImportImageResponse;
if (data.success && data.imageUrl) {
tokenData.image = data.imageUrl;
- // Update the form state with the new image URL
setCoinDropImageUrl(data.imageUrl);
if (previewSetterRef.current) {
previewSetterRef.current(data.imageUrl);
@@ -1539,12 +604,9 @@ export default function Create() {
}
}
- // Store token data in localStorage for later use
- // Include the isToken2022 flag
localStorage.setItem("import_token_data", JSON.stringify(tokenData));
setHasStoredToken(true);
- // Populate the form with token data
setForm((prev) => ({
...prev,
name: tokenData.name || form.importAddress.slice(0, 8),
@@ -1559,7 +621,6 @@ export default function Create() {
},
}));
- // Success message - ready to register
setImportStatus({
type: "success",
message:
@@ -1580,653 +641,34 @@ export default function Create() {
setImportStatus({
type: "error",
message:
- error instanceof Error ? error.message : "Failed to import token",
- });
- } finally {
- setIsImporting(false);
- }
- };
-
- // Function to validate a token address (Solana address is 32-44 characters, base58)
- const isValidTokenAddress = (address: string): boolean => {
- if (!address || address.trim().length < 32 || address.trim().length > 44) {
- return false;
- }
-
- // Check if it's valid base58 (Solana addresses use base58)
- return BASE58_REGEX.test(address.trim());
- };
-
- // Handle paste in the import address field
- const handleImportAddressPaste = (
- e: React.ClipboardEvent,
- ) => {
- const pastedText = e.clipboardData.getData("text");
-
- if (!isValidTokenAddress(pastedText)) {
- // Prevent default paste if invalid
- e.preventDefault();
-
- setErrors((prev) => ({
- ...prev,
- importAddress:
- "Invalid token address format. Please check and try again.",
- }));
-
- return false;
- }
-
- // Clear any previous errors when pasting valid address
- setErrors((prev) => ({
- ...prev,
- importAddress: "",
- }));
-
- return true;
- };
-
- // Check if form is valid
- const isFormValid =
- !!form.name &&
- !!form.symbol &&
- !!form.description &&
- !errors.name &&
- !errors.symbol &&
- !errors.description &&
- !errors.initialSol;
-
- // Update coinDropImageUrl directly when we have a preview URL
- const handlePreviewChange = useCallback((previewUrl: string | null) => {
- setCoinDropImageUrl(previewUrl);
- }, []);
-
- // Function to generate all fields
- const generateAll = useCallback(
- async (
- setPrompt?: ((prompt: string) => void) | null,
- onPromptChange?: ((prompt: string) => void) | null,
- ) => {
- try {
- setIsGenerating(true);
- setGeneratingField("name,symbol,description,prompt");
-
- // Get auth token from localStorage with quote handling
- const authToken = getAuthToken();
-
- // Prepare headers
- const headers: Record = {
- "Content-Type": "application/json",
- };
-
- if (authToken) {
- headers["Authorization"] = `Bearer ${authToken}`;
- }
-
- // Get a pre-generated token
- const response = await fetch(
- env.apiUrl + "/api/generation/pre-generated-token",
- {
- method: "GET",
- headers,
- credentials: "include",
- },
- );
-
- if (!response.ok) {
- throw new Error("Failed to get pre-generated token");
- }
-
- const data = (await response.json()) as PreGeneratedTokenResponse;
- const { token } = data;
-
- console.log("token", token);
-
- // Store token ID for later use when creating
- if (token.id) {
- setCurrentPreGeneratedTokenId(token.id);
- }
-
- // Update forms with generated data
- setForm((prev) => ({
- ...prev,
- name: token.name,
- symbol: token.ticker,
- description: token.description,
- prompt: token.prompt,
- }));
-
- // Update auto form
- setAutoForm((prev) => ({
- ...prev,
- name: token.name,
- symbol: token.ticker,
- description: token.description,
- prompt: token.prompt,
- concept: token.prompt,
- }));
-
- // Set user prompt
- setUserPrompt(token.prompt);
-
- // Set the prompt text so it can be reused
- if (setPrompt) setPrompt(token.prompt);
- if (onPromptChange) onPromptChange(token.prompt);
- // If we have an image URL, use it directly
- if (token.image) {
- // Transform R2 URLs to use local endpoint if needed
- let imageUrl = token.image;
- if (imageUrl.includes("r2.dev")) {
- // Extract the filename from the R2 URL
- const filename = imageUrl.split("/").pop();
- // Use local endpoint instead
- imageUrl = `${env.apiUrl}/api/image/${filename}`;
- }
-
- const imageBlob = await fetch(imageUrl).then((r) => r.blob());
- const imageFile = new File([imageBlob], "generated-image.png", {
- type: "image/png",
- });
- setImageFile(imageFile);
-
- // Create a preview URL
- const previewUrl = URL.createObjectURL(imageBlob);
- setCoinDropImageUrl(previewUrl);
- setAutoForm((prev) => ({
- ...prev,
- imageUrl: previewUrl,
- }));
-
- // Update preview in FormImageInput
- if (previewSetterRef.current) {
- previewSetterRef.current(previewUrl);
- }
- } else {
- // If no image, generate one using the prompt
- const imageResponse = await fetch(
- env.apiUrl + "/api/generation/generate",
- {
- method: "POST",
- headers,
- credentials: "include",
- body: JSON.stringify({
- prompt: token.prompt,
- type: "image",
- }),
- },
- );
-
- if (!imageResponse.ok) {
- throw new Error("Failed to generate image");
- }
-
- const imageData =
- (await imageResponse.json()) as GenerateImageResponse;
- const imageUrl = imageData.mediaUrl;
-
- // Convert image URL to File object
- const imageBlob = await fetch(imageUrl).then((r) => r.blob());
- const imageFile = new File([imageBlob], "generated-image.png", {
- type: "image/png",
- });
- setImageFile(imageFile);
-
- // Create a preview URL
- const previewUrl = URL.createObjectURL(imageBlob);
- setCoinDropImageUrl(previewUrl);
- setAutoForm((prev) => ({
- ...prev,
- imageUrl: previewUrl,
- }));
-
- // Update preview in FormImageInput
- if (previewSetterRef.current) {
- previewSetterRef.current(previewUrl);
- }
- }
-
- // Set token as generated
- setHasGeneratedToken(true);
- } catch (error) {
- console.error("Error generating metadata:", error);
- toast.error(
- error instanceof Error
- ? error.message
- : "Failed to generate metadata. Please try again.",
- );
- } finally {
- setIsGenerating(false);
- setGeneratingField(null);
- }
- },
- [setIsGenerating, setGeneratingField],
- );
-
- // --- Vanity Generation Functions --- (Copied and adapted)
- const stopVanityGeneration = useCallback(() => {
- if (!isGeneratingVanityRef.current) return; // Use ref to check if actually running
- setIsGeneratingVanity(false); // Set state immediately
- workersRef.current.forEach((worker) => {
- try {
- worker.postMessage("stop"); // Attempt graceful stop
- } catch (e) {
- console.warn("Couldn't send stop message to worker", e);
- }
- // Terminate after a delay, ensures state update propagates
- setTimeout(() => {
- try {
- worker.terminate();
- } catch (e) {
- /* ignore */
- }
- }, 100);
- });
- workersRef.current = [];
- startTimeRef.current = null;
- if (displayUpdateIntervalRef.current) {
- clearInterval(displayUpdateIntervalRef.current);
- displayUpdateIntervalRef.current = null;
- }
- }, []); // Removed isGeneratingVanity and vanityResult dependency
-
- const startVanityGeneration = useCallback(() => {
- const suffix = vanitySuffix.trim();
- setVanityResult(null); // <-- Add this line to clear the previous result
- setDisplayedPublicKey("Generating..."); // Reset display immediately
-
- let currentError = null;
-
- // 1. Validation
- if (!suffix) {
- currentError = "Suffix cannot be empty.";
- } else if (suffix.length > 5) {
- currentError = "Suffix cannot be longer than 5 characters.";
- } else if (!BASE58_REGEX.test(suffix)) {
- currentError =
- "Invalid suffix. Base58 must be used. This includes: numbers 1-9, letters A-H, J-N, P-Z, a-k, m-z.";
- }
-
- // 2. Warnings
- if (!currentError) {
- if (suffix.length === 5) {
- currentError = "Warning: 5-letter suffix may take 24+ hours to find!";
- toast.warn(currentError);
- } else if (suffix.length === 4) {
- currentError = "Note: 4-letter suffix may take some time to find.";
- toast.info(currentError);
- }
- }
-
- setSuffixError(currentError);
- if (
- currentError &&
- !currentError.startsWith("Warning") &&
- !currentError.startsWith("Note")
- ) {
- return; // Stop if it's a blocking error
- }
-
- // Stop previous generation if any
- stopVanityGeneration();
-
- setIsGeneratingVanity(true);
-
- const numWorkers =
- navigator.hardwareConcurrency > 12
- ? 8
- : navigator.hardwareConcurrency || 4;
- startTimeRef.current = Date.now();
- workersRef.current = [];
-
- // Start rolling display effect
- const base58Chars =
- "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
- const generateRandomString = (length: number) => {
- let result = "";
- for (let i = 0; i < length; i++) {
- result += base58Chars.charAt(
- Math.floor(Math.random() * base58Chars.length),
- );
- }
- return result;
- };
-
- displayUpdateIntervalRef.current = setInterval(() => {
- // Generate a random prefix matching Solana address length minus suffix length
- const prefixLength = 44 - suffix.length;
- const randomPrefix = generateRandomString(prefixLength);
- setDisplayedPublicKey(`${randomPrefix}${suffix}`);
- }, 100); // Update display frequently
-
- for (let i = 0; i < numWorkers; i++) {
- try {
- const worker = new InlineVanityWorker();
- worker.onmessage = (event: MessageEvent) => {
- // Check generation state *inside* the handler
- if (!isGeneratingVanityRef.current) return;
-
- const data = event.data;
- switch (data.type) {
- case "found":
- if (data.validated) {
- // Construct Keypair from secret key array
- const secretKeyUint8Array = new Uint8Array(data.secretKey);
- if (secretKeyUint8Array.length !== 64) {
- console.error(
- "Worker sent invalid secret key length:",
- secretKeyUint8Array.length,
- );
- // Handle error - maybe try next result? For now, stop.
- stopVanityGeneration();
- setSuffixError("Received invalid key from generator.");
- return;
- }
- const foundKeypair = Keypair.fromSecretKey(secretKeyUint8Array);
-
- // Double-check the derived public key matches
- if (foundKeypair.publicKey.toString() !== data.publicKey) {
- console.error(
- "Public key mismatch between worker and derived key!",
- );
- setSuffixError("Key validation mismatch.");
- stopVanityGeneration(); // Stop on critical error
- return;
- }
-
- setVanityResult({
- publicKey: data.publicKey,
- secretKey: foundKeypair,
- });
- setDisplayedPublicKey(data.publicKey); // Show final result
- stopVanityGeneration(); // Stop all workers
- } else {
- console.warn(
- `Worker ${data.workerId} found potential match but validation failed.`,
- );
- // Keep searching
- }
- break;
- case "progress":
- // Optional: Update stats
- // attemptBatchRef.current += data.count;
- break;
- case "error":
- console.error(`Worker ${data.workerId} error: ${data.error}`);
- // Optional: Stop all if one worker fails critically?
- // stopVanityGeneration();
- break;
- }
- };
-
- worker.onerror = (err: any) => {
- console.error(`Worker ${i} fatal error:`, err);
- setSuffixError(`Worker ${i} failed: ${err.message || "Unknown"}`);
- workersRef.current = workersRef.current.filter((w) => w !== worker);
- if (
- workersRef.current.length === 0 &&
- isGeneratingVanityRef.current
- ) {
- setSuffixError("All vanity generators failed!");
- stopVanityGeneration();
- }
- try {
- worker.terminate();
- } catch (e) {
- /* ignore */
- }
- };
-
- worker.postMessage({ suffix, workerId: i });
- workersRef.current.push(worker);
- } catch (workerError) {
- console.error(`Failed to create worker ${i}:`, workerError);
- setSuffixError(`Failed to start generator worker ${i}.`);
- }
- }
-
- if (workersRef.current.length === 0) {
- setSuffixError("Could not start any vanity generator workers.");
- setIsGeneratingVanity(false);
- setDisplayedPublicKey("--- Error starting generator ---");
- if (displayUpdateIntervalRef.current) {
- clearInterval(displayUpdateIntervalRef.current);
- displayUpdateIntervalRef.current = null;
- }
- startTimeRef.current = null;
- }
- }, [vanitySuffix, stopVanityGeneration]); // Removed isGeneratingVanity as we use ref
-
- // Use a ref for isGeneratingVanity inside callbacks to avoid stale closures
- const isGeneratingVanityRef = useRef(isGeneratingVanity);
- useEffect(() => {
- isGeneratingVanityRef.current = isGeneratingVanity;
- }, [isGeneratingVanity]);
-
- // submitFormToBackend import
- const submitImportFormToBackend = async () => {
- try {
- setIsSubmitting(true);
-
- if (!publicKey) {
- throw new Error("Wallet not connected");
- }
-
- // Check if we're working with imported token data
- const storedTokenData = localStorage.getItem("import_token_data");
- if (storedTokenData) {
- const tokenData = JSON.parse(storedTokenData) as TokenSearchData & {
- isToken2022?: boolean;
- }; // Add type assertion here too
- try {
- // Convert image to base64 if exists
- let media_base64: string | null = null;
- if (imageFile) {
- media_base64 = await new Promise((resolve) => {
- const reader = new FileReader();
- reader.onloadend = () => resolve(reader.result as string);
- reader.readAsDataURL(imageFile);
- });
- }
-
- // Get auth token from localStorage with quote handling
- const authToken = getAuthToken();
-
- // Prepare headers
- const headers: Record = {
- "Content-Type": "application/json",
- };
-
- if (authToken) {
- headers["Authorization"] = `Bearer ${authToken}`;
- }
-
- // Create token with the imported data
- const createResponse = await fetch(env.apiUrl + "/api/create-token", {
- method: "POST",
- headers,
- credentials: "include",
- body: JSON.stringify({
- tokenMint: tokenData.mint,
- mint: tokenData.mint,
- name: form.name,
- symbol: form.symbol,
- description: form.description,
- twitter: form.links.twitter,
- telegram: form.links.telegram,
- website: form.links.website,
- discord: form.links.discord,
- imageBase64: media_base64,
- metadataUrl: tokenData.metadataUri || "",
- creator:
- tokenData.creators || // Use updateAuthority/creator from search result
- tokenData.updateAuthority ||
- tokenData.creator || // Fallback to creator if others missing
- "", // Or handle error if no creator found
- imported: true,
- isToken2022: tokenData.isToken2022 === true, // <<< Pass the flag
- }),
- });
-
- if (!createResponse.ok) {
- const errorData = (await createResponse.json()) as {
- error?: string;
- };
- throw new Error(errorData.error || "Failed to create token entry");
- }
-
- // Clear imported token data from localStorage
- localStorage.removeItem("import_token_data");
- setHasStoredToken(false);
-
- // Trigger confetti to celebrate successful registration
- if (window.createConfettiFireworks) {
- window.createConfettiFireworks();
- }
-
- // Redirect to token page
- navigate(`/token/${tokenData.mint}`);
- return;
- } catch (error) {
- if (
- error instanceof Error &&
- error.message.includes("Token already exists")
- ) {
- navigate(`/token/${tokenData.mint}`);
- return;
- }
-
- if (error instanceof Error) {
- throw error; // Re-throw if it's a permission error
- }
- }
- }
- } catch (error) {
- console.error("Error submitting import form:", error);
- } finally {
- setIsSubmitting(false);
- }
- };
- // Submit form to backend
- const submitFormToBackend = async () => {
- try {
- // --- Check for Vanity Keypair --- START
- if (!vanityResult?.publicKey || !vanityResult?.secretKey) {
- toast.error("Please generate a vanity address first.");
- return;
- }
- const mintKeypair = vanityResult.secretKey;
- const tokenMint = vanityResult.publicKey;
-
- setIsCreating(true);
- setCreationStage("initializing");
- setCreationStep("Preparing token creation...");
-
- setIsSubmitting(true);
-
- if (!publicKey) {
- throw new Error("Wallet not connected");
- }
-
- // Check if we're working with imported token data - ONLY do this check for IMPORT tab
- const storedTokenData = localStorage.getItem("import_token_data");
- if (storedTokenData && activeTab === FormTab.IMPORT) {
- const tokenData = JSON.parse(storedTokenData);
- try {
- // Check if the current wallet has permission to create this token
- // In dev mode, skip this check and allow any wallet to register
- const isCreatorNow =
- (tokenData.updateAuthority &&
- tokenData.updateAuthority === publicKey.toString()) ||
- (tokenData.creators &&
- tokenData.creators.includes(publicKey.toString()));
-
- console.log("Creator wallet check result:", isCreatorNow);
- console.log("Token update authority:", tokenData.updateAuthority);
- console.log("Token creators:", tokenData.creators);
-
- // if (!isCreatorNow) {
- // throw new Error(
- // "You need to connect with the token's creator wallet to register it",
- // );
- // }
-
- // Show coin drop animation
- setShowCoinDrop(true);
-
- // Get auth token from localStorage with quote handling
- const authToken = getAuthToken();
-
- // Prepare headers
- const headers: Record = {
- "Content-Type": "application/json",
- };
-
- if (authToken) {
- headers["Authorization"] = `Bearer ${authToken}`;
- }
-
- // Create token record via API
- const createResponse = await fetch(env.apiUrl + "/api/create-token", {
- method: "POST",
- headers,
- credentials: "include",
- body: JSON.stringify({
- tokenMint: tokenData.mint,
- mint: tokenData.mint,
- name: form.name,
- symbol: form.symbol,
- description: form.description,
- twitter: form.links.twitter,
- telegram: form.links.telegram,
- website: form.links.website,
- discord: form.links.discord,
- imageUrl: tokenData.image || "",
- metadataUrl: tokenData.metadataUri || "",
- // Include the import flag to indicate this is an imported token
- imported: false,
- }),
- });
-
- if (!createResponse.ok) {
- const errorData = (await createResponse.json()) as {
- error?: string;
- };
- throw new Error(errorData.error || "Failed to create token entry");
- }
+ error instanceof Error ? error.message : "Failed to import token",
+ });
+ } finally {
+ setIsImporting(false);
+ }
+ };
- // Clear imported token data from localStorage
- localStorage.removeItem("import_token_data");
- setHasStoredToken(false);
+ const importToken = async () => {
+ if (!hasStoredToken) {
+ toast.error("Please load token data via the import field above.");
+ return;
+ }
- // Trigger confetti to celebrate successful registration
- if (window.createConfettiFireworks) {
- window.createConfettiFireworks();
- }
+ try {
+ setIsSubmitting(true);
- // Redirect to token page
- navigate(`/token/${tokenData.mint}`);
- return;
- } catch (error) {
- if (
- error instanceof Error &&
- error.message.includes("Token already exists")
- ) {
- navigate(`/token/${tokenData.mint}`);
- return;
- }
- console.error("Error handling imported token:", error);
- if (error instanceof Error) {
- throw error; // Re-throw if it's a permission error
- }
- }
+ if (!publicKey) {
+ throw new Error("Wallet not connected");
}
- // For AUTO and MANUAL tabs, we proceed with the regular token creation flow
+ const storedTokenData = localStorage.getItem("import_token_data");
+ if (!storedTokenData) {
+ throw new Error("No token data found");
+ }
- // --- Fetch Vanity Keypair from Backend --- REMOVED
- // let tokenMint: string; // Defined above from vanityResult
- // let mintKeypair: Keypair; // Defined above from vanityResult
- // try { ... } catch (error) { ... }
- // --- Fetch Vanity Keypair from Backend --- REMOVED
+ const tokenData = JSON.parse(storedTokenData) as TokenSearchData & {
+ isToken2022?: boolean;
+ };
// Convert image to base64 if exists
let media_base64: string | null = null;
@@ -2238,256 +680,113 @@ export default function Create() {
});
}
- // Create token metadata
- const tokenMetadata: TokenMetadata = {
- name: form.name,
- symbol: form.symbol,
- description: form.description,
- initialSol: parseFloat(form.initialSol) || 0,
- links: {
- ...form.links,
- },
- imageBase64: media_base64 || null,
- tokenMint, // Use client-generated mint address
- decimals: 9,
- supply: 1000000000000000,
- freezeAuthority: publicKey?.toBase58() || "",
- mintAuthority: publicKey?.toBase58() || "",
+ const authToken = getAuthToken();
+ const headers: Record = {
+ "Content-Type": "application/json",
};
- // First, uploadImage if needed
- let imageUrl = "";
- let metadataUrl = "";
-
- // Show coin drop with the image we have
- setShowCoinDrop(true);
-
- if (media_base64) {
- try {
- const uploadResult = await uploadImage({
- ...tokenMetadata,
- tokenMint,
- });
- imageUrl = uploadResult.imageUrl;
- metadataUrl = uploadResult.metadataUrl;
-
- // Verify metadata URL is valid
- if (!metadataUrl || metadataUrl === "undefined") {
- console.error(
- "Upload succeeded but metadata URL is invalid:",
- metadataUrl,
- );
- // Fallback: generate a unique metadata URL based on mint address
- metadataUrl = env.getMetadataUrl(tokenMint);
- }
-
- // Update the coin drop image to use the final uploaded URL
- if (imageUrl) {
- setCoinDropImageUrl(imageUrl);
- }
- } catch (uploadError) {
- console.error("Error uploading image:", uploadError);
- throw new Error("Failed to upload token image");
- }
- } else if (activeTab === FormTab.IMPORT && coinDropImageUrl) {
- // For imported tokens, use the image URL directly
- imageUrl = coinDropImageUrl;
-
- // Generate a metadata URL if none exists
- if (!metadataUrl) {
- metadataUrl = env.getMetadataUrl(tokenMint);
- console.log(
- "Using default metadata URL for imported token:",
- metadataUrl,
- );
- }
- } else if (!media_base64 && !metadataUrl) {
- // No image provided, generate minimal metadata URL
- metadataUrl = env.getMetadataUrl(tokenMint);
- }
-
- // Double-check that we have a valid metadata URL
- if (!metadataUrl) {
- console.warn("No metadata URL set, using fallback");
- metadataUrl = env.getMetadataUrl(tokenMint);
- }
-
- // Create token on-chain
- try {
- console.log("Creating token on-chain...");
- // Pass the client-generated mintKeypair
- await createTokenOnChain(tokenMetadata, mintKeypair, metadataUrl);
- } catch (onChainError) {
- console.error("Error creating token on-chain:", onChainError);
-
- // Format a user-friendly error message
- let errorMessage = "Failed to create token on-chain";
-
- if (onChainError instanceof Error) {
- // Handle deserialization errors specially
- if (
- onChainError.message.includes("instruction format mismatch") ||
- onChainError.message.includes("InstructionDidNotDeserialize") ||
- onChainError.message.includes("custom program error: 0x66")
- ) {
- errorMessage =
- "The token creation transaction was rejected by the blockchain. This may be due to a temporary program upgrade. Please try again in a few minutes.";
-
- // Try again with different parameters
- try {
- // Wait a moment before retrying
- await new Promise((resolve) => setTimeout(resolve, 1000));
- toast.info(
- "Retrying token creation with different parameters...",
- );
-
- // Modify token metadata for retry
- const retryMetadata = {
- ...tokenMetadata,
- decimals: 9, // Ensure decimals is 9
- supply: 1000000000000000, // Set explicit supply
- };
- // Pass the client-generated mintKeypair again
- await createTokenOnChain(retryMetadata, mintKeypair, metadataUrl);
- } catch (retryError) {
- console.error("Retry also failed:", retryError);
- // Continue to error handling below
- throw new Error(errorMessage);
- }
- } else {
- // Include original error message for other types of errors
- errorMessage = `Failed to create token on-chain: ${onChainError.message}`;
- }
- }
-
- throw new Error(errorMessage);
+ if (authToken) {
+ headers["Authorization"] = `Bearer ${authToken}`;
}
- // If we have a pre-generated token ID, mark it as used and remove duplicates
- if (currentPreGeneratedTokenId && activeTab === FormTab.AUTO) {
- try {
- // Get auth token from localStorage with quote handling
- const authToken = getAuthToken();
-
- // Prepare headers
- const headers: Record = {
- "Content-Type": "application/json",
- };
-
- if (authToken) {
- headers["Authorization"] = `Bearer ${authToken}`;
- }
+ const createResponse = await fetch(env.apiUrl + "/api/create-token", {
+ method: "POST",
+ headers,
+ credentials: "include",
+ body: JSON.stringify({
+ tokenMint: tokenData.mint,
+ mint: tokenData.mint,
+ name: form.name,
+ symbol: form.symbol,
+ description: form.description,
+ twitter: form.links.twitter,
+ telegram: form.links.telegram,
+ website: form.links.website,
+ discord: form.links.discord,
+ imageBase64: media_base64,
+ metadataUrl: tokenData.metadataUri || "",
+ creator:
+ tokenData.creators || // Use updateAuthority/creator from search result
+ tokenData.updateAuthority ||
+ tokenData.creator || // Fallback to creator if others missing
+ "", // Or handle error if no creator found
+ imported: true,
+ isToken2022: tokenData.isToken2022 === true,
+ }),
+ });
- // Mark the token as used and delete any other tokens with the same name or ticker
- await fetch(env.apiUrl + "/api/generation/mark-token-used", {
- method: "POST",
- headers,
- credentials: "include",
- body: JSON.stringify({
- id: currentPreGeneratedTokenId,
- name: form.name,
- ticker: form.symbol,
- concept: activeTab === FormTab.AUTO ? userPrompt : null,
- }),
- });
- } catch (error) {
- console.error("Error marking pre-generated token as used:", error);
- // Continue with token creation even if this fails
- }
+ if (!createResponse.ok) {
+ const errorData = (await createResponse.json()) as {
+ error?: string;
+ };
+ throw new Error(errorData.error || "Failed to create token entry");
}
- await waitForTokenCreation(tokenMint);
+ // Clear imported token data from localStorage
+ localStorage.removeItem("import_token_data");
+ setHasStoredToken(false);
- // Trigger confetti to celebrate successful minting
+ // Trigger confetti to celebrate successful registration
if (window.createConfettiFireworks) {
window.createConfettiFireworks();
}
- // Clear imported token data from localStorage if it exists
- localStorage.removeItem("import_token_data");
- setHasStoredToken(false);
-
- navigate(`/token/${tokenMint}`); // Use client-generated mint
-
- // After transaction confirmation
- setCreationStage("confirming");
- setCreationStep("Waiting for wallet confirmation...");
-
- // After token creation
- setCreationStage("creating");
- setCreationStep("Creating token on-chain...");
+ // Redirect to token page
+ navigate(`/token/${tokenData.mint}`);
+ } catch (error) {
+ console.error("Error submitting import form:", error);
- // After token record creation
- setCreationStage("validating");
- setCreationStep("Validating transaction...");
+ if (
+ error instanceof Error &&
+ error.message.includes("Token already exists")
+ ) {
+ const storedTokenData = localStorage.getItem("import_token_data");
+ if (storedTokenData) {
+ const tokenData = JSON.parse(storedTokenData) as TokenSearchData;
+ navigate(`/token/${tokenData.mint}`);
+ return;
+ }
+ }
- // After validation
- setCreationStage("finalizing");
- setCreationStep("Finalizing token setup...");
- } catch (error) {
- console.error("Error creating token:", error);
toast.error(
error instanceof Error
? error.message
- : "Failed to create token. Please try again.",
+ : "Failed to import token. Please try again.",
);
- setIsCreating(false);
- setCreationStep("");
- setCreationStage("initializing");
- // Ensure coin drop is hidden on error
- setShowCoinDrop(false);
} finally {
setIsSubmitting(false);
}
};
- // Handle form submission
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
- // Validate required fields
- const newErrors = { ...errors };
- if (!form.name) newErrors.name = "Name is required";
- if (!form.symbol) newErrors.symbol = "Symbol is required";
- if (!form.description) newErrors.description = "Description is required";
-
- // Validate vanity keypair generation for non-import tabs
- if (activeTab !== FormTab.IMPORT && (!vanityResult || isGeneratingVanity)) {
- toast.error("Please generate and wait for a vanity address.");
+ if (!validateForm()) {
return;
}
- // Validate SOL balance - skip this check for imported tokens
- if (
- isAuthenticated &&
- insufficientBalance &&
- !(activeTab === FormTab.IMPORT)
- ) {
- newErrors.initialSol =
- "Insufficient SOL balance (need 0.05 SOL for fees)";
- toast.error("You don't have enough SOL to create this token");
+ if (activeTab === FormTab.IMPORT) {
+ await importToken();
+ return;
}
- // Check if there are any errors
- if (
- newErrors.name ||
- newErrors.symbol ||
- newErrors.description ||
- newErrors.initialSol
- ) {
- setErrors(newErrors);
+ if (!vanityResult || isGeneratingVanity) {
+ toast.error("Please generate and wait for a vanity address.");
return;
}
- // Submit form to backend
- if (activeTab !== FormTab.IMPORT) {
- await submitFormToBackend();
- } else {
- await submitImportFormToBackend();
+ if (isAuthenticated && insufficientBalance) {
+ setFormErrors((prev) => ({
+ ...prev,
+ initialSol: "Insufficient SOL balance (need 0.05 SOL for fees)",
+ }));
+ toast.error("You don't have enough SOL to create this token");
+ return;
}
+
+ await submitFormToBackend(e);
};
- // Fetch pre-generated token on mount for Auto mode
useEffect(() => {
const loadPreGeneratedToken = async () => {
if (activeTab === FormTab.AUTO && !hasGeneratedToken) {
@@ -2495,10 +794,8 @@ export default function Create() {
setIsGenerating(true);
setGeneratingField("name,symbol,description,prompt");
- // Get auth token from localStorage with quote handling
const authToken = getAuthToken();
- // Prepare headers
const headers: Record = {
"Content-Type": "application/json",
};
@@ -2507,7 +804,6 @@ export default function Create() {
headers["Authorization"] = `Bearer ${authToken}`;
}
- // Get a pre-generated token
const response = await fetch(
env.apiUrl + "/api/generation/pre-generated-token",
{
@@ -2524,15 +820,12 @@ export default function Create() {
const data = (await response.json()) as PreGeneratedTokenResponse;
const { token } = data;
- // Store token ID for later use when creating
if (token.id) {
setCurrentPreGeneratedTokenId(token.id);
}
- // Set user prompt with the concept
setUserPrompt(token.prompt);
- // Update forms with generated data
setForm((prev) => ({
...prev,
name: token.name,
@@ -2550,24 +843,19 @@ export default function Create() {
concept: token.prompt,
}));
- // Set prompt functions
if (promptFunctions.setPrompt)
promptFunctions.setPrompt(token.prompt);
if (promptFunctions.onPromptChange)
promptFunctions.onPromptChange(token.prompt);
- // If token has an image, load it
if (token.image) {
- // Transform R2 URLs to use local endpoint if needed
let imageUrl = token.image;
if (
imageUrl.includes("r2.dev") &&
env.apiUrl?.includes("localhost") &&
env.apiUrl?.includes("127.0.0.1")
) {
- // Extract the filename from the R2 URL
const filename = imageUrl.split("/").pop();
- // Use local endpoint instead
imageUrl = `${env.apiUrl}/api/image/${filename}`;
}
@@ -2576,10 +864,8 @@ export default function Create() {
type: "image/png",
});
- // Set image file
setImageFile(imageFile);
- // Create preview URL
const previewUrl = URL.createObjectURL(imageBlob);
setCoinDropImageUrl(previewUrl);
setAutoForm((prev) => ({
@@ -2587,13 +873,11 @@ export default function Create() {
imageUrl: previewUrl,
}));
- // Update preview in FormImageInput
if (previewSetterRef.current) {
previewSetterRef.current(previewUrl);
}
}
- // Set the token as generated since we loaded it from pre-generated
setHasGeneratedToken(true);
} catch (error) {
console.error("Error loading pre-generated token:", error);
@@ -2612,17 +896,13 @@ export default function Create() {
hasGeneratedToken,
]);
- // When switching tabs, ensure image state is properly separated
useEffect(() => {
if (activeTab === FormTab.AUTO) {
- // When switching to Auto, load the auto image
if (autoForm.imageUrl && previewSetterRef.current) {
previewSetterRef.current(autoForm.imageUrl);
setCoinDropImageUrl(autoForm.imageUrl);
}
} else if (activeTab === FormTab.MANUAL) {
- // Manual mode should always start clean (image was already cleared in handleTabChange)
- // Only set the image if manualForm has an imageFile from previous Manual session
if (manualForm.imageFile) {
const manualImageUrl = URL.createObjectURL(manualForm.imageFile);
setImageFile(manualForm.imageFile);
@@ -2631,7 +911,6 @@ export default function Create() {
}
setCoinDropImageUrl(manualImageUrl);
} else {
- // Ensure everything is cleared for Manual mode
setImageFile(null);
if (previewSetterRef.current) {
previewSetterRef.current(null);
@@ -2639,12 +918,10 @@ export default function Create() {
setCoinDropImageUrl(null);
}
} else if (activeTab === FormTab.IMPORT && hasStoredToken) {
- // Import tab should only set image from stored token data
const storedTokenData = localStorage.getItem("import_token_data");
if (storedTokenData) {
try {
const tokenData = JSON.parse(storedTokenData) as TokenSearchData;
- // Set the image if available
if (tokenData.image && previewSetterRef.current) {
previewSetterRef.current(tokenData.image);
setCoinDropImageUrl(tokenData.image);
@@ -2654,9 +931,18 @@ export default function Create() {
}
}
}
- }, [activeTab, autoForm.imageUrl, manualForm.imageFile, hasStoredToken]);
- // Update manualForm when imageFile changes in Manual mode
+ stopVanityGeneration();
+ setVanitySuffix("FUN");
+ }, [
+ activeTab,
+ autoForm.imageUrl,
+ manualForm.imageFile,
+ hasStoredToken,
+ stopVanityGeneration,
+ setVanitySuffix,
+ ]);
+
useEffect(() => {
if (activeTab === FormTab.MANUAL && imageFile) {
setManualForm((prev) => ({
@@ -2666,115 +952,70 @@ export default function Create() {
}
}, [imageFile, activeTab]);
- // Helper function to calculate token amount based on SOL input using bonding curve formula
- const calculateTokensFromSol = (solAmount: number): number => {
- // Convert SOL to lamports
- const lamports = solAmount * 1e9;
-
- // Using constant product formula: (dx * y) / (x + dx)
- // where x is virtual reserves, y is token supply, dx is input SOL amount
- const tokenAmount =
- (lamports * TOKEN_SUPPLY) / (VIRTUAL_RESERVES + lamports);
-
- return tokenAmount;
- };
-
- // Helper function to calculate percentage of total supply for a given token amount
- const calculatePercentage = (tokenAmount: number): number => {
- return (tokenAmount / TOKEN_SUPPLY) * 100;
- };
-
- // Cleanup object URLs when component unmounts or when URL changes
useEffect(() => {
- // Store created URLs for cleanup
const createdUrls: string[] = [];
return () => {
- // Cleanup any object URLs to prevent memory leaks
createdUrls.forEach((url) => {
URL.revokeObjectURL(url);
});
};
}, []);
- // Additional cleanup for autoForm.imageUrl when it changes
useEffect(() => {
const prevImageUrl = autoForm.imageUrl;
return () => {
- // Only cleanup URLs that look like object URLs (blob:)
if (prevImageUrl && prevImageUrl.startsWith("blob:")) {
URL.revokeObjectURL(prevImageUrl);
}
};
}, [autoForm.imageUrl]);
- // Cleanup vanity workers on component unmount
- useEffect(() => {
- // Store the refs in variables before returning the cleanup function
- const workersToTerminate = workersRef.current;
- const timerToClear = displayUpdateIntervalRef.current;
+ const autoTabErrors: {
+ userPrompt?: string;
+ [k: string]: string | undefined;
+ } = {
+ userPrompt: formErrors.userPrompt,
+ };
- return () => {
- workersToTerminate.forEach((worker) => {
- try {
- worker.postMessage("stop");
- } catch (e) {
- /* ignore */
- }
- try {
- worker.terminate();
- } catch (e) {
- /* ignore */
- }
- });
- if (timerToClear) {
- clearInterval(timerToClear);
- }
- // Clear the refs directly - this part is fine
- workersRef.current = [];
- displayUpdateIntervalRef.current = null;
- };
- }, []); // Empty dependency array ensures this runs only on mount and unmount
+ const importTabErrors: {
+ importAddress?: string;
+ [k: string]: string | undefined;
+ } = {
+ importAddress: formErrors.importAddress,
+ };
- const canLaunch = () => {
+ const canLaunch = useCallback(() => {
if (!publicKey) return false;
if (activeTab === FormTab.IMPORT) {
- // For import, we just need the form data loaded
return hasStoredToken && !isImporting;
}
- // For Auto/Manual, need valid form, generated vanity key, and enough SOL
const initialSol = parseFloat(form.initialSol) || 0;
- const hasEnoughSol = solBalance >= initialSol + 0.01; // Add buffer for mint cost
+ const hasEnoughSol = solBalance >= initialSol + 0.01;
const hasVanityKey = !!vanityResult?.publicKey && !isGeneratingVanity;
return (
hasEnoughSol &&
- isFormValid && // Checks name, symbol, desc, initialSol errors
+ checkFormValid() &&
hasVanityKey &&
- !Object.values(errors).some(
+ !Object.values(formErrors).some(
(error) =>
error &&
- !["userPrompt", "importAddress", "percentage"].includes(error), // Ignore non-blocking errors
+ !["userPrompt", "importAddress", "percentage"].includes(error),
)
);
- };
-
- // Add handler for coin drop cancellation
- const handleCoinDropCancel = useCallback(() => {
- setShowCoinDrop(false);
- setIsSubmitting(false); // Also reset submitting state
- setIsCreating(false); // Reset creating state
- setCreationStage("initializing"); // Reset creation stage
- setCreationStep("");
- // Consider stopping vanity generation if cancelled here?
- // stopVanityGeneration();
- }, []);
-
- const [isCreating, setIsCreating] = useState(false);
- const [creationStep, setCreationStep] = useState("");
- const [creationStage, setCreationStage] = useState<
- "initializing" | "confirming" | "creating" | "validating" | "finalizing"
- >("initializing");
+ }, [
+ publicKey,
+ activeTab,
+ hasStoredToken,
+ isImporting,
+ form.initialSol,
+ solBalance,
+ vanityResult,
+ isGeneratingVanity,
+ checkFormValid,
+ formErrors,
+ ]);
const [rotation, setRotation] = useState(0);
@@ -2786,789 +1027,277 @@ export default function Create() {
return () => clearInterval(interval);
}, []);
- // Add effect to listen for log messages and update state
- useEffect(() => {
- const originalConsoleLog = console.log;
- console.log = (...args) => {
- originalConsoleLog(...args);
-
- // Check for specific log messages to update state
- const message = args[0];
- if (typeof message === "string") {
- if (message.includes("Token created on-chain successfully")) {
- setCreationStage("validating");
- setCreationStep("Token created successfully, validating...");
- } else if (
- message.includes("Waiting for token creation confirmation")
- ) {
- setCreationStage("validating");
- setCreationStep("Waiting for blockchain confirmation...");
- } else if (message.includes("waiting for creation from token mint")) {
- setCreationStage("validating");
- setCreationStep("Confirming token creation on-chain...");
- } else if (message.includes("Creating token on-chain...")) {
- setCreationStage("creating");
- setCreationStep("Creating token on-chain...");
+ const {
+ isUploading,
+ uploadProgress,
+ uploadedImageUrl,
+ handleImageUpload,
+ resetUpload,
+ } = useImageUpload({
+ onImageUploaded: (url) => {
+ setCoinDropImageUrl(url);
+ if (previewSetterRef.current) {
+ previewSetterRef.current(url);
+ }
+ },
+ onError: (error) => {
+ toast.error(error);
+ },
+ });
+
+ const handleImageChange = useCallback(
+ async (file: File | null) => {
+ if (!file) {
+ resetUpload();
+ setImageFile(null);
+ setCoinDropImageUrl(null);
+ if (previewSetterRef.current) {
+ previewSetterRef.current(null);
}
+ return;
}
- };
- return () => {
- console.log = originalConsoleLog;
- };
+ setImageFile(file);
+
+ const previewUrl = URL.createObjectURL(file);
+ setCoinDropImageUrl(previewUrl);
+ if (previewSetterRef.current) {
+ previewSetterRef.current(previewUrl);
+ }
+
+ if (activeTab === FormTab.MANUAL) {
+ setManualForm((prev) => ({
+ ...prev,
+ imageFile: file,
+ }));
+ }
+
+ try {
+ // Convert file to base64 with proper format
+ const imageBase64 = await new Promise((resolve) => {
+ const reader = new FileReader();
+ reader.onloadend = () => {
+ const base64String = reader.result as string;
+ // Ensure the base64 string has the proper data URL format
+ if (!base64String.startsWith("data:")) {
+ resolve(`data:${file.type};base64,${base64String.split(",")[1]}`);
+ } else {
+ resolve(base64String);
+ }
+ };
+ reader.readAsDataURL(file);
+ });
+
+ console.log(
+ "Image base64 format:",
+ imageBase64.substring(0, 50) + "...",
+ ); // Debug log
+
+ const tokenMetadata: TokenMetadata = {
+ name: form.name,
+ symbol: form.symbol,
+ description: form.description,
+ initialSol: parseFloat(form.initialSol) || 0,
+ links: form.links,
+ imageBase64,
+ tokenMint: vanityResult?.publicKey || "",
+ decimals: 9,
+ supply: 1000000000000000,
+ freezeAuthority: publicKey?.toBase58() || "",
+ mintAuthority: publicKey?.toBase58() || "",
+ };
+
+ await handleImageUpload(file, tokenMetadata);
+ } catch (error) {
+ console.error("Error uploading image:", error);
+ toast.error("Failed to upload image. Please try again.");
+ }
+ },
+ [activeTab, handleImageUpload, resetUpload, form, vanityResult, publicKey],
+ );
+
+ const handleImportAddressPaste = (
+ e: React.ClipboardEvent,
+ ) => {
+ const pastedText = e.clipboardData.getData("text");
+
+ if (!isValidTokenAddress(pastedText)) {
+ e.preventDefault();
+
+ setFormErrors((prev) => ({
+ ...prev,
+ importAddress:
+ "Invalid token address format. Please check and try again.",
+ }));
+
+ return false;
+ }
+
+ setFormErrors((prev) => ({
+ ...prev,
+ importAddress: "",
+ }));
+
+ return true;
+ };
+
+ const handlePreviewChange = useCallback((previewUrl: string | null) => {
+ setCoinDropImageUrl(previewUrl);
}, []);
- return (
-
- {/* {showCoinDrop ? (
-
- ) : null} */}
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const { isCreating, creationStep, creationStage, createToken } =
+ useTokenCreation({
+ publicKey: publicKey?.toBase58() || null,
+ signTransaction: signTransaction || null,
+ });
+
+ const submitFormToBackend = async (e: React.FormEvent) => {
+ e.preventDefault();
+ try {
+ setIsSubmitting(true);
+ if (!vanityResult?.publicKey || !vanityResult?.secretKey) {
+ toast.error("Please generate a vanity address first.");
+ return;
+ }
+
+ const tokenMetadata: TokenMetadata = {
+ name: form.name,
+ symbol: form.symbol,
+ description: form.description,
+ initialSol: parseFloat(form.initialSol) || 0,
+ links: form.links,
+ imageBase64: null,
+ tokenMint: vanityResult.publicKey,
+ decimals: 9,
+ supply: 1000000000000000,
+ freezeAuthority: publicKey?.toBase58() || "",
+ mintAuthority: publicKey?.toBase58() || "",
+ };
+
+ await createToken(
+ tokenMetadata,
+ vanityResult.secretKey,
+ vanityResult,
+ imageFile,
+ currentPreGeneratedTokenId || undefined,
+ userPrompt || undefined,
+ activeTab,
+ );
+ } catch (error) {
+ console.error("Error creating token:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Failed to create token. Please try again.",
+ );
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+ return (
+
- {/* Creation Loading Modal */}
- {isCreating && (
-
-
-
-
-
- {/* Front face - left half */}
-
-
-
-
- {/* Right face - right half */}
-
-
-
-
-
-
- {/* Back face - left half */}
-
-
-
-
- {/* Left face - right half */}
-
-
-
-
-
-
-
-
- {creationStep}
-
- {creationStage === "confirming" && (
-
- Please confirm the transaction in your wallet
-
- )}
-
-
-
- )}
+
);
}
diff --git a/packages/server/src/routes/token.ts b/packages/server/src/routes/token.ts
index 07acb7940..1899b947f 100644
--- a/packages/server/src/routes/token.ts
+++ b/packages/server/src/routes/token.ts
@@ -57,6 +57,8 @@ import {
} from "./validators/tokenUpdateQuery";
import { parseSearchTokenRequest } from "./validators/tokenSearchQuery";
import { normalizeParams, makeCacheKey } from "../tools/normalizeParams";
+import { parseTransactionAndCreateNewToken } from "../util";
+import { sign } from "node:crypto";
import { sortBy } from "lodash";
if (!process.env.CODEX_API_KEY) {
@@ -1534,6 +1536,7 @@ tokenRouter.post("/create-token", async (c) => {
website,
discord,
imported,
+ signature,
} = body;
const mintAddress = tokenMint || mint;
if (!mintAddress) {
@@ -1685,6 +1688,38 @@ tokenRouter.post("/create-token", async (c) => {
imageUrl = "";
}
}
+ if (!imported) {
+ const newToken = await parseTransactionAndCreateNewToken(
+ signature,
+ mintAddress,
+ imageUrl,
+ tokenStats?.metadataUri || ""
+ );
+ if (newToken) {
+ const partialNewToken = {
+ id: newToken.id,
+ mint: newToken.mint,
+ name: newToken.name,
+ ticker: newToken.ticker,
+ url: newToken.url,
+ image: newToken.image,
+ description: newToken.description,
+ twitter: newToken.twitter,
+ telegram: newToken.telegram,
+ farcaster: newToken.farcaster,
+ website: newToken.website,
+ discord: newToken.discord,
+ creator: newToken.creator,
+ status: newToken.status,
+ imported: newToken.imported,
+ createdAt: newToken.createdAt,
+ isToken2022: false,
+ };
+
+ logger.log(`New token created successfully: ${newToken}`);
+ return c.json({ success: true, token: partialNewToken });
+ }
+ }
// Create token data with all required fields from the token schema
const now = new Date();
@@ -1757,11 +1792,9 @@ tokenRouter.post("/create-token", async (c) => {
image: imageUrl || "",
createdAt: now,
imported: importedValue,
- isToken2022: isToken2022Value, // <<< Include in response if needed
+ isToken2022: isToken2022Value,
};
- // Trigger immediate updates for price and holders in the background
- // for both imported and newly created tokens
logger.log(
`Triggering immediate price and holder update for token: ${mintAddress}`
);
@@ -1769,23 +1802,22 @@ tokenRouter.post("/create-token", async (c) => {
if (imported) {
try {
const redisCache = await getGlobalRedisCache();
- const importedToken = await ExternalToken.create(
- mintAddress,
- redisCache
- );
- const { marketData } = await importedToken.registerWebhook();
- // Fetch historical data in the background
- (async () => await importedToken.fetchHistoricalSwapData())();
- // Merge any immediately available market data
- if (marketData && marketData.newTokenData) {
- Object.assign(tokenData, marketData.newTokenData);
- }
+ // const importedToken = await ExternalToken.create(
+ // mintAddress,
+ // redisCache
+ // );
+ // const { marketData } = await importedToken.registerWebhook();
+ // // Fetch historical data in the background
+ // (async () => await importedToken.fetchHistoricalSwapData())();
+ // // Merge any immediately available market data
+ // if (marketData && marketData.newTokenData) {
+ // Object.assign(tokenData, marketData.newTokenData);
+ // }
} catch (webhookError) {
logger.error(
`Failed to register webhook for imported token ${mintAddress}:`,
webhookError
);
- // Continue even if webhook registration fails, especially locally
}
}
diff --git a/packages/server/src/util.ts b/packages/server/src/util.ts
index 768691783..5c502eede 100644
--- a/packages/server/src/util.ts
+++ b/packages/server/src/util.ts
@@ -16,7 +16,7 @@ import {
} from "@solana/web3.js";
import { desc, sql } from "drizzle-orm";
import { CacheService } from "./cache";
-import { Token, tokens } from "./db";
+import { Token, tokens, getDB } from "./db";
import { calculateTokenMarketData, getSOLPrice } from "./mcap";
import { initSolanaConfig, getProgram } from "./solana";
import { Autofun } from "@autodotfun/types/types/autofun";
@@ -166,9 +166,58 @@ export async function getTxIdAndCreatorFromTokenAddress(tokenAddress: string) {
throw new Error(`No transaction found for token address: ${tokenAddress}`);
}
-/**
- * Creates a new token record with all required data
- */
+export async function parseTransactionAndCreateNewToken(
+ txId: string,
+ tokenAddress: string,
+ imageUrl: string,
+ uri: string
+): Promise
| undefined> {
+ const rpcUrl = getRpcUrl();
+ const connection = new Connection(rpcUrl, "confirmed");
+ const parsedTx = (await connection.getParsedTransaction(txId, {
+ maxSupportedTransactionVersion: 0,
+ })) as any;
+ if (!parsedTx) {
+ throw new Error(`Transaction ${txId} not found`);
+ }
+ if (!parsedTx?.meta?.logMessages) {
+ throw new Error(`Transaction ${txId} has no log messages`);
+ }
+ const newTokenLog = parsedTx.meta.logMessages.find((log: any) =>
+ log.includes("NewToken:")
+ );
+ const parts = newTokenLog.split(" ");
+ const rawTokenAddress = parts[parts.length - 2].replace(/[",)]/g, "");
+ const rawCreatorAddress = parts[parts.length - 1].replace(/[",)]/g, "");
+ if (!/^[1-9A-HJ-NP-Za-km-z]+$/.test(rawTokenAddress)) {
+ throw new Error(`Malformed token address: ${rawTokenAddress}`);
+ }
+ const creatorAddress = rawCreatorAddress;
+ tokenAddress = rawTokenAddress;
+
+ const newToken = await createNewTokenData(txId, tokenAddress, creatorAddress);
+ if (!newToken) {
+ logger.error(`Failed to create new token data for ${rawTokenAddress}`);
+ return undefined;
+ }
+ if (newToken.tokenSupplyUiAmount !== 1000000000) {
+ logger.error(
+ `Token supply is not 1 billion for ${rawTokenAddress}: ${newToken.tokenSupplyUiAmount}`
+ );
+ return undefined;
+ }
+ const createdToken = await getDB()
+ .insert(tokens)
+ .values([newToken as Token])
+ .onConflictDoNothing();
+ if (createdToken) {
+ logger.log(
+ `Created new token ${newToken.name} (${newToken.ticker}) with address ${tokenAddress}`
+ );
+ return newToken;
+ }
+}
+
export async function createNewTokenData(
txId: string,
tokenAddress: string,
@@ -178,8 +227,6 @@ export async function createNewTokenData(
// Get a Solana config with the right environment
const solanaConfig = initSolanaConfig();
- console.log("solanaConfig", solanaConfig);
-
const metadata = await fetchMetadataWithBackoff(
solanaConfig.umi,
tokenAddress
@@ -222,10 +269,6 @@ export async function createNewTokenData(
`Bonding curve account not found for token ${tokenAddress}`
);
}
- console.log("bondingCurveAccount", bondingCurveAccount);
- console.log("reserveToken", Number(bondingCurveAccount.reserveToken));
- console.log("reserveLamport", Number(bondingCurveAccount.reserveLamport));
- console.log("curveLimit", Number(bondingCurveAccount.curveLimit));
const currentPrice =
Number(bondingCurveAccount.reserveToken) > 0
@@ -234,15 +277,12 @@ export async function createNewTokenData(
(Number(bondingCurveAccount.reserveToken) /
Math.pow(10, TOKEN_DECIMALS))
: 0;
- console.log("currentPrice", currentPrice);
const tokenPriceInSol = currentPrice / Math.pow(10, TOKEN_DECIMALS);
- console.log("tokenPriceInSol", tokenPriceInSol);
const tokenPriceUSD =
currentPrice > 0
? tokenPriceInSol * solPrice * Math.pow(10, TOKEN_DECIMALS)
: 0;
- console.log("tokenPriceUSD", tokenPriceUSD);
// Get TOKEN_SUPPLY from env if available, otherwise use default
const supply = await updateTokenSupplyFromChain(tokenAddress);
@@ -251,7 +291,7 @@ export async function createNewTokenData(
: Number(process.env.TOKEN_SUPPLY);
const marketCapUSD =
(tokenSupply / Math.pow(10, TOKEN_DECIMALS)) * tokenPriceUSD;
- console.log("marketCapUSD", marketCapUSD);
+ // console.log("marketCapUSD", marketCapUSD);
// Get virtual reserves from env if available, otherwise use default
const virtualReserves = process.env.VIRTUAL_RESERVES
From 8f69eb5c810e1d31a5d9c916d088b0c6d14ee34d Mon Sep 17 00:00:00 2001
From: MarcoMandar
Date: Fri, 9 May 2025 13:31:53 +0300
Subject: [PATCH 2/3] update
Signed-off-by: MarcoMandar
---
.../create/components/BuySection.tsx | 148 ++++++++
.../src/create/components/AutoTabContent.tsx | 79 ++++
.../src/create/components/BuySection.tsx | 150 ++++++++
.../components/CreationLoadingModal.tsx | 187 ++++++++++
.../src/create/components/FormSection.tsx | 217 +++++++++++
.../create/components/ImportTabContent.tsx | 115 ++++++
.../src/create/components/LaunchButton.tsx | 55 +++
.../src/create/components/TabNavigation.tsx | 44 +++
.../components/VanityAddressSection.tsx | 124 +++++++
packages/client/src/create/consts.ts | 47 +++
.../src/create/forms/FormImageInput.tsx | 338 ++++++++++++++++++
.../client/src/create/forms/FormInput.tsx | 57 +++
.../client/src/create/forms/FormTextArea.tsx | 50 +++
packages/client/src/create/hooks/index.ts | 6 +
.../client/src/create/hooks/useImageUpload.ts | 69 ++++
.../src/create/hooks/useTokenCreation.ts | 255 +++++++++++++
.../client/src/create/hooks/useTokenForm.ts | 146 ++++++++
.../src/create/hooks/useTokenGeneration.ts | 164 +++++++++
.../src/create/hooks/useVanityAddress.ts | 129 +++++++
packages/client/src/create/hooks/useWallet.ts | 69 ++++
packages/client/src/create/types.ts | 148 ++++++++
.../client/src/create/utils/uploadImage.ts | 88 +++++
.../src/create/utils/waitFortokenCreation.ts | 25 ++
packages/client/src/create/validators.ts | 6 +
24 files changed, 2716 insertions(+)
create mode 100644 packages/client/src/components/create/components/BuySection.tsx
create mode 100644 packages/client/src/create/components/AutoTabContent.tsx
create mode 100644 packages/client/src/create/components/BuySection.tsx
create mode 100644 packages/client/src/create/components/CreationLoadingModal.tsx
create mode 100644 packages/client/src/create/components/FormSection.tsx
create mode 100644 packages/client/src/create/components/ImportTabContent.tsx
create mode 100644 packages/client/src/create/components/LaunchButton.tsx
create mode 100644 packages/client/src/create/components/TabNavigation.tsx
create mode 100644 packages/client/src/create/components/VanityAddressSection.tsx
create mode 100644 packages/client/src/create/consts.ts
create mode 100644 packages/client/src/create/forms/FormImageInput.tsx
create mode 100644 packages/client/src/create/forms/FormInput.tsx
create mode 100644 packages/client/src/create/forms/FormTextArea.tsx
create mode 100644 packages/client/src/create/hooks/index.ts
create mode 100644 packages/client/src/create/hooks/useImageUpload.ts
create mode 100644 packages/client/src/create/hooks/useTokenCreation.ts
create mode 100644 packages/client/src/create/hooks/useTokenForm.ts
create mode 100644 packages/client/src/create/hooks/useTokenGeneration.ts
create mode 100644 packages/client/src/create/hooks/useVanityAddress.ts
create mode 100644 packages/client/src/create/hooks/useWallet.ts
create mode 100644 packages/client/src/create/types.ts
create mode 100644 packages/client/src/create/utils/uploadImage.ts
create mode 100644 packages/client/src/create/utils/waitFortokenCreation.ts
create mode 100644 packages/client/src/create/validators.ts
diff --git a/packages/client/src/components/create/components/BuySection.tsx b/packages/client/src/components/create/components/BuySection.tsx
new file mode 100644
index 000000000..a80a33a02
--- /dev/null
+++ b/packages/client/src/components/create/components/BuySection.tsx
@@ -0,0 +1,148 @@
+import { FormTab } from "../types";
+import { Icons } from "../../icons";
+import { MAX_INITIAL_SOL, TOKEN_SUPPLY, VIRTUAL_RESERVES } from "../consts";
+
+interface BuySectionProps {
+ activeTab: FormTab;
+ buyValue: string;
+ solBalance: number;
+ isAuthenticated: boolean;
+ isFormValid: boolean;
+ insufficientBalance: boolean;
+ maxInputSol: number;
+ onBuyValueChange: (value: string) => void;
+}
+
+export const BuySection = ({
+ activeTab,
+ buyValue,
+ solBalance,
+ isAuthenticated,
+ isFormValid,
+ insufficientBalance,
+ maxInputSol,
+ onBuyValueChange,
+}: BuySectionProps) => {
+ if (activeTab === FormTab.IMPORT) return null;
+
+ // Helper function to calculate token amount based on SOL input using bonding curve formula
+ const calculateTokensFromSol = (solAmount: number): number => {
+ // Convert SOL to lamports
+ const lamports = solAmount * 1e9;
+ // Using constant product formula: (dx * y) / (x + dx)
+ // where x is virtual reserves, y is token supply, dx is input SOL amount
+ const tokenAmount = (lamports * TOKEN_SUPPLY) / (VIRTUAL_RESERVES + lamports);
+ return tokenAmount;
+ };
+
+ // Helper function to calculate percentage of total supply for a given token amount
+ const calculatePercentage = (tokenAmount: number): number => {
+ return (tokenAmount / TOKEN_SUPPLY) * 100;
+ };
+
+ return (
+
+
+
+ Buy
+
+
+
+
+ Choose how much of the token you want to buy on launch:
+
+
+ • SOL : Amount of SOL to invest
+
+
+ • % : Percentage of token supply to acquire
+
+
+
+ Total token supply: {TOKEN_SUPPLY.toLocaleString()} tokens
+
+
+ Pricing follows a bonding curve, your percentage increases with more SOL.
+
+
+
+
+ Maximum supply of 50% can be purchased prior to coin launch
+
+
+
+
+
+
+
+ {
+ let value = e.target.value.replace(" SOL", "");
+ value = value.replace(/[^\d.]/g, "");
+ const decimalCount = (value.match(/\./g) || []).length;
+ if (decimalCount > 1) {
+ value = value.substring(0, value.lastIndexOf(".")); // Keep only first decimal
+ }
+ const parts = value.split(".");
+ let wholePart = parts[0] || "0"; // Default to 0 if empty
+ let decimalPart = parts[1] || "";
+
+ // Limit whole part length (e.g., 2 digits for SOL up to 99)
+ if (wholePart.length > String(Math.floor(MAX_INITIAL_SOL)).length) {
+ wholePart = wholePart.slice(0, String(Math.floor(MAX_INITIAL_SOL)).length);
+ }
+ // Limit decimal part length
+ if (decimalPart.length > 2) {
+ // Allow 2 decimal places
+ decimalPart = decimalPart.slice(0, 2);
+ }
+
+ value = decimalPart ? `${wholePart}.${decimalPart}` : wholePart;
+
+ // Final numeric check against maxInputSol
+ const numValue = parseFloat(value);
+ if (!isNaN(numValue)) {
+ if (numValue < 0) value = "0";
+ else if (numValue > maxInputSol) value = maxInputSol.toString();
+ } else if (value !== "") {
+ value = "0"; // Reset invalid non-empty strings
+ }
+
+ onBuyValueChange(value);
+ }}
+ min="0"
+ max={maxInputSol.toString()}
+ step="0.01"
+ className="w-26 pr-10 text-white text-xl font-medium text-right inline border-b border-b-[#424242] focus:outline-none focus:border-white bg-transparent"
+ />
+
+ SOL
+
+
+
+
+ {parseFloat(buyValue) > 0 && (
+
+ ≈ {calculatePercentage(calculateTokensFromSol(parseFloat(buyValue))).toFixed(2)} % of supply
+
+ )}
+
+ {/* Balance information */}
+
+ Balance: {solBalance?.toFixed(2) ?? "0.00"} SOL
+ {isAuthenticated && isFormValid && insufficientBalance && (
+
+ Insufficient SOL balance (need ~0.05 SOL for mint + buy amount)
+
+ )}
+ {Number(buyValue) === maxInputSol && maxInputSol < MAX_INITIAL_SOL && (
+
+ Maximum amount based on your balance
+
+ )}
+
+
+ );
+};
\ No newline at end of file
diff --git a/packages/client/src/create/components/AutoTabContent.tsx b/packages/client/src/create/components/AutoTabContent.tsx
new file mode 100644
index 000000000..b171fb394
--- /dev/null
+++ b/packages/client/src/create/components/AutoTabContent.tsx
@@ -0,0 +1,79 @@
+interface AutoTabContentProps {
+ userPrompt: string;
+ setUserPrompt: (prompt: string) => void;
+ errors: { userPrompt?: string; [k: string]: string | undefined };
+ isProcessingPrompt: boolean;
+ generateFromPrompt: () => Promise;
+}
+
+export const AutoTabContent = ({
+ userPrompt,
+ setUserPrompt,
+ errors,
+ isProcessingPrompt,
+ generateFromPrompt,
+}: AutoTabContentProps) => {
+ return (
+ <>
+
+
setUserPrompt(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ }
+ }}
+ placeholder="Enter a concept like 'a halloween token about arnold schwarzenegger'"
+ className="flex-1 truncate my-2 p-0 border-b-2 pb-2.5 border-b-[#03FF24] text-white bg-transparent focus:outline-none focus:border-b-white"
+ />
+
+ {
+ const img = e.target as HTMLImageElement;
+ if (!isProcessingPrompt) {
+ img.src = "/create/generatedown.svg";
+ }
+ }}
+ onMouseUp={(e) => {
+ const img = e.target as HTMLImageElement;
+ if (!isProcessingPrompt) {
+ img.src = "/create/generateup.svg";
+ }
+ }}
+ onDragStart={(e) => {
+ e.preventDefault();
+ const img = e.target as HTMLImageElement;
+ if (!isProcessingPrompt) {
+ img.src = "/create/generateup.svg";
+ }
+ }}
+ onMouseOut={(e) => {
+ e.preventDefault();
+ const img = e.target as HTMLImageElement;
+ if (!isProcessingPrompt) {
+ img.src = "/create/generateup.svg";
+ }
+ }}
+ />
+
+
+ {errors.userPrompt && (
+ {errors.userPrompt}
+ )}
+ >
+ );
+};
diff --git a/packages/client/src/create/components/BuySection.tsx b/packages/client/src/create/components/BuySection.tsx
new file mode 100644
index 000000000..79665d1d1
--- /dev/null
+++ b/packages/client/src/create/components/BuySection.tsx
@@ -0,0 +1,150 @@
+import { Icons } from "@/components/icons";
+import { MAX_INITIAL_SOL, TOKEN_SUPPLY, VIRTUAL_RESERVES } from "../consts";
+import { FormTab } from "../types";
+
+interface BuySectionProps {
+ activeTab: FormTab;
+ buyValue: string;
+ solBalance: number;
+ isAuthenticated: boolean;
+ isFormValid: boolean;
+ insufficientBalance: boolean;
+ maxInputSol: number;
+ onBuyValueChange: (value: string) => void;
+}
+
+export const BuySection = ({
+ activeTab,
+ buyValue,
+ solBalance,
+ isAuthenticated,
+ isFormValid,
+ insufficientBalance,
+ maxInputSol,
+ onBuyValueChange,
+}: BuySectionProps) => {
+ if (activeTab === FormTab.IMPORT) return null;
+
+ const calculateTokensFromSol = (solAmount: number): number => {
+ const lamports = solAmount * 1e9;
+ const tokenAmount =
+ (lamports * TOKEN_SUPPLY) / (VIRTUAL_RESERVES + lamports);
+ return tokenAmount;
+ };
+
+ const calculatePercentage = (tokenAmount: number): number => {
+ return (tokenAmount / TOKEN_SUPPLY) * 100;
+ };
+
+ return (
+
+
+
+ Buy
+
+
+
+
+ Choose how much of the token you want to buy on launch:
+
+
+ • SOL : Amount of SOL to invest
+
+
+ • % : Percentage of token supply to acquire
+
+
+
+ Total token supply: {TOKEN_SUPPLY.toLocaleString()} tokens
+
+
+ Pricing follows a bonding curve, your percentage increases
+ with more SOL.
+
+
+
+
+ Maximum supply of 50% can be purchased prior to coin launch
+
+
+
+
+
+
+
+ {
+ let value = e.target.value.replace(" SOL", "");
+ value = value.replace(/[^\d.]/g, "");
+ const decimalCount = (value.match(/\./g) || []).length;
+ if (decimalCount > 1) {
+ value = value.substring(0, value.lastIndexOf("."));
+ }
+ const parts = value.split(".");
+ let wholePart = parts[0] || "0";
+ let decimalPart = parts[1] || "";
+
+ if (
+ wholePart.length > String(Math.floor(MAX_INITIAL_SOL)).length
+ ) {
+ wholePart = wholePart.slice(
+ 0,
+ String(Math.floor(MAX_INITIAL_SOL)).length,
+ );
+ }
+ if (decimalPart.length > 2) {
+ decimalPart = decimalPart.slice(0, 2);
+ }
+
+ value = decimalPart ? `${wholePart}.${decimalPart}` : wholePart;
+
+ const numValue = parseFloat(value);
+ if (!isNaN(numValue)) {
+ if (numValue < 0) value = "0";
+ else if (numValue > maxInputSol)
+ value = maxInputSol.toString();
+ } else if (value !== "") {
+ value = "0";
+ }
+
+ onBuyValueChange(value);
+ }}
+ min="0"
+ max={maxInputSol.toString()}
+ step="0.01"
+ className="w-26 pr-10 text-white text-xl font-medium text-right inline border-b border-b-[#424242] focus:outline-none focus:border-white bg-transparent"
+ />
+
+ SOL
+
+
+
+
+ {parseFloat(buyValue) > 0 && (
+
+ ≈{" "}
+ {calculatePercentage(
+ calculateTokensFromSol(parseFloat(buyValue)),
+ ).toFixed(2)}{" "}
+ % of supply
+
+ )}
+
+
+ Balance: {solBalance?.toFixed(2) ?? "0.00"} SOL
+ {isAuthenticated && isFormValid && insufficientBalance && (
+
+ Insufficient SOL balance (need ~0.05 SOL for mint + buy amount)
+
+ )}
+ {Number(buyValue) === maxInputSol && maxInputSol < MAX_INITIAL_SOL && (
+
+ Maximum amount based on your balance
+
+ )}
+
+
+ );
+};
diff --git a/packages/client/src/create/components/CreationLoadingModal.tsx b/packages/client/src/create/components/CreationLoadingModal.tsx
new file mode 100644
index 000000000..19f140eaf
--- /dev/null
+++ b/packages/client/src/create/components/CreationLoadingModal.tsx
@@ -0,0 +1,187 @@
+import { useEffect, useState } from "react";
+
+interface CreationLoadingModalProps {
+ isCreating: boolean;
+ creationStep: string;
+ creationStage:
+ | "initializing"
+ | "confirming"
+ | "creating"
+ | "validating"
+ | "finalizing";
+}
+
+export const CreationLoadingModal = ({
+ isCreating,
+ creationStep,
+ creationStage,
+}: CreationLoadingModalProps) => {
+ const [rotation, setRotation] = useState(0);
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setRotation((prev) => prev + 90);
+ }, 1000);
+
+ return () => clearInterval(interval);
+ }, []);
+
+ if (!isCreating) return null;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {creationStep}
+
+ {creationStage === "confirming" && (
+
+ Please confirm the transaction in your wallet
+
+ )}
+
+
+
+ );
+};
diff --git a/packages/client/src/create/components/FormSection.tsx b/packages/client/src/create/components/FormSection.tsx
new file mode 100644
index 000000000..b54140574
--- /dev/null
+++ b/packages/client/src/create/components/FormSection.tsx
@@ -0,0 +1,217 @@
+import { FormImageInput } from "../forms/FormImageInput";
+import { FormTextArea } from "../forms/FormTextArea";
+import { FormTab } from "../types";
+import { BuySection } from "./BuySection";
+import { LaunchButton } from "./LaunchButton";
+import { VanityAddressSection } from "./VanityAddressSection";
+
+interface FormSectionProps {
+ activeTab: FormTab;
+ hasGeneratedToken: boolean;
+ hasStoredToken: boolean;
+ form: {
+ name: string;
+ symbol: string;
+ description: string;
+ initialSol: string;
+ links: {
+ twitter: string;
+ telegram: string;
+ website: string;
+ discord: string;
+ farcaster: string;
+ };
+ };
+ errors: {
+ name: string;
+ symbol: string;
+ description: string;
+ prompt: string;
+ initialSol: string;
+ userPrompt: string;
+ importAddress: string;
+ percentage: string;
+ };
+ isGenerating: boolean;
+ generatingField: string | null;
+ imageFile: File | null;
+ coinDropImageUrl: string | null;
+ autoForm: {
+ name: string;
+ symbol: string;
+ description: string;
+ prompt: string;
+ concept: string;
+ imageUrl: string | null;
+ };
+ manualForm: {
+ name: string;
+ symbol: string;
+ description: string;
+ imageFile: File | null;
+ };
+ buyValue: string;
+ solBalance: number;
+ isAuthenticated: boolean;
+ isFormValid: boolean;
+ insufficientBalance: boolean;
+ maxInputSol: number;
+ isSubmitting: boolean;
+ isCreating: boolean;
+ canLaunch: () => boolean;
+ onImageChange: (file: File | null) => void;
+ onPromptChange: (prompt: string) => void;
+ onPromptFunctionsChange: (
+ setPrompt: ((prompt: string) => void) | null,
+ onPromptChange: ((prompt: string) => void) | null,
+ ) => void;
+ onPreviewChange: (previewUrl: string | null) => void;
+ onDirectPreviewSet: (
+ setter: ((preview: string | null) => void) | null,
+ ) => void;
+ onNameChange: (value: string) => void;
+ onTickerChange: (value: string) => void;
+ onDescriptionChange: (value: string) => void;
+ onBuyValueChange: (value: string) => void;
+ onGenerateAll: () => void;
+ // Vanity props
+ isGeneratingVanity: boolean;
+ displayedPublicKey: string;
+ vanitySuffix: string;
+ vanityResult: { publicKey: string; secretKey: any } | null;
+ suffixError: string | null;
+ onSuffixChange: (suffix: string) => void;
+ onGenerateClick: () => void;
+}
+
+export const FormSection = ({
+ activeTab,
+ hasGeneratedToken,
+ hasStoredToken,
+ form,
+ errors,
+ isGenerating,
+ generatingField,
+ imageFile,
+ coinDropImageUrl,
+ autoForm,
+ manualForm,
+ buyValue,
+ solBalance,
+ isAuthenticated,
+ isFormValid,
+ insufficientBalance,
+ maxInputSol,
+ isSubmitting,
+ isCreating,
+ canLaunch,
+ onImageChange,
+ onPromptChange,
+ onPromptFunctionsChange,
+ onPreviewChange,
+ onDirectPreviewSet,
+ onNameChange,
+ onTickerChange,
+ onDescriptionChange,
+ onBuyValueChange,
+ onGenerateAll,
+ // Vanity props
+ isGeneratingVanity,
+ displayedPublicKey,
+ vanitySuffix,
+ vanityResult,
+ suffixError,
+ onSuffixChange,
+ onGenerateClick,
+}: FormSectionProps) => {
+ if (
+ !(
+ activeTab === FormTab.MANUAL ||
+ (activeTab === FormTab.AUTO && hasGeneratedToken) ||
+ (activeTab === FormTab.IMPORT && hasStoredToken)
+ )
+ ) {
+ return null;
+ }
+
+ return (
+
+
+ {}}
+ setGeneratingField={() => {}}
+ onPromptFunctionsChange={onPromptFunctionsChange}
+ onPreviewChange={onPreviewChange}
+ imageUrl={
+ activeTab === FormTab.AUTO
+ ? autoForm.imageUrl
+ : activeTab === FormTab.IMPORT && hasStoredToken
+ ? coinDropImageUrl
+ : undefined
+ }
+ onDirectPreviewSet={onDirectPreviewSet}
+ activeTab={activeTab}
+ nameValue={form.name}
+ onNameChange={onNameChange}
+ tickerValue={form.symbol}
+ onTickerChange={onTickerChange}
+ key={`image-input-${activeTab}`}
+ />
+
+ {activeTab === FormTab.IMPORT ? (
+
+ {form.description}
+
+ ) : (
+ ) =>
+ onDescriptionChange(e.target.value)
+ }
+ label="Description"
+ minRows={1}
+ placeholder="Description"
+ maxLength={2000}
+ error={errors.description}
+ onClick={onGenerateAll}
+ isLoading={isGenerating && generatingField === "description"}
+ />
+ )}
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/client/src/create/components/ImportTabContent.tsx b/packages/client/src/create/components/ImportTabContent.tsx
new file mode 100644
index 000000000..29abcc517
--- /dev/null
+++ b/packages/client/src/create/components/ImportTabContent.tsx
@@ -0,0 +1,115 @@
+import { Icons } from "@/components/icons";
+
+interface ImportStatus {
+ type: "success" | "error" | "warning";
+ message: string;
+}
+
+interface ImportTabContentProps {
+ importAddress: string;
+ onImportAddressChange: (val: string) => void;
+ handleImportAddressPaste: (e: React.ClipboardEvent) => void;
+ errors: { importAddress?: string; [k: string]: string | undefined };
+ isImporting: boolean;
+ isValidTokenAddress: (address: string) => boolean;
+ importTokenFromAddress: () => Promise;
+ importStatus: ImportStatus | null;
+}
+
+export const ImportTabContent = ({
+ importAddress,
+ onImportAddressChange,
+ handleImportAddressPaste,
+ errors,
+ isImporting,
+ isValidTokenAddress,
+ importTokenFromAddress,
+ importStatus,
+}: ImportTabContentProps) => {
+ return (
+
+
+
+
onImportAddressChange(e.target.value)}
+ onPaste={handleImportAddressPaste}
+ placeholder="Enter any Solana token address (mint)"
+ className="flex-1 truncate my-2 p-0 border-b-2 pb-2.5 border-b-[#03FF24] text-white bg-transparent focus:outline-none focus:border-b-white"
+ />
+
+ {
+ const img = e.target as HTMLImageElement;
+ if (!isImporting) {
+ img.src = "/create/importdown.svg";
+ }
+ }}
+ onMouseUp={(e) => {
+ const img = e.target as HTMLImageElement;
+ if (!isImporting) {
+ img.src = "/create/importup.svg";
+ }
+ }}
+ onDragStart={(e) => {
+ e.preventDefault();
+ const img = e.target as HTMLImageElement;
+ if (!isImporting) {
+ img.src = "/create/importup.svg";
+ }
+ }}
+ onMouseOut={(e) => {
+ e.preventDefault();
+ const img = e.target as HTMLImageElement;
+ if (!isImporting) {
+ img.src = "/create/importup.svg";
+ }
+ }}
+ />
+
+
+ {errors.importAddress && (
+
{errors.importAddress}
+ )}
+
+ {importStatus && (
+
+
+ {importStatus.type === "success" ? (
+
+ ) : importStatus.type === "warning" ? (
+
+ ) : (
+
+ )}
+ {importStatus.message}
+
+
+ )}
+
+
+ );
+};
diff --git a/packages/client/src/create/components/LaunchButton.tsx b/packages/client/src/create/components/LaunchButton.tsx
new file mode 100644
index 000000000..fa30c2aa7
--- /dev/null
+++ b/packages/client/src/create/components/LaunchButton.tsx
@@ -0,0 +1,55 @@
+import { FormTab } from "../types";
+
+interface LaunchButtonProps {
+ activeTab: FormTab;
+ isSubmitting: boolean;
+ isCreating: boolean;
+ isAuthenticated: boolean;
+ canLaunch: boolean;
+ hasStoredToken: boolean;
+}
+
+export const LaunchButton = ({
+ activeTab,
+ isSubmitting,
+ isCreating,
+ isAuthenticated,
+ canLaunch,
+ hasStoredToken,
+}: LaunchButtonProps) => {
+ return (
+
+
+
+
+ {/* Validation/Auth Messages */}
+ {!isAuthenticated ? (
+
+ Please connect your wallet to create a token.
+
+ ) : !canLaunch && !isSubmitting && activeTab !== FormTab.IMPORT ? (
+
+ Please fill required fields, ensure sufficient SOL, and generate a vanity address.
+
+ ) : !canLaunch && !isSubmitting && activeTab === FormTab.IMPORT ? (
+
+ Please load token data via the import field above.
+
+ ) : null}
+
+ );
+};
\ No newline at end of file
diff --git a/packages/client/src/create/components/TabNavigation.tsx b/packages/client/src/create/components/TabNavigation.tsx
new file mode 100644
index 000000000..db0207adb
--- /dev/null
+++ b/packages/client/src/create/components/TabNavigation.tsx
@@ -0,0 +1,44 @@
+import { FormTab } from "@/create/types";
+
+interface TabNavigationProps {
+ activeTab: FormTab;
+ onTabChange: (tab: FormTab) => void;
+}
+
+export const TabNavigation = ({
+ activeTab,
+ onTabChange,
+}: TabNavigationProps) => {
+ return (
+
+
+
+
+
+
+ {Object.values(FormTab).map((tab) => (
+ onTabChange(tab)}
+ >
+ {tab}
+
+ ))}
+
+
+ );
+};
diff --git a/packages/client/src/create/components/VanityAddressSection.tsx b/packages/client/src/create/components/VanityAddressSection.tsx
new file mode 100644
index 000000000..58496030a
--- /dev/null
+++ b/packages/client/src/create/components/VanityAddressSection.tsx
@@ -0,0 +1,124 @@
+import { FormTab } from "../types";
+
+interface VanityAddressSectionProps {
+ activeTab: FormTab;
+ isGeneratingVanity: boolean;
+ displayedPublicKey: string;
+ vanitySuffix: string;
+ vanityResult: { publicKey: string; secretKey: any } | null;
+ suffixError: string | null;
+ onSuffixChange: (suffix: string) => void;
+ onGenerateClick: () => void;
+}
+
+export const VanityAddressSection = ({
+ activeTab,
+ isGeneratingVanity,
+ displayedPublicKey,
+ vanitySuffix,
+ vanityResult,
+ suffixError,
+ onSuffixChange,
+ onGenerateClick,
+}: VanityAddressSectionProps) => {
+ if (activeTab === FormTab.IMPORT) return null;
+
+ return (
+
+
+ Generate Contract Address
+
+
+
+ {isGeneratingVanity ? (
+
+ {displayedPublicKey.slice(0, -vanitySuffix.length)}
+ {displayedPublicKey.slice(-vanitySuffix.length)}
+
+ ) : vanityResult ? (
+
+ {displayedPublicKey.slice(0, -vanitySuffix.length)}
+ {displayedPublicKey.slice(-vanitySuffix.length)}
+
+ ) : (
+
+ {displayedPublicKey.slice(0, -vanitySuffix.length)}
+ {displayedPublicKey.slice(-vanitySuffix.length)}
+
+ )}
+
+
+
+
+
onSuffixChange(e.target.value)}
+ placeholder="FUN"
+ maxLength={5}
+ className={`bg-autofun-background-input w-20 py-1.5 px-2 ${
+ suffixError &&
+ !suffixError.startsWith("Warning") &&
+ !suffixError.startsWith("Note")
+ ? "border-red-500"
+ : ""
+ } text-white text-center font-mono focus:outline-none focus:border-white disabled:opacity-50`}
+ />
+
+ {
+ const img = e.target as HTMLImageElement;
+ if (!isGeneratingVanity) {
+ img.src = "/create/generatedown.svg";
+ }
+ }}
+ onMouseUp={(e) => {
+ const img = e.target as HTMLImageElement;
+ if (!isGeneratingVanity) {
+ img.src = "/create/generateup.svg";
+ }
+ }}
+ onDragStart={(e) => {
+ e.preventDefault();
+ const img = e.target as HTMLImageElement;
+ if (!isGeneratingVanity) {
+ img.src = "/create/generateup.svg";
+ }
+ }}
+ onMouseOut={(e) => {
+ e.preventDefault();
+ const img = e.target as HTMLImageElement;
+ if (!isGeneratingVanity) {
+ img.src = "/create/generateup.svg";
+ }
+ }}
+ />
+
+
+
+ Choose a custom suffix
+
+ Longer suffixes are slower to generate
+
+
+ {suffixError && (
+
+ {suffixError}
+
+ )}
+
+ );
+};
diff --git a/packages/client/src/create/consts.ts b/packages/client/src/create/consts.ts
new file mode 100644
index 000000000..f04792fb1
--- /dev/null
+++ b/packages/client/src/create/consts.ts
@@ -0,0 +1,47 @@
+import { env, isDevnet } from "@/utils/env";
+
+export const MAX_INITIAL_SOL = 1000;
+export const MAX_NAME_LENGTH = 32;
+export const MAX_SYMBOL_LENGTH = 10;
+export const MAX_DESCRIPTION_LENGTH = 500;
+export const MAX_PROMPT_LENGTH = 1000;
+
+export const CREATION_STAGES = {
+ UPLOADING: {
+ step: 1,
+ stage: "uploading" as const,
+ message: "Uploading image...",
+ },
+ CREATING: {
+ step: 2,
+ stage: "creating" as const,
+ message: "Creating token...",
+ },
+ FINALIZING: {
+ step: 3,
+ stage: "finalizing" as const,
+ message: "Finalizing token creation...",
+ },
+ COMPLETE: {
+ step: 4,
+ stage: "complete" as const,
+ message: "Token created successfully!",
+ },
+};
+
+export const ERROR_MESSAGES = {
+ WALLET_NOT_CONNECTED: "Please connect your wallet to continue",
+ INVALID_FORM: "Please fill in all required fields correctly",
+ UPLOAD_FAILED: "Failed to upload image. Please try again",
+ CREATION_FAILED: "Failed to create token. Please try again",
+ INVALID_ADDRESS: "Invalid address provided",
+ INSUFFICIENT_BALANCE: "Insufficient balance for token creation",
+ NETWORK_ERROR: "Network error. Please check your connection",
+ UNKNOWN_ERROR: "An unknown error occurred. Please try again",
+};
+
+export const TOKEN_SUPPLY = Number(env.tokenSupply) || 1000000000;
+export const VIRTUAL_RESERVES = Number(env.virtualReserves) || 100;
+
+export const TAB_STATE_KEY = "auto_fun_active_tab";
+export const BASE58_REGEX = /^[1-9A-HJ-NP-Za-km-z]+$/;
diff --git a/packages/client/src/create/forms/FormImageInput.tsx b/packages/client/src/create/forms/FormImageInput.tsx
new file mode 100644
index 000000000..4a84ce9bd
--- /dev/null
+++ b/packages/client/src/create/forms/FormImageInput.tsx
@@ -0,0 +1,338 @@
+import { EmptyState } from "@/components/empty-state";
+import React, { useCallback, useEffect, useRef, useState } from "react";
+import { toast } from "react-toastify";
+import { FormTab } from "../types";
+
+export interface FormImageInputProps {
+ onChange: (file: File | null) => void;
+ onPromptChange: (prompt: string) => void;
+ isGenerating: boolean;
+ setIsGenerating: (value: boolean) => void;
+ setGeneratingField: (value: string | null) => void;
+ onPromptFunctionsChange: (
+ setPrompt: (prompt: string) => void,
+ onPromptChange: (prompt: string) => void,
+ ) => void;
+ onPreviewChange?: (previewUrl: string | null) => void;
+ imageUrl?: string | null;
+ onDirectPreviewSet?: (setter: (preview: string | null) => void) => void;
+ activeTab: FormTab;
+ nameValue?: string;
+ onNameChange?: (value: string) => void;
+ tickerValue?: string;
+ onTickerChange?: (value: string) => void;
+}
+export const FormImageInput = ({
+ onChange,
+ onPromptChange,
+ isGenerating,
+ setIsGenerating,
+ setGeneratingField,
+ onPromptFunctionsChange,
+ onPreviewChange,
+ imageUrl,
+ onDirectPreviewSet,
+ activeTab,
+ nameValue,
+ onNameChange,
+ tickerValue,
+ onTickerChange,
+}: FormImageInputProps) => {
+ const [preview, setPreview] = useState(null);
+ const [prompt, setPrompt] = useState("");
+ const [lastGeneratedImage, setLastGeneratedImage] = useState(
+ null,
+ );
+ const promptDebounceRef = useRef(null);
+ const hasDirectlySetPreview = useRef(false);
+ const fileInputRef = useRef(null);
+ const [nameInputFocused, setNameInputFocused] = useState(false);
+ const [tickerInputFocused, setTickerInputFocused] = useState(false);
+
+ useEffect(() => {
+ if (onDirectPreviewSet) {
+ onDirectPreviewSet((preview) => {
+ hasDirectlySetPreview.current = true;
+ setPreview(preview);
+ });
+ }
+ }, [onDirectPreviewSet]);
+
+ useEffect(() => {
+ if (imageUrl && !preview && !hasDirectlySetPreview.current) {
+ setPreview(imageUrl);
+ }
+ }, [imageUrl, preview]);
+
+ const debouncedPromptChange = useCallback(
+ (value: string) => {
+ if (promptDebounceRef.current) {
+ window.clearTimeout(promptDebounceRef.current);
+ }
+ promptDebounceRef.current = window.setTimeout(() => {
+ onPromptChange(value);
+ }, 500);
+ },
+ [onPromptChange],
+ );
+
+ useEffect(() => {
+ if (preview) {
+ setLastGeneratedImage(preview);
+ if (onPreviewChange) {
+ onPreviewChange(preview);
+ }
+ } else if (onPreviewChange) {
+ onPreviewChange(null);
+ }
+ }, [preview, onPreviewChange]);
+
+ useEffect(() => {
+ onPromptFunctionsChange(setPrompt, onPromptChange);
+ }, []);
+
+ const handlePromptChange = useCallback(
+ (e: React.ChangeEvent) => {
+ const value = e.target.value;
+ setPrompt(value);
+ debouncedPromptChange(value);
+ },
+ [debouncedPromptChange],
+ );
+
+ const handleCancel = useCallback(() => {
+ setIsGenerating(false);
+ setGeneratingField(null);
+ setPreview(lastGeneratedImage);
+ onChange(null);
+ }, [lastGeneratedImage, onChange, setIsGenerating, setGeneratingField]);
+
+ const handleFileChange = useCallback(
+ (e: React.ChangeEvent) => {
+ const files = e.target.files;
+ if (files && files.length > 0) {
+ const file = files[0];
+
+ if (!file.type.startsWith("image/")) {
+ toast.error("Please select an image file");
+ return;
+ }
+
+ if (file.size > 5 * 1024 * 1024) {
+ toast.error(
+ "File is too large. Please select an image less than 5MB.",
+ );
+ return;
+ }
+
+ const previewUrl = URL.createObjectURL(file);
+ setPreview(previewUrl);
+
+ onChange(file);
+ }
+ },
+ [onChange],
+ );
+
+ const handleDrop = useCallback(
+ (e: React.DragEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
+ const file = e.dataTransfer.files[0];
+ if (!file.type.startsWith("image/")) {
+ toast.error("Please drop an image file");
+ return;
+ }
+ if (file.size > 5 * 1024 * 1024) {
+ toast.error(
+ "File is too large. Please select an image less than 5MB.",
+ );
+ return;
+ }
+
+ const previewUrl = URL.createObjectURL(file);
+ setPreview(previewUrl);
+ onChange(file);
+ }
+ },
+ [onChange],
+ );
+
+ const handleDragOver = useCallback((e: React.DragEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ }, []);
+
+ const triggerFileInput = useCallback(() => {
+ if (fileInputRef.current) {
+ fileInputRef.current.click();
+ }
+ }, []);
+
+ const handleRemoveImage = useCallback(() => {
+ if (activeTab === FormTab.MANUAL) {
+ setPreview(null);
+ onChange(null);
+ if (fileInputRef.current) {
+ fileInputRef.current.value = "";
+ }
+ }
+ }, [activeTab, onChange]);
+
+ useEffect(() => {
+ return () => {
+ if (promptDebounceRef.current) {
+ window.clearTimeout(promptDebounceRef.current);
+ }
+ };
+ }, []);
+
+ if (activeTab === FormTab.IMPORT && !preview && !imageUrl) {
+ return null;
+ }
+
+ return (
+
+
+ {isGenerating ? (
+
+
+
Generating your image...
+
+ Cancel
+
+
+ ) : preview || imageUrl ? (
+
+
+
+ {activeTab === FormTab.MANUAL && (
+
+
+
+
+
+
+ )}
+
+
+ {
+
+ {activeTab === FormTab.IMPORT && (
+
+ {nameValue}
+
+ )}
+ {activeTab !== FormTab.IMPORT && (
+
+ onNameChange && onNameChange(e.target.value)
+ }
+ placeholder="Token Name"
+ maxLength={128}
+ onFocus={() => setNameInputFocused(true)}
+ onBlur={() => setNameInputFocused(false)}
+ className={`bg-transparent text-white text-xl font-bold border-b-2 ${
+ nameInputFocused ? "border-white" : "border-gray-500"
+ } focus:outline-none px-1 py-0.5 w-[280px] max-w-[95%]`}
+ />
+ )}
+
+ }
+ {onTickerChange && (
+
+
+ $
+ {activeTab === FormTab.IMPORT && (
+
+ {tickerValue}
+
+ )}
+ {activeTab !== FormTab.IMPORT && (
+ onTickerChange(e.target.value)}
+ placeholder="TICKER"
+ maxLength={16}
+ onFocus={() => setTickerInputFocused(true)}
+ onBlur={() => setTickerInputFocused(false)}
+ className={`bg-transparent text-white text-lg font-semibold border-b-2 ${
+ tickerInputFocused ? "border-white" : "border-gray-500"
+ } focus:outline-none px-1 py-0.5 max-w-[60%]`}
+ />
+ )}
+
+
+ )}
+
+ ) : (
+
+ {activeTab === FormTab.MANUAL ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+
+ );
+};
diff --git a/packages/client/src/create/forms/FormInput.tsx b/packages/client/src/create/forms/FormInput.tsx
new file mode 100644
index 000000000..02bb77538
--- /dev/null
+++ b/packages/client/src/create/forms/FormInput.tsx
@@ -0,0 +1,57 @@
+import React from "react";
+export interface FormInputProps {
+ label?: string;
+ isOptional?: boolean;
+ error?: string;
+ leftIndicator?: React.ReactNode;
+ rightIndicator?: React.ReactNode;
+ inputTag?: React.ReactNode;
+ onClick?: () => void;
+ isLoading?: boolean;
+ [key: string]: any;
+}
+export const FormInput = ({
+ label,
+ isOptional,
+ error,
+ leftIndicator,
+ rightIndicator,
+ inputTag,
+ onClick,
+ isLoading,
+ ...props
+}: FormInputProps) => {
+ return (
+
+
+ {label && (
+
+ {label}
+
+ )}
+
+
+ {inputTag && (
+
+ {inputTag}
+
+ )}
+ {leftIndicator && (
+
{leftIndicator}
+ )}
+
+ {rightIndicator && (
+
+ {rightIndicator}
+
+ )}
+
+ {error &&
{error}
}
+
+ );
+};
diff --git a/packages/client/src/create/forms/FormTextArea.tsx b/packages/client/src/create/forms/FormTextArea.tsx
new file mode 100644
index 000000000..acd5d811b
--- /dev/null
+++ b/packages/client/src/create/forms/FormTextArea.tsx
@@ -0,0 +1,50 @@
+import React from "react";
+
+export interface FormTextAreaProps {
+ label?: string;
+ rightIndicator?: React.ReactNode;
+ minRows?: number;
+ maxLength?: number;
+ onClick?: () => void;
+ isLoading?: boolean;
+ [key: string]: any;
+}
+export const FormTextArea = ({
+ label,
+ rightIndicator,
+ minRows = 3,
+ maxLength,
+ onClick,
+ isLoading,
+ error,
+ ...props
+}: FormTextAreaProps) => {
+ return (
+
+
+ {isLoading && (
+
+ )}
+
+
+
+ );
+};
diff --git a/packages/client/src/create/hooks/index.ts b/packages/client/src/create/hooks/index.ts
new file mode 100644
index 000000000..7d2c01958
--- /dev/null
+++ b/packages/client/src/create/hooks/index.ts
@@ -0,0 +1,6 @@
+export * from "./useTokenForm";
+export * from "./useImageUpload";
+export * from "./useTokenGeneration";
+export * from "./useWallet";
+export * from "./useVanityAddress";
+export * from "./useTokenCreation";
\ No newline at end of file
diff --git a/packages/client/src/create/hooks/useImageUpload.ts b/packages/client/src/create/hooks/useImageUpload.ts
new file mode 100644
index 000000000..692981cb1
--- /dev/null
+++ b/packages/client/src/create/hooks/useImageUpload.ts
@@ -0,0 +1,69 @@
+import { useCallback, useState } from "react";
+import { uploadImage } from "../utils/uploadImage";
+import { TokenMetadata } from "../types";
+
+interface UseImageUploadProps {
+ onImageUploaded?: (url: string) => void;
+ onError?: (error: string) => void;
+}
+
+interface UploadResponse {
+ imageUrl: string;
+ metadataUrl: string;
+}
+
+export const useImageUpload = ({
+ onImageUploaded,
+ onError,
+}: UseImageUploadProps = {}) => {
+ const [isUploading, setIsUploading] = useState(false);
+ const [uploadProgress, setUploadProgress] = useState(0);
+ const [uploadedImageUrl, setUploadedImageUrl] = useState(null);
+
+ const handleImageUpload = useCallback(
+ async (file: File, tokenMetadata: TokenMetadata): Promise => {
+ try {
+ setIsUploading(true);
+ setUploadProgress(0);
+
+ if (!tokenMetadata.imageBase64) {
+ throw new Error("Image data (base64) is required");
+ }
+
+ console.log('Uploading with metadata:', tokenMetadata);
+
+ const result = await uploadImage(tokenMetadata);
+
+ setUploadedImageUrl(result.imageUrl);
+ if (onImageUploaded) {
+ onImageUploaded(result.imageUrl);
+ }
+
+ return result;
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : "Failed to upload image";
+ if (onError) {
+ onError(errorMessage);
+ }
+ throw error;
+ } finally {
+ setIsUploading(false);
+ }
+ },
+ [onImageUploaded, onError],
+ );
+
+ const resetUpload = useCallback(() => {
+ setUploadedImageUrl(null);
+ setUploadProgress(0);
+ }, []);
+
+ return {
+ isUploading,
+ uploadProgress,
+ uploadedImageUrl,
+ handleImageUpload,
+ resetUpload,
+ };
+};
diff --git a/packages/client/src/create/hooks/useTokenCreation.ts b/packages/client/src/create/hooks/useTokenCreation.ts
new file mode 100644
index 000000000..7b9d73e93
--- /dev/null
+++ b/packages/client/src/create/hooks/useTokenCreation.ts
@@ -0,0 +1,255 @@
+import { useCreateToken } from "@/hooks/use-create-token";
+import { getAuthToken } from "@/utils/auth";
+import { env } from "@/utils/env";
+import { Keypair } from "@solana/web3.js";
+import { useCallback, useState } from "react";
+import { useNavigate } from "react-router-dom";
+import { toast } from "react-toastify";
+import { FormTab, TokenMetadata } from "../types";
+import { uploadImage } from "../utils/uploadImage";
+
+type CreationStage =
+ | "initializing"
+ | "confirming"
+ | "creating"
+ | "validating"
+ | "finalizing";
+
+interface UseTokenCreationProps {
+ publicKey: string | null;
+ signTransaction: ((transaction: any) => Promise) | null;
+}
+
+interface UseTokenCreationReturn {
+ isCreating: boolean;
+ creationStep: string;
+ creationStage: CreationStage;
+ isSubmitting: boolean;
+ createToken: (
+ tokenMetadata: TokenMetadata,
+ mintKeypair: Keypair,
+ vanityResult: { publicKey: string; secretKey: Keypair },
+ imageFile: File | null,
+ currentPreGeneratedTokenId?: string,
+ userPrompt?: string,
+ activeTab?: FormTab,
+ ) => Promise;
+}
+
+export const useTokenCreation = ({
+ publicKey,
+ signTransaction,
+}: UseTokenCreationProps): UseTokenCreationReturn => {
+ const navigate = useNavigate();
+ const { mutateAsync: createTokenOnChainAsync } = useCreateToken();
+ const [isCreating, setIsCreating] = useState(false);
+ const [creationStep, setCreationStep] = useState("");
+ const [creationStage, setCreationStage] =
+ useState("initializing");
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const createToken = useCallback(
+ async (
+ tokenMetadata: TokenMetadata,
+ mintKeypair: Keypair,
+ vanityResult: { publicKey: string; secretKey: Keypair },
+ imageFile: File | null,
+ currentPreGeneratedTokenId?: string,
+ userPrompt?: string,
+ activeTab?: FormTab,
+ ) => {
+ try {
+ if (!publicKey) {
+ throw new Error("Wallet not connected");
+ }
+
+ if (!signTransaction) {
+ throw new Error("Wallet doesn't support signing");
+ }
+
+ setIsCreating(true);
+ setCreationStage("initializing");
+ setCreationStep("Preparing token creation...");
+ setIsSubmitting(true);
+
+ let media_base64: string | null = null;
+ if (imageFile) {
+ media_base64 = await new Promise((resolve) => {
+ const reader = new FileReader();
+ reader.onloadend = () => {
+ const base64String = reader.result as string;
+ if (!base64String.startsWith("data:")) {
+ resolve(
+ `data:${imageFile.type};base64,${base64String.split(",")[1]}`,
+ );
+ } else {
+ resolve(base64String);
+ }
+ };
+ reader.readAsDataURL(imageFile);
+ });
+ }
+
+ let imageUrl = "";
+ let metadataUrl = "";
+
+ if (media_base64) {
+ try {
+ console.log(
+ "Uploading with base64 format:",
+ media_base64.substring(0, 50) + "...",
+ );
+ const uploadResult = await uploadImage({
+ ...tokenMetadata,
+ tokenMint: vanityResult.publicKey,
+ imageBase64: media_base64,
+ });
+ imageUrl = uploadResult.imageUrl;
+ metadataUrl = uploadResult.metadataUrl;
+
+ if (!metadataUrl || metadataUrl === "undefined") {
+ metadataUrl = env.getMetadataUrl(vanityResult.publicKey);
+ }
+ } catch (uploadError) {
+ console.error("Error uploading image:", uploadError);
+ throw new Error("Failed to upload token image");
+ }
+ } else if (!metadataUrl) {
+ metadataUrl = env.getMetadataUrl(vanityResult.publicKey);
+ }
+
+ try {
+ console.log("Creating token on-chain...");
+ setCreationStage("confirming");
+ setCreationStep("Waiting for wallet confirmation...");
+ setCreationStage("creating");
+ setCreationStep("Creating token on-chain...");
+ setCreationStage("validating");
+ setCreationStep("Validating transaction...");
+ const { signature } = await createTokenOnChainAsync({
+ tokenMetadata,
+ metadataUrl,
+ mintKeypair,
+ });
+
+ if (!signature) {
+ return;
+ }
+
+ setCreationStage("finalizing");
+ setCreationStep("Finalizing token setup...");
+
+ // Send token creation data to backend with signature
+ const authToken = getAuthToken();
+ const headers: Record = {
+ "Content-Type": "application/json",
+ };
+
+ if (authToken) {
+ headers["Authorization"] = `Bearer ${authToken}`;
+ }
+
+ const createResponse = await fetch(env.apiUrl + "/api/create-token", {
+ method: "POST",
+ headers,
+ credentials: "include",
+ body: JSON.stringify({
+ tokenMint: vanityResult.publicKey,
+ mint: vanityResult.publicKey,
+ name: tokenMetadata.name,
+ symbol: tokenMetadata.symbol,
+ description: tokenMetadata.description,
+ twitter: tokenMetadata.links.twitter,
+ telegram: tokenMetadata.links.telegram,
+ website: tokenMetadata.links.website,
+ discord: tokenMetadata.links.discord,
+ imageUrl: imageUrl || "",
+ metadataUrl: metadataUrl || "",
+ signature: signature,
+ decimals: tokenMetadata.decimals,
+ supply: tokenMetadata.supply,
+ freezeAuthority: tokenMetadata.freezeAuthority,
+ mintAuthority: tokenMetadata.mintAuthority,
+ }),
+ });
+
+ if (!createResponse.ok) {
+ const errorData = (await createResponse.json()) as {
+ error?: string;
+ };
+ throw new Error(errorData.error || "Failed to create token entry");
+ }
+
+ if (currentPreGeneratedTokenId && activeTab === FormTab.AUTO) {
+ try {
+ const authToken = getAuthToken();
+ const headers: Record = {
+ "Content-Type": "application/json",
+ };
+
+ if (authToken) {
+ headers["Authorization"] = `Bearer ${authToken}`;
+ }
+
+ await fetch(env.apiUrl + "/api/generation/mark-token-used", {
+ method: "POST",
+ headers,
+ credentials: "include",
+ body: JSON.stringify({
+ id: currentPreGeneratedTokenId,
+ name: tokenMetadata.name,
+ ticker: tokenMetadata.symbol,
+ concept: userPrompt,
+ }),
+ });
+ } catch (error) {
+ console.error(
+ "Error marking pre-generated token as used:",
+ error,
+ );
+ }
+ }
+
+ if (window.createConfettiFireworks) {
+ window.createConfettiFireworks();
+ }
+
+ localStorage.removeItem("import_token_data");
+
+ navigate(`/token/${vanityResult.publicKey}`);
+ } catch (error) {
+ console.error("Error creating token:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Failed to create token. Please try again.",
+ );
+ setIsCreating(false);
+ setCreationStep("");
+ setCreationStage("initializing");
+ } finally {
+ setIsSubmitting(false);
+ }
+ } catch (error) {
+ console.error("Error creating token:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Failed to create token. Please try again.",
+ );
+ setIsCreating(false);
+ setCreationStep("");
+ setCreationStage("initializing");
+ }
+ },
+ [publicKey, signTransaction, createTokenOnChainAsync, navigate],
+ );
+
+ return {
+ isCreating,
+ creationStep,
+ creationStage,
+ isSubmitting,
+ createToken,
+ };
+};
diff --git a/packages/client/src/create/hooks/useTokenForm.ts b/packages/client/src/create/hooks/useTokenForm.ts
new file mode 100644
index 000000000..946954d27
--- /dev/null
+++ b/packages/client/src/create/hooks/useTokenForm.ts
@@ -0,0 +1,146 @@
+import { useState, useCallback } from "react";
+import { FormTab, FormState, FormErrors } from "../types";
+import { MAX_INITIAL_SOL } from "../consts";
+
+interface UseTokenFormProps {
+ initialForm?: Partial;
+ onFormChange?: (form: FormState) => void;
+}
+
+export const useTokenForm = ({ initialForm, onFormChange }: UseTokenFormProps = {}) => {
+ const [form, setForm] = useState({
+ name: initialForm?.name || "",
+ symbol: initialForm?.symbol || "",
+ description: initialForm?.description || "",
+ prompt: initialForm?.prompt || "",
+ initialSol: initialForm?.initialSol || "0",
+ links: {
+ twitter: initialForm?.links?.twitter || "",
+ telegram: initialForm?.links?.telegram || "",
+ website: initialForm?.links?.website || "",
+ discord: initialForm?.links?.discord || "",
+ farcaster: initialForm?.links?.farcaster || "",
+ },
+ importAddress: initialForm?.importAddress || "",
+ });
+
+ const [errors, setErrors] = useState({
+ name: "",
+ symbol: "",
+ description: "",
+ prompt: "",
+ initialSol: "",
+ userPrompt: "",
+ importAddress: "",
+ percentage: "",
+ });
+
+ const handleChange = useCallback((field: string, value: string) => {
+ setForm((prev) => {
+ let newForm;
+ if (field.includes(".")) {
+ const [parent, child] = field.split(".");
+ if (parent === "links") {
+ newForm = {
+ ...prev,
+ links: {
+ ...prev.links,
+ [child]: value,
+ },
+ };
+ } else {
+ newForm = prev;
+ }
+ } else {
+ newForm = {
+ ...prev,
+ [field]: value,
+ };
+ }
+
+ if (onFormChange) {
+ onFormChange(newForm);
+ }
+
+ return newForm;
+ });
+
+ if (field === "name" || field === "symbol" || field === "description") {
+ if (value) {
+ setErrors((prev) => ({
+ ...prev,
+ [field]: "",
+ }));
+ } else {
+ setErrors((prev) => ({
+ ...prev,
+ [field]: `${field.charAt(0) + field.slice(1)} is required`,
+ }));
+ }
+ }
+
+ if (field === "initialSol" && value) {
+ const numValue = parseFloat(value);
+ if (numValue < 0 || numValue > MAX_INITIAL_SOL) {
+ setErrors((prev) => ({
+ ...prev,
+ initialSol: `Max initial SOL is ${MAX_INITIAL_SOL}`,
+ }));
+ } else {
+ setErrors((prev) => ({
+ ...prev,
+ initialSol: "",
+ }));
+ }
+ }
+ }, [onFormChange]);
+
+ const validateForm = useCallback(() => {
+ const newErrors = { ...errors };
+ let isValid = true;
+
+ if (!form.name) {
+ newErrors.name = "Name is required";
+ isValid = false;
+ }
+ if (!form.symbol) {
+ newErrors.symbol = "Symbol is required";
+ isValid = false;
+ }
+ if (!form.description) {
+ newErrors.description = "Description is required";
+ isValid = false;
+ }
+
+ const initialSol = parseFloat(form.initialSol);
+ if (isNaN(initialSol) || initialSol < 0 || initialSol > MAX_INITIAL_SOL) {
+ newErrors.initialSol = `Initial SOL must be between 0 and ${MAX_INITIAL_SOL}`;
+ isValid = false;
+ }
+
+ setErrors(newErrors);
+ return isValid;
+ }, [form, errors]);
+
+ const isFormValid = useCallback(() => {
+ return (
+ !!form.name &&
+ !!form.symbol &&
+ !!form.description &&
+ !errors.name &&
+ !errors.symbol &&
+ !errors.description &&
+ !errors.initialSol
+ );
+ }, [form, errors]);
+
+ return {
+ form,
+ errors,
+ handleChange,
+ validateForm,
+ isFormValid,
+ setForm,
+ setErrors,
+ };
+};
\ No newline at end of file
diff --git a/packages/client/src/create/hooks/useTokenGeneration.ts b/packages/client/src/create/hooks/useTokenGeneration.ts
new file mode 100644
index 000000000..e6751a6c3
--- /dev/null
+++ b/packages/client/src/create/hooks/useTokenGeneration.ts
@@ -0,0 +1,164 @@
+import { useState, useCallback } from "react";
+import type { TokenMetadata, GenerateMetadataResponse, GenerateImageResponse } from "../types";
+import { ERROR_MESSAGES } from "../consts";
+import { env } from "@/utils/env";
+import { getAuthToken } from "@/utils/auth";
+
+interface UseTokenGenerationProps {
+ onGenerationComplete?: (metadata: TokenMetadata) => void;
+ onError?: (error: string) => void;
+ onImageUrlUpdate?: (imageUrl: string, imageFile?: File) => void;
+}
+
+export const useTokenGeneration = ({
+ onGenerationComplete,
+ onError,
+ onImageUrlUpdate
+}: UseTokenGenerationProps = {}) => {
+ const [isGenerating, setIsGenerating] = useState(false);
+ const [generationProgress, setGenerationProgress] = useState(0);
+ const [generatedMetadata, setGeneratedMetadata] = useState(null);
+ const [currentImageUrl, setCurrentImageUrl] = useState(null);
+ const [currentImageFile, setCurrentImageFile] = useState(null);
+
+ const generateToken = useCallback(async (prompt: string) => {
+ try {
+ setIsGenerating(true);
+ setGenerationProgress(0);
+ setCurrentImageUrl(null);
+ setCurrentImageFile(null);
+
+ const authToken = getAuthToken();
+ const headers: Record = {
+ "Content-Type": "application/json",
+ };
+ if (authToken) {
+ headers["Authorization"] = `Bearer ${authToken}`;
+ }
+
+ setGenerationProgress(25);
+ const metadataResponse = await fetch(
+ `${env.apiUrl}/api/generation/generate-metadata`,
+ {
+ method: "POST",
+ headers,
+ credentials: "include",
+ body: JSON.stringify({
+ prompt,
+ fields: ["name", "symbol", "description", "prompt"],
+ }),
+ }
+ );
+
+ if (!metadataResponse.ok) {
+ throw new Error("Failed to generate metadata from prompt");
+ }
+
+ const metadataData = (await metadataResponse.json()) as GenerateMetadataResponse;
+ if (!metadataData.success || !metadataData.metadata) {
+ throw new Error("Invalid response from the metadata generation API");
+ }
+
+ setGenerationProgress(50);
+ const imageResponse = await fetch(
+ `${env.apiUrl}/api/generation/generate`,
+ {
+ method: "POST",
+ headers,
+ credentials: "include",
+ body: JSON.stringify({
+ prompt: metadataData.metadata.prompt,
+ type: "image",
+ }),
+ }
+ );
+
+ if (!imageResponse.ok) {
+ const errorText = await imageResponse.text();
+ console.error("Image generation API returned an error:", errorText);
+ const backendError = JSON.parse(errorText).error;
+
+ let userErrorMessage = "Failed to generate image for token.";
+ if (backendError.includes("NSFW")) {
+ userErrorMessage = "Your input contains inappropriate content. Please modify and try again.";
+ }
+ throw new Error(userErrorMessage);
+ }
+
+ const imageData = (await imageResponse.json()) as GenerateImageResponse;
+ if (!imageData.success || !imageData.mediaUrl) {
+ throw new Error("Image generation API returned invalid data");
+ }
+
+ try {
+ const imageBlob = await fetch(imageData.mediaUrl).then(r => r.blob());
+ const imageFile = new File([imageBlob], "generated-image.png", {
+ type: "image/png",
+ });
+ setCurrentImageFile(imageFile);
+
+ setCurrentImageUrl(imageData.mediaUrl);
+ if (onImageUrlUpdate) {
+ onImageUrlUpdate(imageData.mediaUrl, imageFile);
+ }
+ } catch (error) {
+ console.error("Error creating image file:", error);
+ setCurrentImageUrl(imageData.mediaUrl);
+ if (onImageUrlUpdate) {
+ onImageUrlUpdate(imageData.mediaUrl);
+ }
+ }
+
+ setGenerationProgress(75);
+ const finalMetadata: TokenMetadata = {
+ name: metadataData.metadata.name,
+ symbol: metadataData.metadata.symbol,
+ description: metadataData.metadata.description,
+ initialSol: 0,
+ links: {
+ twitter: "",
+ telegram: "",
+ farcaster: "",
+ website: "",
+ discord: "",
+ },
+ imageBase64: null,
+ tokenMint: "",
+ decimals: 9,
+ supply: 1000000000000000,
+ freezeAuthority: "",
+ mintAuthority: "",
+ };
+
+ setGenerationProgress(100);
+ setGeneratedMetadata(finalMetadata);
+ if (onGenerationComplete) {
+ onGenerationComplete(finalMetadata);
+ }
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : ERROR_MESSAGES.UNKNOWN_ERROR;
+ if (onError) {
+ onError(errorMessage);
+ }
+ } finally {
+ setIsGenerating(false);
+ }
+ }, [onGenerationComplete, onError, onImageUrlUpdate]);
+
+ const resetGeneration = useCallback(() => {
+ setGeneratedMetadata(null);
+ setGenerationProgress(0);
+ setCurrentImageUrl(null);
+ setCurrentImageFile(null);
+ }, []);
+
+ return {
+ isGenerating,
+ generationProgress,
+ generatedMetadata,
+ currentImageUrl,
+ currentImageFile,
+ generateToken,
+ resetGeneration,
+ };
+};
\ No newline at end of file
diff --git a/packages/client/src/create/hooks/useVanityAddress.ts b/packages/client/src/create/hooks/useVanityAddress.ts
new file mode 100644
index 000000000..cba4dd22e
--- /dev/null
+++ b/packages/client/src/create/hooks/useVanityAddress.ts
@@ -0,0 +1,129 @@
+import { useCallback, useEffect, useRef, useState } from "react";
+import { Keypair } from "@solana/web3.js";
+import { toast } from "react-toastify";
+import { BASE58_REGEX } from "../consts";
+import InlineVanityWorker from "@/workers/vanityWorker?worker&inline";
+import { VanityResult, WorkerMessage } from "../types";
+
+export const useVanityAddress = () => {
+ const [vanitySuffix, setVanitySuffix] = useState("FUN");
+ const [isGeneratingVanity, setIsGeneratingVanity] = useState(false);
+ const [vanityResult, setVanityResult] = useState(null);
+ const [displayedPublicKey, setDisplayedPublicKey] = useState(
+ "--- Generate a vanity address ---"
+ );
+ const [suffixError, setSuffixError] = useState(null);
+
+ const workersRef = useRef([]);
+ const startTimeRef = useRef(null);
+ const displayUpdateIntervalRef = useRef(null);
+ const isGeneratingVanityRef = useRef(isGeneratingVanity);
+
+ useEffect(() => {
+ isGeneratingVanityRef.current = isGeneratingVanity;
+ }, [isGeneratingVanity]);
+
+ const stopVanityGeneration = useCallback(() => {
+ if (!isGeneratingVanityRef.current) return;
+ setIsGeneratingVanity(false);
+ workersRef.current.forEach((worker) => {
+ try {
+ worker.postMessage("stop");
+ } catch (e) {
+ console.warn("Couldn't send stop message to worker", e);
+ }
+ setTimeout(() => {
+ try {
+ worker.terminate();
+ } catch (e) {
+ /* ignore */
+ }
+ }, 100);
+ });
+ workersRef.current = [];
+ startTimeRef.current = null;
+ if (displayUpdateIntervalRef.current) {
+ clearInterval(displayUpdateIntervalRef.current);
+ displayUpdateIntervalRef.current = null;
+ }
+ }, []);
+
+ const startVanityGeneration = useCallback(() => {
+ const suffix = vanitySuffix.trim();
+ setVanityResult(null);
+ setDisplayedPublicKey("Generating...");
+
+ let currentError = null;
+
+ if (!suffix) {
+ currentError = "Suffix cannot be empty.";
+ } else if (suffix.length > 5) {
+ currentError = "Suffix cannot be longer than 5 characters.";
+ } else if (!BASE58_REGEX.test(suffix)) {
+ currentError = "Suffix contains invalid Base58 characters.";
+ }
+
+ if (!currentError) {
+ if (suffix.length === 5) {
+ currentError = "Warning: 5-letter suffix may take 24+ hours to find!";
+ toast.warn(currentError);
+ } else if (suffix.length === 4) {
+ currentError = "Note: 4-letter suffix may take some time to find.";
+ toast.info(currentError);
+ }
+ }
+
+ setSuffixError(currentError);
+ if (currentError && !currentError.startsWith("Warning") && !currentError.startsWith("Note")) {
+ return;
+ }
+
+ stopVanityGeneration();
+ setIsGeneratingVanity(true);
+
+ const numWorkers = navigator.hardwareConcurrency > 12 ? 8 : navigator.hardwareConcurrency || 4;
+ startTimeRef.current = Date.now();
+ workersRef.current = [];
+
+ const base58Chars = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
+ const generateRandomString = (length: number) => {
+ let result = "";
+ for (let i = 0; i < length; i++) {
+ result += base58Chars.charAt(Math.floor(Math.random() * base58Chars.length));
+ }
+ return result;
+ };
+
+ displayUpdateIntervalRef.current = setInterval(() => {
+ const prefixLength = 44 - suffix.length;
+ const randomPrefix = generateRandomString(prefixLength);
+ setDisplayedPublicKey(`${randomPrefix}${suffix}`);
+ }, 100);
+
+ for (let i = 0; i < numWorkers; i++) {
+ const worker = new InlineVanityWorker();
+ worker.onmessage = (e: MessageEvent) => {
+ if (e.data.type === "found") {
+ const { publicKey, secretKey } = e.data;
+ const keypair = Keypair.fromSecretKey(new Uint8Array(secretKey));
+ setVanityResult({ publicKey, secretKey: keypair });
+ setDisplayedPublicKey(publicKey);
+ stopVanityGeneration();
+ }
+ };
+ worker.postMessage({ suffix });
+ workersRef.current.push(worker);
+ }
+ }, [suffixError, stopVanityGeneration, vanitySuffix]);
+
+ return {
+ vanitySuffix,
+ setVanitySuffix,
+ isGeneratingVanity,
+ vanityResult,
+ displayedPublicKey,
+ suffixError,
+ startVanityGeneration,
+ stopVanityGeneration,
+ };
+};
\ No newline at end of file
diff --git a/packages/client/src/create/hooks/useWallet.ts b/packages/client/src/create/hooks/useWallet.ts
new file mode 100644
index 000000000..cf17d75a0
--- /dev/null
+++ b/packages/client/src/create/hooks/useWallet.ts
@@ -0,0 +1,69 @@
+import { useWallet as useSolanaWallet } from "@solana/wallet-adapter-react";
+import { PublicKey, Transaction, VersionedTransaction } from "@solana/web3.js";
+import { useCallback, useEffect, useState } from "react";
+import { ERROR_MESSAGES } from "../consts";
+
+interface UseWalletReturn {
+ publicKey: PublicKey | null;
+ signTransaction:
+ | ((
+ transaction: T,
+ ) => Promise)
+ | null;
+ isConnected: boolean;
+ isConnecting: boolean;
+ connect: () => Promise;
+ disconnect: () => Promise;
+ error: string | null;
+}
+
+export const useWallet = (): UseWalletReturn => {
+ const {
+ publicKey,
+ signTransaction,
+ connected,
+ connecting,
+ connect: solanaConnect,
+ disconnect: solanaDisconnect,
+ } = useSolanaWallet();
+
+ const [error, setError] = useState(null);
+
+ const connect = useCallback(async () => {
+ try {
+ setError(null);
+ await solanaConnect();
+ } catch (err) {
+ setError(
+ err instanceof Error ? err.message : ERROR_MESSAGES.UNKNOWN_ERROR,
+ );
+ }
+ }, [solanaConnect]);
+
+ const disconnect = useCallback(async () => {
+ try {
+ setError(null);
+ await solanaDisconnect();
+ } catch (err) {
+ setError(
+ err instanceof Error ? err.message : ERROR_MESSAGES.UNKNOWN_ERROR,
+ );
+ }
+ }, [solanaDisconnect]);
+
+ useEffect(() => {
+ if (!connected) {
+ setError(null);
+ }
+ }, [connected]);
+
+ return {
+ publicKey,
+ signTransaction: signTransaction as UseWalletReturn["signTransaction"],
+ isConnected: connected,
+ isConnecting: connecting,
+ connect,
+ disconnect,
+ error,
+ };
+};
diff --git a/packages/client/src/create/types.ts b/packages/client/src/create/types.ts
new file mode 100644
index 000000000..0b7892772
--- /dev/null
+++ b/packages/client/src/create/types.ts
@@ -0,0 +1,148 @@
+import { Keypair } from "@solana/web3.js";
+
+export enum FormTab {
+ AUTO = "auto",
+ MANUAL = "manual",
+ IMPORT = "import",
+}
+
+export interface FormState {
+ name: string;
+ symbol: string;
+ description: string;
+ prompt: string;
+ initialSol: string;
+ links: {
+ twitter: string;
+ telegram: string;
+ website: string;
+ discord: string;
+ farcaster: string;
+ };
+ importAddress: string;
+ coinDropImageUrl?: string;
+}
+
+export interface FormErrors {
+ name: string;
+ symbol: string;
+ description: string;
+ prompt: string;
+ initialSol: string;
+ userPrompt: string;
+ importAddress: string;
+ percentage: string;
+}
+
+export interface TokenMetadata {
+ name: string;
+ symbol: string;
+ description: string;
+ initialSol: number;
+ links: {
+ twitter: string;
+ telegram: string;
+ farcaster: string;
+ website: string;
+ discord: string;
+ };
+ imageBase64: string | null;
+ tokenMint: string;
+ decimals: number;
+ supply: number;
+ freezeAuthority: string;
+ mintAuthority: string;
+}
+
+export interface TokenCreationStage {
+ step: number;
+ stage: "uploading" | "creating" | "finalizing" | "complete";
+ message: string;
+}
+
+export interface TokenCreationError {
+ message: string;
+ code?: string;
+ retryable?: boolean;
+}
+
+export interface TokenCreationResult {
+ mintAddress: string;
+ metadataUrl: string;
+ tokenMetadata: TokenMetadata;
+}
+
+export interface UploadResponse {
+ success: boolean;
+ imageUrl: string;
+ metadataUrl: string;
+}
+
+export interface GenerateImageResponse {
+ success: boolean;
+ mediaUrl: string;
+ remainingGenerations: number;
+ resetTime: string;
+}
+
+export interface PreGeneratedTokenResponse {
+ success: boolean;
+ token: {
+ id: string;
+ name: string;
+ ticker: string;
+ description: string;
+ prompt: string;
+ image?: string;
+ createdAt: string;
+ used: number;
+ };
+}
+
+export interface GenerateMetadataResponse {
+ success: boolean;
+ metadata: {
+ name: string;
+ symbol: string;
+ description: string;
+ prompt: string;
+ };
+}
+
+export interface UploadImportImageResponse {
+ success: boolean;
+ imageUrl: string;
+}
+
+export interface TokenSearchData {
+ name?: string;
+ symbol?: string;
+ description?: string;
+ creator?: string;
+ creators?: string[];
+ image?: string;
+ mint: string;
+ twitter?: string;
+ telegram?: string;
+ website?: string;
+ discord?: string;
+ metadataUri?: string;
+ isCreator?: boolean;
+ updateAuthority?: string;
+}
+
+export type VanityResult = {
+ publicKey: string;
+ secretKey: Keypair;
+};
+
+export type WorkerMessage =
+ | {
+ type: "found";
+ workerId: number;
+ publicKey: string;
+ secretKey: number[];
+ validated: boolean;
+ }
+ | { type: "progress"; workerId: number; count: number }
+ | { type: "error"; workerId: number; error: string };
diff --git a/packages/client/src/create/utils/uploadImage.ts b/packages/client/src/create/utils/uploadImage.ts
new file mode 100644
index 000000000..a4bd18e7f
--- /dev/null
+++ b/packages/client/src/create/utils/uploadImage.ts
@@ -0,0 +1,88 @@
+import { TokenMetadata, UploadResponse } from "@/create/types";
+import { getAuthToken } from "@/utils/auth";
+import { env } from "@/utils/env";
+
+export const uploadImage = async (metadata: TokenMetadata) => {
+ if (!metadata.imageBase64) {
+ throw new Error("Image data (base64) is required");
+ }
+
+ // Determine a safe filename based on token metadata
+ const safeName = metadata.name.toLowerCase().replace(/[^a-z0-9]/g, "_");
+
+ // Get the image type from the data URL
+ const contentType =
+ metadata.imageBase64.match(/^data:([A-Za-z-+/]+);base64,/)?.[1] || "";
+
+ let extension = ".jpg";
+ if (contentType.includes("png")) extension = ".png";
+ else if (contentType.includes("gif")) extension = ".gif";
+ else if (contentType.includes("svg")) extension = ".svg";
+ else if (contentType.includes("webp")) extension = ".webp";
+
+ const filename = `${safeName}${extension}`;
+
+ console.log(
+ `Uploading image as ${filename} with content type ${contentType}`,
+ );
+
+ // Get auth token from localStorage with quote handling
+ const authToken = getAuthToken();
+
+ // Prepare headers
+ const headers: Record = {
+ "Content-Type": "application/json",
+ };
+
+ if (authToken) {
+ headers["Authorization"] = `Bearer ${authToken}`;
+ }
+
+ // Extract the base64 data without the data URL prefix
+ const base64Data = metadata.imageBase64.split(",")[1];
+ if (!base64Data) {
+ throw new Error("Invalid base64 image data format");
+ }
+
+ console.log("Sending request with base64 data length:", base64Data.length);
+
+ const response = await fetch(env.apiUrl + "/api/upload", {
+ method: "POST",
+ headers,
+ credentials: "include",
+ body: JSON.stringify({
+ image: metadata.imageBase64,
+ metadata: {
+ name: metadata.name,
+ symbol: metadata.symbol,
+ description: metadata.description,
+ twitter: metadata.links.twitter,
+ telegram: metadata.links.telegram,
+ website: metadata.links.website,
+ discord: metadata.links.discord,
+ },
+ }),
+ });
+
+ if (!response.ok) {
+ if (response.status === 401) {
+ throw new Error(
+ "Authentication required. Please connect your wallet and try again.",
+ );
+ }
+ const errorText = await response.text();
+ console.error("Upload error response:", errorText);
+ throw new Error("Failed to upload image: " + errorText);
+ }
+
+ const result = (await response.json()) as UploadResponse;
+
+ if (!result.metadataUrl || result.metadataUrl === "undefined") {
+ console.warn("No metadata URL returned from server, using fallback URL");
+ result.metadataUrl = env.getMetadataUrl(
+ metadata.tokenMint || crypto.randomUUID(),
+ );
+ }
+
+ return result;
+};
diff --git a/packages/client/src/create/utils/waitFortokenCreation.ts b/packages/client/src/create/utils/waitFortokenCreation.ts
new file mode 100644
index 000000000..20ef8ccef
--- /dev/null
+++ b/packages/client/src/create/utils/waitFortokenCreation.ts
@@ -0,0 +1,25 @@
+import { HomepageTokenSchema } from "@/hooks/use-tokens";
+import { getSocket } from "@/utils/socket";
+
+export const waitForTokenCreation = async (mint: string, timeout = 80_000) => {
+ return new Promise((resolve, reject) => {
+ const socket = getSocket();
+
+ const newTokenListener = (token: unknown) => {
+ const { mint: newMint } = HomepageTokenSchema.parse(token);
+ if (newMint === mint) {
+ clearTimeout(timerId);
+ socket.off("newToken", newTokenListener);
+ resolve();
+ }
+ };
+
+ socket.emit("subscribeGlobal");
+ socket.on("newToken", newTokenListener);
+
+ const timerId = setTimeout(() => {
+ socket.off("newToken", newTokenListener);
+ reject(new Error("Token creation timed out"));
+ }, timeout);
+ });
+};
diff --git a/packages/client/src/create/validators.ts b/packages/client/src/create/validators.ts
new file mode 100644
index 000000000..30b6785ae
--- /dev/null
+++ b/packages/client/src/create/validators.ts
@@ -0,0 +1,6 @@
+export const BASE58_REGEX = /^[1-9A-HJ-NP-Za-km-z]+$/;
+export function isValidTokenAddress(address: string): boolean {
+ return (
+ BASE58_REGEX.test(address) && address.length >= 32 && address.length <= 44
+ );
+}
From 76ec5007f9ec87579a4b8ef8d1aec19cc4b8acaa Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Fri, 9 May 2025 10:32:55 +0000
Subject: [PATCH 3/3] chore(lint): auto-fix lint issues
---
.../create/components/BuySection.tsx | 28 +-
.../src/create/components/LaunchButton.tsx | 9 +-
packages/client/src/create/consts.ts | 2 +-
packages/client/src/create/hooks/index.ts | 6 +-
.../client/src/create/hooks/useImageUpload.ts | 9 +-
.../client/src/create/hooks/useTokenForm.ts | 112 ++++----
.../src/create/hooks/useTokenGeneration.ts | 251 +++++++++---------
.../src/create/hooks/useVanityAddress.ts | 28 +-
packages/client/src/hooks/use-create-token.ts | 6 +-
9 files changed, 248 insertions(+), 203 deletions(-)
diff --git a/packages/client/src/components/create/components/BuySection.tsx b/packages/client/src/components/create/components/BuySection.tsx
index a80a33a02..e2e147aea 100644
--- a/packages/client/src/components/create/components/BuySection.tsx
+++ b/packages/client/src/components/create/components/BuySection.tsx
@@ -1,6 +1,6 @@
-import { FormTab } from "../types";
import { Icons } from "../../icons";
import { MAX_INITIAL_SOL, TOKEN_SUPPLY, VIRTUAL_RESERVES } from "../consts";
+import { FormTab } from "../types";
interface BuySectionProps {
activeTab: FormTab;
@@ -31,7 +31,8 @@ export const BuySection = ({
const lamports = solAmount * 1e9;
// Using constant product formula: (dx * y) / (x + dx)
// where x is virtual reserves, y is token supply, dx is input SOL amount
- const tokenAmount = (lamports * TOKEN_SUPPLY) / (VIRTUAL_RESERVES + lamports);
+ const tokenAmount =
+ (lamports * TOKEN_SUPPLY) / (VIRTUAL_RESERVES + lamports);
return tokenAmount;
};
@@ -62,7 +63,8 @@ export const BuySection = ({
Total token supply: {TOKEN_SUPPLY.toLocaleString()} tokens
- Pricing follows a bonding curve, your percentage increases with more SOL.
+ Pricing follows a bonding curve, your percentage increases
+ with more SOL.
@@ -90,8 +92,13 @@ export const BuySection = ({
let decimalPart = parts[1] || "";
// Limit whole part length (e.g., 2 digits for SOL up to 99)
- if (wholePart.length > String(Math.floor(MAX_INITIAL_SOL)).length) {
- wholePart = wholePart.slice(0, String(Math.floor(MAX_INITIAL_SOL)).length);
+ if (
+ wholePart.length > String(Math.floor(MAX_INITIAL_SOL)).length
+ ) {
+ wholePart = wholePart.slice(
+ 0,
+ String(Math.floor(MAX_INITIAL_SOL)).length,
+ );
}
// Limit decimal part length
if (decimalPart.length > 2) {
@@ -105,7 +112,8 @@ export const BuySection = ({
const numValue = parseFloat(value);
if (!isNaN(numValue)) {
if (numValue < 0) value = "0";
- else if (numValue > maxInputSol) value = maxInputSol.toString();
+ else if (numValue > maxInputSol)
+ value = maxInputSol.toString();
} else if (value !== "") {
value = "0"; // Reset invalid non-empty strings
}
@@ -125,7 +133,11 @@ export const BuySection = ({
{parseFloat(buyValue) > 0 && (
- ≈ {calculatePercentage(calculateTokensFromSol(parseFloat(buyValue))).toFixed(2)} % of supply
+ ≈{" "}
+ {calculatePercentage(
+ calculateTokensFromSol(parseFloat(buyValue)),
+ ).toFixed(2)}{" "}
+ % of supply
)}
@@ -145,4 +157,4 @@ export const BuySection = ({
);
-};
\ No newline at end of file
+};
diff --git a/packages/client/src/create/components/LaunchButton.tsx b/packages/client/src/create/components/LaunchButton.tsx
index fa30c2aa7..0ce12ba8d 100644
--- a/packages/client/src/create/components/LaunchButton.tsx
+++ b/packages/client/src/create/components/LaunchButton.tsx
@@ -29,8 +29,8 @@ export const LaunchButton = ({
isSubmitting || isCreating
? "/create/launching.svg"
: activeTab === FormTab.IMPORT
- ? "/create/importup-thick.svg"
- : "/create/launchup.svg"
+ ? "/create/importup-thick.svg"
+ : "/create/launchup.svg"
}
alt="Launch"
className="h-32 mb-4 select-none pointer-events-none"
@@ -43,7 +43,8 @@ export const LaunchButton = ({
) : !canLaunch && !isSubmitting && activeTab !== FormTab.IMPORT ? (
- Please fill required fields, ensure sufficient SOL, and generate a vanity address.
+ Please fill required fields, ensure sufficient SOL, and generate a
+ vanity address.
) : !canLaunch && !isSubmitting && activeTab === FormTab.IMPORT ? (
@@ -52,4 +53,4 @@ export const LaunchButton = ({
) : null}
);
-};
\ No newline at end of file
+};
diff --git a/packages/client/src/create/consts.ts b/packages/client/src/create/consts.ts
index f04792fb1..87519a868 100644
--- a/packages/client/src/create/consts.ts
+++ b/packages/client/src/create/consts.ts
@@ -1,4 +1,4 @@
-import { env, isDevnet } from "@/utils/env";
+import { env } from "@/utils/env";
export const MAX_INITIAL_SOL = 1000;
export const MAX_NAME_LENGTH = 32;
diff --git a/packages/client/src/create/hooks/index.ts b/packages/client/src/create/hooks/index.ts
index 7d2c01958..6383b950e 100644
--- a/packages/client/src/create/hooks/index.ts
+++ b/packages/client/src/create/hooks/index.ts
@@ -1,6 +1,6 @@
-export * from "./useTokenForm";
export * from "./useImageUpload";
+export * from "./useTokenCreation";
+export * from "./useTokenForm";
export * from "./useTokenGeneration";
-export * from "./useWallet";
export * from "./useVanityAddress";
-export * from "./useTokenCreation";
\ No newline at end of file
+export * from "./useWallet";
diff --git a/packages/client/src/create/hooks/useImageUpload.ts b/packages/client/src/create/hooks/useImageUpload.ts
index 692981cb1..9433bd626 100644
--- a/packages/client/src/create/hooks/useImageUpload.ts
+++ b/packages/client/src/create/hooks/useImageUpload.ts
@@ -1,6 +1,6 @@
import { useCallback, useState } from "react";
-import { uploadImage } from "../utils/uploadImage";
import { TokenMetadata } from "../types";
+import { uploadImage } from "../utils/uploadImage";
interface UseImageUploadProps {
onImageUploaded?: (url: string) => void;
@@ -21,7 +21,10 @@ export const useImageUpload = ({
const [uploadedImageUrl, setUploadedImageUrl] = useState(null);
const handleImageUpload = useCallback(
- async (file: File, tokenMetadata: TokenMetadata): Promise => {
+ async (
+ file: File,
+ tokenMetadata: TokenMetadata,
+ ): Promise => {
try {
setIsUploading(true);
setUploadProgress(0);
@@ -30,7 +33,7 @@ export const useImageUpload = ({
throw new Error("Image data (base64) is required");
}
- console.log('Uploading with metadata:', tokenMetadata);
+ console.log("Uploading with metadata:", tokenMetadata);
const result = await uploadImage(tokenMetadata);
diff --git a/packages/client/src/create/hooks/useTokenForm.ts b/packages/client/src/create/hooks/useTokenForm.ts
index 946954d27..9c12fc2fa 100644
--- a/packages/client/src/create/hooks/useTokenForm.ts
+++ b/packages/client/src/create/hooks/useTokenForm.ts
@@ -1,13 +1,16 @@
-import { useState, useCallback } from "react";
-import { FormTab, FormState, FormErrors } from "../types";
+import { useCallback, useState } from "react";
import { MAX_INITIAL_SOL } from "../consts";
+import { FormErrors, FormState } from "../types";
interface UseTokenFormProps {
initialForm?: Partial;
onFormChange?: (form: FormState) => void;
}
-export const useTokenForm = ({ initialForm, onFormChange }: UseTokenFormProps = {}) => {
+export const useTokenForm = ({
+ initialForm,
+ onFormChange,
+}: UseTokenFormProps = {}) => {
const [form, setForm] = useState({
name: initialForm?.name || "",
symbol: initialForm?.symbol || "",
@@ -35,65 +38,68 @@ export const useTokenForm = ({ initialForm, onFormChange }: UseTokenFormProps =
percentage: "",
});
- const handleChange = useCallback((field: string, value: string) => {
- setForm((prev) => {
- let newForm;
- if (field.includes(".")) {
- const [parent, child] = field.split(".");
- if (parent === "links") {
+ const handleChange = useCallback(
+ (field: string, value: string) => {
+ setForm((prev) => {
+ let newForm;
+ if (field.includes(".")) {
+ const [parent, child] = field.split(".");
+ if (parent === "links") {
+ newForm = {
+ ...prev,
+ links: {
+ ...prev.links,
+ [child]: value,
+ },
+ };
+ } else {
+ newForm = prev;
+ }
+ } else {
newForm = {
...prev,
- links: {
- ...prev.links,
- [child]: value,
- },
+ [field]: value,
};
- } else {
- newForm = prev;
}
- } else {
- newForm = {
- ...prev,
- [field]: value,
- };
- }
- if (onFormChange) {
- onFormChange(newForm);
- }
+ if (onFormChange) {
+ onFormChange(newForm);
+ }
- return newForm;
- });
+ return newForm;
+ });
- if (field === "name" || field === "symbol" || field === "description") {
- if (value) {
- setErrors((prev) => ({
- ...prev,
- [field]: "",
- }));
- } else {
- setErrors((prev) => ({
- ...prev,
- [field]: `${field.charAt(0) + field.slice(1)} is required`,
- }));
+ if (field === "name" || field === "symbol" || field === "description") {
+ if (value) {
+ setErrors((prev) => ({
+ ...prev,
+ [field]: "",
+ }));
+ } else {
+ setErrors((prev) => ({
+ ...prev,
+ [field]: `${field.charAt(0) + field.slice(1)} is required`,
+ }));
+ }
}
- }
- if (field === "initialSol" && value) {
- const numValue = parseFloat(value);
- if (numValue < 0 || numValue > MAX_INITIAL_SOL) {
- setErrors((prev) => ({
- ...prev,
- initialSol: `Max initial SOL is ${MAX_INITIAL_SOL}`,
- }));
- } else {
- setErrors((prev) => ({
- ...prev,
- initialSol: "",
- }));
+ if (field === "initialSol" && value) {
+ const numValue = parseFloat(value);
+ if (numValue < 0 || numValue > MAX_INITIAL_SOL) {
+ setErrors((prev) => ({
+ ...prev,
+ initialSol: `Max initial SOL is ${MAX_INITIAL_SOL}`,
+ }));
+ } else {
+ setErrors((prev) => ({
+ ...prev,
+ initialSol: "",
+ }));
+ }
}
- }
- }, [onFormChange]);
+ },
+ [onFormChange],
+ );
const validateForm = useCallback(() => {
const newErrors = { ...errors };
@@ -143,4 +149,4 @@ export const useTokenForm = ({ initialForm, onFormChange }: UseTokenFormProps =
setForm,
setErrors,
};
-};
\ No newline at end of file
+};
diff --git a/packages/client/src/create/hooks/useTokenGeneration.ts b/packages/client/src/create/hooks/useTokenGeneration.ts
index e6751a6c3..e463eb84e 100644
--- a/packages/client/src/create/hooks/useTokenGeneration.ts
+++ b/packages/client/src/create/hooks/useTokenGeneration.ts
@@ -1,8 +1,12 @@
-import { useState, useCallback } from "react";
-import type { TokenMetadata, GenerateMetadataResponse, GenerateImageResponse } from "../types";
-import { ERROR_MESSAGES } from "../consts";
-import { env } from "@/utils/env";
import { getAuthToken } from "@/utils/auth";
+import { env } from "@/utils/env";
+import { useCallback, useState } from "react";
+import { ERROR_MESSAGES } from "../consts";
+import type {
+ GenerateImageResponse,
+ GenerateMetadataResponse,
+ TokenMetadata,
+} from "../types";
interface UseTokenGenerationProps {
onGenerationComplete?: (metadata: TokenMetadata) => void;
@@ -10,140 +14,149 @@ interface UseTokenGenerationProps {
onImageUrlUpdate?: (imageUrl: string, imageFile?: File) => void;
}
-export const useTokenGeneration = ({
- onGenerationComplete,
+export const useTokenGeneration = ({
+ onGenerationComplete,
onError,
- onImageUrlUpdate
+ onImageUrlUpdate,
}: UseTokenGenerationProps = {}) => {
const [isGenerating, setIsGenerating] = useState(false);
const [generationProgress, setGenerationProgress] = useState(0);
- const [generatedMetadata, setGeneratedMetadata] = useState(null);
+ const [generatedMetadata, setGeneratedMetadata] =
+ useState(null);
const [currentImageUrl, setCurrentImageUrl] = useState(null);
const [currentImageFile, setCurrentImageFile] = useState(null);
- const generateToken = useCallback(async (prompt: string) => {
- try {
- setIsGenerating(true);
- setGenerationProgress(0);
- setCurrentImageUrl(null);
- setCurrentImageFile(null);
-
- const authToken = getAuthToken();
- const headers: Record = {
- "Content-Type": "application/json",
- };
- if (authToken) {
- headers["Authorization"] = `Bearer ${authToken}`;
- }
-
- setGenerationProgress(25);
- const metadataResponse = await fetch(
- `${env.apiUrl}/api/generation/generate-metadata`,
- {
- method: "POST",
- headers,
- credentials: "include",
- body: JSON.stringify({
- prompt,
- fields: ["name", "symbol", "description", "prompt"],
- }),
+ const generateToken = useCallback(
+ async (prompt: string) => {
+ try {
+ setIsGenerating(true);
+ setGenerationProgress(0);
+ setCurrentImageUrl(null);
+ setCurrentImageFile(null);
+
+ const authToken = getAuthToken();
+ const headers: Record = {
+ "Content-Type": "application/json",
+ };
+ if (authToken) {
+ headers["Authorization"] = `Bearer ${authToken}`;
}
- );
- if (!metadataResponse.ok) {
- throw new Error("Failed to generate metadata from prompt");
- }
-
- const metadataData = (await metadataResponse.json()) as GenerateMetadataResponse;
- if (!metadataData.success || !metadataData.metadata) {
- throw new Error("Invalid response from the metadata generation API");
- }
+ setGenerationProgress(25);
+ const metadataResponse = await fetch(
+ `${env.apiUrl}/api/generation/generate-metadata`,
+ {
+ method: "POST",
+ headers,
+ credentials: "include",
+ body: JSON.stringify({
+ prompt,
+ fields: ["name", "symbol", "description", "prompt"],
+ }),
+ },
+ );
+
+ if (!metadataResponse.ok) {
+ throw new Error("Failed to generate metadata from prompt");
+ }
- setGenerationProgress(50);
- const imageResponse = await fetch(
- `${env.apiUrl}/api/generation/generate`,
- {
- method: "POST",
- headers,
- credentials: "include",
- body: JSON.stringify({
- prompt: metadataData.metadata.prompt,
- type: "image",
- }),
+ const metadataData =
+ (await metadataResponse.json()) as GenerateMetadataResponse;
+ if (!metadataData.success || !metadataData.metadata) {
+ throw new Error("Invalid response from the metadata generation API");
}
- );
- if (!imageResponse.ok) {
- const errorText = await imageResponse.text();
- console.error("Image generation API returned an error:", errorText);
- const backendError = JSON.parse(errorText).error;
+ setGenerationProgress(50);
+ const imageResponse = await fetch(
+ `${env.apiUrl}/api/generation/generate`,
+ {
+ method: "POST",
+ headers,
+ credentials: "include",
+ body: JSON.stringify({
+ prompt: metadataData.metadata.prompt,
+ type: "image",
+ }),
+ },
+ );
+
+ if (!imageResponse.ok) {
+ const errorText = await imageResponse.text();
+ console.error("Image generation API returned an error:", errorText);
+ const backendError = JSON.parse(errorText).error;
+
+ let userErrorMessage = "Failed to generate image for token.";
+ if (backendError.includes("NSFW")) {
+ userErrorMessage =
+ "Your input contains inappropriate content. Please modify and try again.";
+ }
+ throw new Error(userErrorMessage);
+ }
- let userErrorMessage = "Failed to generate image for token.";
- if (backendError.includes("NSFW")) {
- userErrorMessage = "Your input contains inappropriate content. Please modify and try again.";
+ const imageData = (await imageResponse.json()) as GenerateImageResponse;
+ if (!imageData.success || !imageData.mediaUrl) {
+ throw new Error("Image generation API returned invalid data");
}
- throw new Error(userErrorMessage);
- }
- const imageData = (await imageResponse.json()) as GenerateImageResponse;
- if (!imageData.success || !imageData.mediaUrl) {
- throw new Error("Image generation API returned invalid data");
- }
+ try {
+ const imageBlob = await fetch(imageData.mediaUrl).then((r) =>
+ r.blob(),
+ );
+ const imageFile = new File([imageBlob], "generated-image.png", {
+ type: "image/png",
+ });
+ setCurrentImageFile(imageFile);
+
+ setCurrentImageUrl(imageData.mediaUrl);
+ if (onImageUrlUpdate) {
+ onImageUrlUpdate(imageData.mediaUrl, imageFile);
+ }
+ } catch (error) {
+ console.error("Error creating image file:", error);
+ setCurrentImageUrl(imageData.mediaUrl);
+ if (onImageUrlUpdate) {
+ onImageUrlUpdate(imageData.mediaUrl);
+ }
+ }
- try {
- const imageBlob = await fetch(imageData.mediaUrl).then(r => r.blob());
- const imageFile = new File([imageBlob], "generated-image.png", {
- type: "image/png",
- });
- setCurrentImageFile(imageFile);
-
- setCurrentImageUrl(imageData.mediaUrl);
- if (onImageUrlUpdate) {
- onImageUrlUpdate(imageData.mediaUrl, imageFile);
+ setGenerationProgress(75);
+ const finalMetadata: TokenMetadata = {
+ name: metadataData.metadata.name,
+ symbol: metadataData.metadata.symbol,
+ description: metadataData.metadata.description,
+ initialSol: 0,
+ links: {
+ twitter: "",
+ telegram: "",
+ farcaster: "",
+ website: "",
+ discord: "",
+ },
+ imageBase64: null,
+ tokenMint: "",
+ decimals: 9,
+ supply: 1000000000000000,
+ freezeAuthority: "",
+ mintAuthority: "",
+ };
+
+ setGenerationProgress(100);
+ setGeneratedMetadata(finalMetadata);
+ if (onGenerationComplete) {
+ onGenerationComplete(finalMetadata);
}
} catch (error) {
- console.error("Error creating image file:", error);
- setCurrentImageUrl(imageData.mediaUrl);
- if (onImageUrlUpdate) {
- onImageUrlUpdate(imageData.mediaUrl);
+ const errorMessage =
+ error instanceof Error ? error.message : ERROR_MESSAGES.UNKNOWN_ERROR;
+ if (onError) {
+ onError(errorMessage);
}
+ } finally {
+ setIsGenerating(false);
}
-
- setGenerationProgress(75);
- const finalMetadata: TokenMetadata = {
- name: metadataData.metadata.name,
- symbol: metadataData.metadata.symbol,
- description: metadataData.metadata.description,
- initialSol: 0,
- links: {
- twitter: "",
- telegram: "",
- farcaster: "",
- website: "",
- discord: "",
- },
- imageBase64: null,
- tokenMint: "",
- decimals: 9,
- supply: 1000000000000000,
- freezeAuthority: "",
- mintAuthority: "",
- };
-
- setGenerationProgress(100);
- setGeneratedMetadata(finalMetadata);
- if (onGenerationComplete) {
- onGenerationComplete(finalMetadata);
- }
- } catch (error) {
- const errorMessage = error instanceof Error ? error.message : ERROR_MESSAGES.UNKNOWN_ERROR;
- if (onError) {
- onError(errorMessage);
- }
- } finally {
- setIsGenerating(false);
- }
- }, [onGenerationComplete, onError, onImageUrlUpdate]);
+ },
+ [onGenerationComplete, onError, onImageUrlUpdate],
+ );
const resetGeneration = useCallback(() => {
setGeneratedMetadata(null);
@@ -161,4 +174,4 @@ export const useTokenGeneration = ({
generateToken,
resetGeneration,
};
-};
\ No newline at end of file
+};
diff --git a/packages/client/src/create/hooks/useVanityAddress.ts b/packages/client/src/create/hooks/useVanityAddress.ts
index cba4dd22e..415b0d895 100644
--- a/packages/client/src/create/hooks/useVanityAddress.ts
+++ b/packages/client/src/create/hooks/useVanityAddress.ts
@@ -1,8 +1,8 @@
-import { useCallback, useEffect, useRef, useState } from "react";
+import InlineVanityWorker from "@/workers/vanityWorker?worker&inline";
import { Keypair } from "@solana/web3.js";
+import { useCallback, useEffect, useRef, useState } from "react";
import { toast } from "react-toastify";
import { BASE58_REGEX } from "../consts";
-import InlineVanityWorker from "@/workers/vanityWorker?worker&inline";
import { VanityResult, WorkerMessage } from "../types";
export const useVanityAddress = () => {
@@ -10,10 +10,10 @@ export const useVanityAddress = () => {
const [isGeneratingVanity, setIsGeneratingVanity] = useState(false);
const [vanityResult, setVanityResult] = useState(null);
const [displayedPublicKey, setDisplayedPublicKey] = useState(
- "--- Generate a vanity address ---"
+ "--- Generate a vanity address ---",
);
const [suffixError, setSuffixError] = useState(null);
-
+
const workersRef = useRef([]);
const startTimeRef = useRef(null);
const displayUpdateIntervalRef = useRef(null);
@@ -74,22 +74,32 @@ export const useVanityAddress = () => {
}
setSuffixError(currentError);
- if (currentError && !currentError.startsWith("Warning") && !currentError.startsWith("Note")) {
+ if (
+ currentError &&
+ !currentError.startsWith("Warning") &&
+ !currentError.startsWith("Note")
+ ) {
return;
}
stopVanityGeneration();
setIsGeneratingVanity(true);
- const numWorkers = navigator.hardwareConcurrency > 12 ? 8 : navigator.hardwareConcurrency || 4;
+ const numWorkers =
+ navigator.hardwareConcurrency > 12
+ ? 8
+ : navigator.hardwareConcurrency || 4;
startTimeRef.current = Date.now();
workersRef.current = [];
- const base58Chars = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
+ const base58Chars =
+ "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
const generateRandomString = (length: number) => {
let result = "";
for (let i = 0; i < length; i++) {
- result += base58Chars.charAt(Math.floor(Math.random() * base58Chars.length));
+ result += base58Chars.charAt(
+ Math.floor(Math.random() * base58Chars.length),
+ );
}
return result;
};
@@ -126,4 +136,4 @@ export const useVanityAddress = () => {
startVanityGeneration,
stopVanityGeneration,
};
-};
\ No newline at end of file
+};
diff --git a/packages/client/src/hooks/use-create-token.ts b/packages/client/src/hooks/use-create-token.ts
index a5d89b3b0..f6f639a1a 100644
--- a/packages/client/src/hooks/use-create-token.ts
+++ b/packages/client/src/hooks/use-create-token.ts
@@ -129,10 +129,10 @@ const useCreateTokenMutation = () => {
"confirmed",
);
- return {
- mintPublicKey: mintKeypair.publicKey,
+ return {
+ mintPublicKey: mintKeypair.publicKey,
userPublicKey,
- signature: txId
+ signature: txId,
};
},
});