├── .eslintrc.json ├── .gitignore ├── README.md ├── components.json ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public ├── create.png └── dashboard.png ├── src ├── app │ ├── api │ │ ├── auth │ │ │ ├── change-password │ │ │ │ └── route.ts │ │ │ ├── login │ │ │ │ └── route.ts │ │ │ ├── register │ │ │ │ └── route.ts │ │ │ └── validate-token │ │ │ │ └── route.ts │ │ ├── file │ │ │ └── route.ts │ │ ├── tokens │ │ │ ├── my-tokens │ │ │ │ ├── route.ts │ │ │ │ └── total-number │ │ │ │ │ └── route.ts │ │ │ ├── route.ts │ │ │ └── total-number │ │ │ │ └── route.ts │ │ └── users │ │ │ └── route.ts │ ├── auth │ │ ├── register │ │ │ └── page.tsx │ │ └── signin │ │ │ └── page.tsx │ ├── change │ │ └── page.tsx │ ├── create │ │ └── page.tsx │ ├── favicon.ico │ ├── fonts │ │ ├── GeistMonoVF.woff │ │ └── GeistVF.woff │ ├── globals.css │ ├── layout.tsx │ ├── page.tsx │ ├── profile │ │ └── page.tsx │ ├── tokens │ │ └── page.tsx │ └── users │ │ └── page.tsx ├── assets │ └── image │ │ └── logo-sixcool.svg ├── components │ ├── navbar │ │ └── navbar.tsx │ ├── table │ │ └── Table.tsx │ └── ui │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── checkbox.tsx │ │ ├── dialog.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── menubar.tsx │ │ ├── pagination.tsx │ │ ├── select.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ └── textarea.tsx ├── context │ └── global-context.tsx ├── lib │ └── utils.ts └── utils │ ├── api.ts │ ├── jwt.ts │ ├── supabase │ ├── middleware.ts │ └── server.ts │ └── web3.ts ├── tailwind.config.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Solana Token Mint Application 2 | 3 | ## Deployment 4 | 5 | This platform is live on [https://solana-tokenmint-service.vercel.app/](https://solana-tokenmint-service.vercel.app/). 6 | 7 | ![](public/dashboard.png) 8 | ![](public/create.png) 9 | 10 | ## Features 11 | 12 | * Create and mint tokens: Users can create new token accounts and mint tokens there. Token minting is performed at once, it means that users can create token accounts, mint tokens, revoke authorities with one confirmation on his wallet. This is tested with `Phantom` wallet. 13 | 14 | * Metadata: Every tokens will have their meta data with external links like telegram, discord, twitter and custom website. 15 | 16 | * Environment: Users can switch between `mainnet-beta` and `devnet` environment 17 | 18 | * List Tokens: All token infos which users minted are stored on supabase. Users can see the token list on dashboard. 19 | 20 | * Simple authentication system with email and password. 21 | 22 | ## Tech Stack 23 | 24 | * Next.js v14, App Routing, Next API 25 | * Supabase 26 | * `solana/web3.js`, `spl-token` 27 | * Shadcn UI, Tailwind CSS 28 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | eslint: { 4 | ignoreDuringBuilds: true 5 | }, 6 | reactStrictMode: false 7 | }; 8 | 9 | export default nextConfig; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sol-token-mint", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@radix-ui/react-dialog": "^1.1.6", 13 | "@radix-ui/react-label": "^2.1.2", 14 | "@radix-ui/react-menubar": "^1.1.6", 15 | "@radix-ui/react-select": "^2.1.6", 16 | "@radix-ui/react-slot": "^1.1.2", 17 | "@radix-ui/react-switch": "^1.1.3", 18 | "@solana/spl-token": "^0.4.12", 19 | "@solana/spl-token-metadata": "^0.1.6", 20 | "@solana/web3.js": "^1.98.0", 21 | "@supabase/ssr": "^0.5.2", 22 | "@supabase/supabase-js": "^2.48.1", 23 | "@types/jsonwebtoken": "^9.0.8", 24 | "axios": "^1.7.9", 25 | "bcryptjs": "^3.0.0", 26 | "bs58": "^6.0.0", 27 | "class-variance-authority": "^0.7.1", 28 | "clsx": "^2.1.1", 29 | "jsonwebtoken": "^9.0.2", 30 | "lucide-react": "^0.475.0", 31 | "next": "14.2.24", 32 | "react": "^18", 33 | "react-advanced-cropper": "^0.20.0", 34 | "react-dom": "^18", 35 | "react-icons": "^5.5.0", 36 | "react-toastify": "^11.0.3", 37 | "shadcn": "^2.4.0-canary.11", 38 | "sharp": "^0.33.5", 39 | "tailwind-merge": "^3.0.1", 40 | "tailwindcss-animate": "^1.0.7", 41 | "uuid": "^11.1.0" 42 | }, 43 | "devDependencies": { 44 | "@types/node": "^20", 45 | "@types/react": "^18", 46 | "@types/react-dom": "^18", 47 | "@types/uuid": "^10.0.0", 48 | "eslint": "^8", 49 | "eslint-config-next": "14.2.24", 50 | "postcss": "^8", 51 | "tailwindcss": "^3.4.1", 52 | "typescript": "^5" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /public/create.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sm4rtdev/solana_tokenmint_service/d4e87e1fe04fbb546c593bd6a5df030514c52d46/public/create.png -------------------------------------------------------------------------------- /public/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sm4rtdev/solana_tokenmint_service/d4e87e1fe04fbb546c593bd6a5df030514c52d46/public/dashboard.png -------------------------------------------------------------------------------- /src/app/api/auth/change-password/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { createClient } from "@/utils/supabase/server"; 3 | import { hashSync, compareSync } from "bcryptjs"; 4 | import { generate_token, validate_token } from "@/utils/jwt"; 5 | 6 | export async function POST(req: NextRequest) { 7 | try { 8 | const token = req.headers.get('Authorization'); 9 | if (!token || !token.startsWith("Bearer ")) { 10 | return new NextResponse(JSON.stringify({ 11 | message: "Wrong Credentials", 12 | ok: false 13 | })) 14 | } 15 | const { email, name, avatar } = validate_token(token.substring(7)); 16 | const { password, oldPassword } = await req.json(); 17 | const supabase = await createClient(); 18 | const { data: user } = await supabase.from("users").select().eq("email", email).single(); 19 | const check = compareSync(oldPassword, user?.password) 20 | if (check) { 21 | const hash = hashSync(password); 22 | const { error } = await supabase.from("users").update({ password: hash }).eq("email", email); 23 | return new NextResponse(JSON.stringify({ 24 | message: error?.message || "update success", 25 | token: !error ? generate_token(email!, name!, avatar!) : undefined, 26 | ok: !error 27 | })) 28 | } else { 29 | return new NextResponse(JSON.stringify({ 30 | message: "Wrong Credentials", 31 | ok: false 32 | }), {status: 403}) 33 | } 34 | } 35 | catch (error) { 36 | return new NextResponse(JSON.stringify({ 37 | message: error, 38 | ok: false 39 | })) 40 | } 41 | } -------------------------------------------------------------------------------- /src/app/api/auth/login/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { createClient } from "@/utils/supabase/server"; 3 | import { compareSync } from "bcryptjs"; 4 | import { generate_token } from "@/utils/jwt"; 5 | 6 | export async function POST(req: NextRequest) { 7 | try { 8 | const supabase = await createClient(); 9 | const { email, password } = await req.json(); 10 | const { data: user } = await supabase.from("users").select().eq("email", email).single(); 11 | if (!user) { 12 | return new NextResponse(JSON.stringify({ 13 | message: "user not found", 14 | ok: false 15 | })); 16 | } 17 | const check = compareSync(password, user?.password); 18 | if (check) { 19 | const token = generate_token(email, user.name, user.avatar); 20 | return new NextResponse(JSON.stringify({ 21 | message: "login success", 22 | token: token, 23 | name: user.name, 24 | avatar: user.avatar, 25 | ok: true 26 | })); 27 | } else { 28 | return new NextResponse(JSON.stringify({ 29 | message: "Wrong Credentials", 30 | ok: false 31 | })) 32 | } 33 | } 34 | catch (error) { 35 | return new NextResponse(JSON.stringify({ 36 | message: error, 37 | ok: false 38 | })) 39 | } 40 | } -------------------------------------------------------------------------------- /src/app/api/auth/register/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { createClient } from "@/utils/supabase/server"; 3 | import { hashSync } from "bcryptjs"; 4 | 5 | export async function POST(req: NextRequest) { 6 | try { 7 | const { name, email, password } = await req.json(); 8 | const supabase = await createClient(); 9 | const { data: user } = await supabase.from("users").select().eq("email", email).single(); 10 | if (user) { 11 | return new NextResponse(JSON.stringify({ 12 | message: "Email already exists", 13 | ok: false 14 | })); 15 | } 16 | else { 17 | const hash = hashSync(password); 18 | const { error } = await supabase.from("users").insert({ name, email, password: hash }); 19 | return new NextResponse(JSON.stringify({ 20 | message: error?.message || "register success", 21 | ok: !error 22 | })) 23 | } 24 | } 25 | catch (error) { 26 | return new NextResponse(JSON.stringify({ 27 | message: error, 28 | ok: false 29 | })); 30 | } 31 | } -------------------------------------------------------------------------------- /src/app/api/auth/validate-token/route.ts: -------------------------------------------------------------------------------- 1 | import { generate_token, validate_token } from "@/utils/jwt"; 2 | import { NextRequest, NextResponse } from "next/server"; 3 | 4 | export async function POST(req: NextRequest) { 5 | try { 6 | const token = req.headers.get('Authorization'); 7 | if (!token || !token.startsWith("Bearer ")) { 8 | return new NextResponse(JSON.stringify({ 9 | message: "Wrong Credentials", 10 | ok: false 11 | })) 12 | } 13 | else { 14 | const payload = validate_token(token.substring(7)); 15 | const newToken = generate_token(payload.email!, payload.name!, payload.avatar!); 16 | return new NextResponse(JSON.stringify({ ok: payload.ok, message: payload.message, avatar: payload.avatar, name: payload.name, email: payload.email, token: newToken })) 17 | } 18 | } 19 | catch (error) { 20 | return new NextResponse(JSON.stringify({ 21 | message: error, 22 | ok: false 23 | })) 24 | } 25 | } -------------------------------------------------------------------------------- /src/app/api/file/route.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "@/utils/supabase/server"; 2 | import { v4 as uuid } from "uuid"; 3 | 4 | 5 | export async function POST( 6 | req: Request 7 | ) { 8 | const formdata = await req.formData(); 9 | const folder = formdata.get("path") || "avatar"; 10 | const filename = formdata.get("filename") || "image.png"; 11 | const type = formdata.get("type") || "image/png"; 12 | const file = formdata.get('file')!; 13 | 14 | const supabase = await createClient(); 15 | const {data: onlineFile, error} = await supabase.storage 16 | .from("sol-token-mint") 17 | .upload(`${folder}/${uuid()}-${filename}`, file, { 18 | contentType: type as string, 19 | headers: { 20 | "Content-Disposition": "inline" 21 | } 22 | }) 23 | const { data: url } = supabase.storage.from("sol-token-mint").getPublicUrl(`${onlineFile?.path}`); 24 | return new Response(JSON.stringify({ 25 | url: url?.publicUrl 26 | })) 27 | } 28 | 29 | export async function DELETE( 30 | req: Request 31 | ) { 32 | const searchParams = new URL(req.url).searchParams; 33 | const publicUrl = searchParams.get("publicUrl"); 34 | if (!publicUrl) return new Response(JSON.stringify({ 35 | ok: false, 36 | message: "empty url" 37 | })); 38 | const chunks = publicUrl.split("/"); 39 | const path = chunks[chunks.length - 2] + "/" + chunks[chunks.length - 1]; 40 | const supabase = await createClient(); 41 | const {error} = await supabase.storage.from("sol-token-mint").remove([path]); 42 | return new Response(JSON.stringify({ 43 | ok: !error, 44 | message: error?.message 45 | })) 46 | } -------------------------------------------------------------------------------- /src/app/api/tokens/my-tokens/route.ts: -------------------------------------------------------------------------------- 1 | import { validate_token } from "@/utils/jwt"; 2 | import { createClient } from "@/utils/supabase/server"; 3 | 4 | export async function GET( 5 | req: Request 6 | ) { 7 | const url = new URL(req.url); 8 | const page = parseInt(url.searchParams.get("page") || "1"); 9 | const size = parseInt(url.searchParams.get("size") || "10"); 10 | const net = url.searchParams.get("devnet"); 11 | const token = req.headers.get("Authorization"); 12 | if (!token) { 13 | return new Response(JSON.stringify({ 14 | message: "Authentication error", 15 | ok: false 16 | }) , {status: 403}) 17 | } 18 | const { email:user } = validate_token(token.substring(7)); 19 | if (user) { 20 | const supabase = await createClient(); 21 | const tokens = (await supabase.from("tokens" + (net !== null ? "_devnet" : "")).select("name, created_at, url, address, symbol, description, decimals, supply").eq("user", user).order("created_at", { ascending: false}).range(page * size - size, page * size - 1)).data; 22 | return new Response(JSON.stringify(tokens)) 23 | } else { 24 | return new Response(JSON.stringify({ 25 | message: "Authentication error", 26 | ok: false 27 | }) , {status: 403}) 28 | } 29 | } -------------------------------------------------------------------------------- /src/app/api/tokens/my-tokens/total-number/route.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "@/utils/supabase/server"; 2 | import { validate_token } from "@/utils/jwt"; 3 | 4 | export async function GET(req: Request) { 5 | const url = new URL(req.url); 6 | const net = url.searchParams.get("devnet"); 7 | const token = req.headers.get("Authorization"); 8 | if (!token) { 9 | return new Response(JSON.stringify({ 10 | message: "Authentication error", 11 | ok: false 12 | }), { status: 403 }) 13 | } 14 | const { email: user } = validate_token(token.substring(7)); 15 | if (user) { 16 | const supabase = await createClient(); 17 | const { count: totalCount } = (await supabase.from("tokens" + (net !== null ? "_devnet" : "")).select('*', { count: 'exact'}).eq("user", user)); 18 | return new Response(JSON.stringify(totalCount)) 19 | } else { 20 | return new Response(JSON.stringify({ 21 | message: "Authentication error", 22 | ok: false 23 | }), { status: 403 }) 24 | } 25 | } -------------------------------------------------------------------------------- /src/app/api/tokens/route.ts: -------------------------------------------------------------------------------- 1 | import { validate_token } from "@/utils/jwt"; 2 | import { createClient } from "@/utils/supabase/server"; 3 | 4 | 5 | export async function GET( 6 | req: Request 7 | ) { 8 | const url = new URL(req.url); 9 | const page = parseInt(url.searchParams.get("page") || "1"); 10 | const size = parseInt(url.searchParams.get("size") || "10"); 11 | const user = url.searchParams.get("user"); 12 | const net = url.searchParams.get("devnet"); 13 | 14 | let tokens: any[] | null = [] 15 | const supabase = await createClient(); 16 | if (user) { 17 | tokens = (await supabase.from("tokens" + (net !== null ? "_devnet" : "")).select("name, created_at, url, address, symbol, description, decimals, supply").eq("user", user).order("created_at", { ascending: false}).range(page * size - size, page * size - 1)).data; 18 | } else { 19 | tokens = (await supabase.from("tokens" + (net !== null ? "_devnet" : "")).select("name, created_at, url, address, symbol, description, decimals, supply").order("created_at", { ascending: false}).range(page * size - size, page * size - 1)).data; 20 | } 21 | return new Response(JSON.stringify(tokens)) 22 | } 23 | 24 | export async function POST( 25 | req: Request 26 | ) { 27 | const access_token = req.headers.get("Authorization"); 28 | if (!access_token) { 29 | return new Response(JSON.stringify({ 30 | message: "Authentication error", 31 | ok: false 32 | }) , {status: 403}) 33 | } 34 | const url = new URL(req.url); 35 | const net = url.searchParams.get("devnet"); 36 | const { email:user } = validate_token(access_token.substring(7)); 37 | const token = await req.json(); 38 | const supabase = await createClient(); 39 | const {error} = await supabase.from("tokens" + (net !== null ? "_devnet" : "")).insert({ 40 | ...token, 41 | user 42 | }) 43 | return new Response(JSON.stringify({ 44 | ok: !error, 45 | message: error?.message 46 | })) 47 | } -------------------------------------------------------------------------------- /src/app/api/tokens/total-number/route.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | import { createClient } from "@/utils/supabase/server"; 4 | 5 | export async function GET(req: Request) { 6 | const supabase = await createClient(); 7 | const url = new URL(req.url); 8 | const net = url.searchParams.get("devnet"); 9 | const { count: totalCount, error } = (await supabase.from("tokens" + (net !== null ? "_devnet" : "")).select('*', { count: 'exact'})) 10 | return new Response(JSON.stringify(totalCount), { 11 | status: error ? 500 : 200, 12 | headers: { 13 | 'Content-Type': 'application/json', 14 | }, 15 | }); 16 | 17 | } -------------------------------------------------------------------------------- /src/app/api/users/route.ts: -------------------------------------------------------------------------------- 1 | import { generate_token, validate_token } from "@/utils/jwt"; 2 | import { createClient } from "@/utils/supabase/server"; 3 | 4 | export async function GET( 5 | req: Request 6 | ) { 7 | const url = new URL(req.url); 8 | const page = parseInt(url.searchParams.get("page") || "1"); 9 | const size = parseInt(url.searchParams.get("size") || "10"); 10 | const supabase = await createClient(); 11 | const email = url.searchParams.get("email"); 12 | if (email) { 13 | const { data: users } = await supabase.from("users").select().eq(`email`, email); 14 | return new Response(JSON.stringify(users![0])) 15 | } else { 16 | const { data: users } = await supabase.from("users").select().order("created_at", { ascending: false }).range(page * size - size, page * size - 1); 17 | return new Response(JSON.stringify(users)) 18 | } 19 | } 20 | 21 | // export async function POST( 22 | // req: Request 23 | // ) { 24 | 25 | // } 26 | 27 | export async function PUT( 28 | req: Request 29 | ) { 30 | const token = req.headers.get('Authorization'); 31 | if (!token || !token.startsWith("Bearer ")) { 32 | return new Response(JSON.stringify({ 33 | message: "Wrong Credentials", 34 | ok: false 35 | }), {status: 403}) 36 | } 37 | const { email } = validate_token(token.substring(7)); 38 | const { name, avatar } = await req.json(); 39 | const supabase = await createClient(); 40 | const { error } = await supabase.from("users").update({ name, avatar }).eq(`email`, email); 41 | return new Response(JSON.stringify({ 42 | message: error?.message || "Updated", 43 | ok: !error, 44 | token: generate_token(email!, name, avatar) 45 | })) 46 | } 47 | 48 | export async function DELETE( 49 | req: Request 50 | ) { 51 | const { id, email } = await req.json(); 52 | const supabase = await createClient(); 53 | const { error } = await supabase.from("users").delete().or(`id.eq.${id},email.eq.${email}`) 54 | return new Response(error?.message || "Deleted") 55 | } -------------------------------------------------------------------------------- /src/app/auth/register/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import type React from "react" 4 | 5 | import { useState } from "react" 6 | import Link from "next/link" 7 | import { useRouter } from "next/navigation" 8 | import { Eye, EyeOff, Loader2 } from "lucide-react" 9 | import { toast } from "react-toastify"; 10 | import { Button } from "@/components/ui/button" 11 | import { Input } from "@/components/ui/input" 12 | import { Label } from "@/components/ui/label" 13 | import { Checkbox } from "@/components/ui/checkbox" 14 | import { Card, CardContent, CardFooter } from "@/components/ui/card" 15 | import { register } from "@/utils/api" 16 | const Register = () => { 17 | const router = useRouter() 18 | const [isLoading, setIsLoading] = useState(false) 19 | const [showPassword, setShowPassword] = useState(false) 20 | const [formData, setFormData] = useState({ 21 | name: "", 22 | email: "", 23 | password: "", 24 | agreeToTerms: false, 25 | }) 26 | const [errors, setErrors] = useState({ 27 | name: "", 28 | email: "", 29 | password: "", 30 | avatar: "", 31 | agreeToTerms: "", 32 | }) 33 | 34 | const validateForm = () => { 35 | const newErrors = { 36 | name: "", 37 | email: "", 38 | password: "", 39 | avatar: "", 40 | agreeToTerms: "", 41 | } 42 | let isValid = true 43 | 44 | if (!formData.name.trim()) { 45 | newErrors.name = "Name is required" 46 | isValid = false 47 | } 48 | 49 | if (!formData.email) { 50 | newErrors.email = "Email is required" 51 | isValid = false 52 | } else if (!/\S+@\S+\.\S+/.test(formData.email)) { 53 | newErrors.email = "Email is invalid" 54 | isValid = false 55 | } 56 | 57 | if (!formData.password) { 58 | newErrors.password = "Password is required" 59 | isValid = false 60 | } else if (formData.password.length < 8) { 61 | newErrors.password = "Password must be at least 8 characters" 62 | isValid = false 63 | } 64 | 65 | if (!formData.agreeToTerms) { 66 | newErrors.agreeToTerms = "You must agree to the terms and conditions" 67 | isValid = false 68 | } 69 | 70 | setErrors(newErrors) 71 | return isValid 72 | } 73 | 74 | const handleChange = (e: React.ChangeEvent) => { 75 | const { name, value } = e.target 76 | setFormData((prev) => ({ 77 | ...prev, 78 | [name]: value, 79 | })) 80 | } 81 | 82 | const handleCheckboxChange = (checked: boolean) => { 83 | setFormData((prev) => ({ 84 | ...prev, 85 | agreeToTerms: checked, 86 | })) 87 | } 88 | 89 | const handleSubmit = async (e: React.FormEvent) => { 90 | e.preventDefault() 91 | 92 | if (!validateForm()) return 93 | 94 | setIsLoading(true) 95 | 96 | // Simulate API call 97 | try { 98 | const res = await register(formData.name, formData.email, formData.password); 99 | 100 | if (res) { 101 | toast.success("You have successfully created an account.") 102 | } else { 103 | toast.warn("You already have an account.") 104 | } 105 | 106 | router.push("/auth/signin") 107 | } catch (error) { 108 | toast.error("Failed to create account. Please try again.") 109 | } finally { 110 | setIsLoading(false) 111 | } 112 | } 113 | 114 | return ( 115 |
116 | 117 |
118 | 119 |
120 | 121 | 131 | {errors.name &&

{errors.name}

} 132 |
133 | 134 |
135 | 136 | 147 | {errors.email &&

{errors.email}

} 148 |
149 | 150 |
151 | 152 |
153 | 164 | 179 |
180 | {errors.password &&

{errors.password}

} 181 |
182 | 183 |
184 |
185 | handleCheckboxChange(checked as boolean)} 189 | /> 190 | 193 |
194 | {errors.agreeToTerms &&

{errors.agreeToTerms}

} 195 |
196 |
197 | 198 | 199 | 209 | 210 |
211 | Already have an account?{" "} 212 | 213 | Sign in 214 | 215 |
216 |
217 |
218 |
219 |
220 | ) 221 | } 222 | export default Register; 223 | -------------------------------------------------------------------------------- /src/app/auth/signin/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import type React from "react" 4 | 5 | import { useEffect, useState } from "react" 6 | import Link from "next/link" 7 | import { useRouter } from "next/navigation" 8 | import { Eye, EyeOff, Loader2 } from "lucide-react" 9 | import { toast } from "react-toastify" 10 | import { Button } from "@/components/ui/button" 11 | import { Input } from "@/components/ui/input" 12 | import { Label } from "@/components/ui/label" 13 | import { Checkbox } from "@/components/ui/checkbox" 14 | import { Card, CardContent, CardFooter } from "@/components/ui/card" 15 | import { useGlobalContext } from "@/context/global-context" 16 | import { login } from "@/utils/api" 17 | 18 | 19 | const SignIn = ({ searchParams }: { searchParams: { [key: string]: string } }) => { 20 | const redirect = searchParams.redirect || '/tokens'; 21 | const router = useRouter() 22 | const { user, setUser } = useGlobalContext(); 23 | const [isLoading, setIsLoading] = useState(false) 24 | const [showPassword, setShowPassword] = useState(false) 25 | const [formData, setFormData] = useState({ 26 | email: "", 27 | password: "", 28 | rememberMe: false, 29 | }) 30 | const [errors, setErrors] = useState({ 31 | email: "", 32 | password: "", 33 | }) 34 | 35 | const validateForm = () => { 36 | const newErrors = { 37 | email: "", 38 | password: "", 39 | } 40 | let isValid = true 41 | 42 | if (!formData.email) { 43 | newErrors.email = "Email is required" 44 | isValid = false 45 | } else if (!/\S+@\S+\.\S+/.test(formData.email)) { 46 | newErrors.email = "Email is invalid" 47 | isValid = false 48 | } 49 | 50 | if (!formData.password) { 51 | newErrors.password = "Password is required" 52 | isValid = false 53 | } 54 | 55 | setErrors(newErrors) 56 | return isValid 57 | } 58 | 59 | const handleChange = (e: React.ChangeEvent) => { 60 | const { name, value } = e.target 61 | setFormData((prev) => ({ 62 | ...prev, 63 | [name]: value, 64 | })) 65 | } 66 | 67 | const handleCheckboxChange = (checked: boolean) => { 68 | setFormData((prev) => ({ 69 | ...prev, 70 | rememberMe: checked, 71 | })) 72 | } 73 | 74 | const handleSubmit = async (e: React.FormEvent) => { 75 | e.preventDefault() 76 | 77 | if (!validateForm()) return 78 | 79 | setIsLoading(true) 80 | 81 | // Simulate API call 82 | try { 83 | const data = await login(formData.email, formData.password); 84 | 85 | if (data.ok) { 86 | localStorage.setItem('token', data.token); 87 | setUser({ 88 | ...data, 89 | email: formData.email 90 | }); 91 | toast.success(data.message); 92 | router.push(redirect, { scroll: true }); 93 | } 94 | else { toast.warn(data.message) } 95 | } catch (error) { 96 | toast.error("Failed to sign in. Please try again.") 97 | } finally { 98 | setIsLoading(false) 99 | } 100 | } 101 | 102 | useEffect(() => { 103 | if (user) { 104 | router.push(redirect, { scroll: true }); 105 | } 106 | }, [user]) 107 | 108 | return ( 109 |
110 | 111 |
112 | 113 |
114 | 115 | 126 | {errors.email &&

{errors.email}

} 127 |
128 | 129 |
130 |
131 | 132 |
133 |
134 | 145 | 160 |
161 | {errors.password &&

{errors.password}

} 162 |
163 | 164 |
165 | 166 | 167 | 177 | 178 |
179 | Don't have an account?{" "} 180 | 181 | Sign up 182 | 183 |
184 |
185 |
186 |
187 |
188 | ) 189 | } 190 | export default SignIn; 191 | 192 | -------------------------------------------------------------------------------- /src/app/change/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import type React from "react" 4 | import { useState } from "react" 5 | import { Eye, EyeOff, Loader2 } from "lucide-react" 6 | import { toast } from "react-toastify" 7 | import { Button } from "@/components/ui/button" 8 | import { Input } from "@/components/ui/input" 9 | import { Label } from "@/components/ui/label" 10 | import { Card, CardContent, CardFooter } from "@/components/ui/card" 11 | import { changePassword } from "@/utils/api" 12 | 13 | const ChangePassword = () => { 14 | const [isLoading, setIsLoading] = useState(false) 15 | const [showPassword, setShowPassword] = useState(false) 16 | const [showConfirmPassword, setConfirmShowPassword] = useState(false) 17 | const [showOldPassword, setShowOldPassword] = useState(false) 18 | const [formData, setFormData] = useState({ 19 | password: "", 20 | confirmPassword: "", 21 | oldPassword: "", 22 | }) 23 | const [errors, setErrors] = useState({ 24 | password: "", 25 | confirmPassword: "", 26 | oldPassword: "", 27 | isvalid: "", 28 | }) 29 | 30 | const validateForm = () => { 31 | const newErrors = { 32 | password: "", 33 | confirmPassword: "", 34 | oldPassword: "", 35 | isvalid: "" 36 | } 37 | let isValid = true 38 | 39 | if (!formData.password) { 40 | newErrors.password = "Password is required" 41 | isValid = false 42 | } 43 | 44 | if (!formData.confirmPassword) { 45 | newErrors.confirmPassword = "Confirm Password is required" 46 | isValid = false 47 | } 48 | 49 | if (!formData.oldPassword) { 50 | newErrors.oldPassword = "Oldpassword is required" 51 | isValid = false 52 | } 53 | 54 | if (formData.password !== formData.confirmPassword && formData.password && formData.confirmPassword) { 55 | newErrors.isvalid = "Passwords do not match" 56 | toast.error(newErrors.isvalid); 57 | isValid = false 58 | } 59 | 60 | setErrors(newErrors) 61 | return isValid 62 | } 63 | 64 | const handleChange = (e: React.ChangeEvent) => { 65 | const { name, value } = e.target 66 | setFormData((prev) => ({ 67 | ...prev, 68 | [name]: value, 69 | })) 70 | } 71 | 72 | const handleSubmit = async (e: React.FormEvent) => { 73 | e.preventDefault() 74 | 75 | if (!validateForm()) return 76 | 77 | setIsLoading(true) 78 | 79 | // Simulate API call 80 | try { 81 | const res = await changePassword(formData.password, formData.oldPassword) 82 | if (res) { 83 | toast.success("Your password successfully updated."); 84 | } 85 | } catch (error) { 86 | toast.error("Failed to sign in. Please try again.") 87 | } finally { 88 | setIsLoading(false) 89 | } 90 | } 91 | 92 | return ( 93 |
94 | 95 |
96 | 97 |
98 |
99 | 100 |
101 |
102 | 113 | 128 |
129 | {errors.oldPassword &&

{errors.oldPassword}

} 130 |
131 | 132 |
133 |
134 | 135 |
136 |
137 | 148 | 163 |
164 | {errors.password &&

{errors.password}

} 165 |
166 | 167 |
168 |
169 | 170 |
171 |
172 | 183 | 198 |
199 | {errors.confirmPassword &&

{errors.confirmPassword}

} 200 |
201 |
202 | 203 | 204 | 214 | 215 |
216 |
217 |
218 | ) 219 | } 220 | export default ChangePassword; -------------------------------------------------------------------------------- /src/app/create/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { Button } from "@/components/ui/button" 3 | import { clusterApiUrl, Connection, Keypair, SystemProgram, TransactionMessage, VersionedTransaction } from "@solana/web3.js" 4 | import { AuthorityType, createAssociatedTokenAccountInstruction, createInitializeMetadataPointerInstruction, createInitializeMintInstruction, createMintToCheckedInstruction, createSetAuthorityInstruction, ExtensionType, getAssociatedTokenAddressSync, getMintLen, LENGTH_SIZE, TOKEN_2022_PROGRAM_ID, TYPE_SIZE } from "@solana/spl-token" 5 | import { useEffect, useRef, useState } from "react" 6 | import { toast } from "react-toastify"; 7 | import { Label } from "@/components/ui/label" 8 | import { Input } from "@/components/ui/input" 9 | import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" 10 | import { CropperRef, Cropper, } from 'react-advanced-cropper'; 11 | import 'react-advanced-cropper/dist/style.css' 12 | import { connectSolana, getSolanaProvider } from "@/utils/web3" 13 | import { removeFile, saveToken, uploadFile } from "@/utils/api" 14 | import { createInitializeInstruction, pack, TokenMetadata } from "@solana/spl-token-metadata" 15 | import { Loader2 } from "lucide-react" 16 | import { useGlobalContext } from "@/context/global-context" 17 | import { Textarea } from "@/components/ui/textarea" 18 | import { useRouter } from "next/navigation" 19 | 20 | 21 | export default function CreateToken() { 22 | const { user, net } = useGlobalContext(); 23 | const router = useRouter(); 24 | const avatarRef = useRef(null); 25 | const [imgFile, setImgFile] = useState(); 26 | const [fileInfo, setFileInfo] = useState<{ name: string, type: string }>(); 27 | const cropperRef = useRef(null); 28 | const [image, setImage] = useState(); 29 | const [preview, setPreview] = useState(); 30 | const [name, setName] = useState(); 31 | const [symbol, setSymbol] = useState(); 32 | const [description, setDescription] = useState(); 33 | const [decimal, setDecimal] = useState(9); 34 | const [supply, setSupply] = useState(1_000_000); 35 | const [open, setOpen] = useState(false); 36 | const [spinner, setSpinner] = useState(false); 37 | const [creator, setCreator] = useState({ 38 | name: "Sixcool", 39 | site: process.env.NEXT_PUBLIC_ORIGIN || "https://mint-solana-token.vercel.app" 40 | }) 41 | const [telegram, setTelegram] = useState(""); 42 | const [discord, setDiscord] = useState(""); 43 | const [website, setWebsite] = useState(""); 44 | const [twitter, setTwitter] = useState(""); 45 | 46 | const download = (mint: Keypair) => { 47 | const url = URL.createObjectURL(new Blob([`[${mint.secretKey.toString()}]`])) 48 | const a = document.createElement('a') 49 | a.href = url 50 | a.download = `${mint.publicKey.toBase58()}.json` 51 | document.body.appendChild(a) 52 | a.click() 53 | a.remove() 54 | } 55 | const onChooseImage = () => { 56 | if (avatarRef.current?.files![0]) { 57 | const file = avatarRef.current.files[0]; 58 | setOpen(true); 59 | setFileInfo(file) 60 | const url = URL.createObjectURL(file) 61 | setImage(url); 62 | } 63 | } 64 | const onCrop = () => { 65 | setOpen(false); 66 | if (cropperRef.current && avatarRef.current) { 67 | avatarRef.current.value = ''; 68 | const canvas = cropperRef.current?.getCanvas({ width: 128, height: 128 }); 69 | if (canvas) { 70 | canvas.toBlob(blob => { 71 | blob && setImgFile(new File([blob], fileInfo?.name!, { type: 'image/png' })); 72 | }, 'image/png'); 73 | setPreview(canvas.toDataURL()); 74 | } 75 | } 76 | } 77 | const onCancel = () => { 78 | setOpen(false); 79 | if (cropperRef.current && avatarRef.current) { 80 | setImgFile(avatarRef.current.files![0]); 81 | setPreview(image); 82 | avatarRef.current.value = ''; 83 | } 84 | } 85 | 86 | const createToken = async () => { 87 | if (!user) { 88 | router.push('/auth/signin?redirect=/create', {scroll: true}) 89 | } 90 | if (!name) { 91 | toast.warn("Input the token name!"); 92 | return; 93 | } 94 | if (!symbol) { 95 | toast.warn("Input the token symbol!"); 96 | return; 97 | } 98 | if (!description) { 99 | toast.warn("Input the token description!"); 100 | return; 101 | } 102 | if (!preview || !imgFile) { 103 | toast.warn("Choose the token avatar!"); 104 | return; 105 | } 106 | const payer = await connectSolana(); 107 | if (!payer) { 108 | toast.warn("Failed to connect your wallet!"); 109 | return; 110 | } 111 | setSpinner(true); 112 | const mint = Keypair.generate(); 113 | 114 | const image = await uploadFile(imgFile, fileInfo?.name!, fileInfo?.type, net === "devnet" ? "token-asset-devnet" : 'token-asset'); 115 | if (!image) return; 116 | const extensions : {[key: string]: string} | undefined = (!telegram && !discord && !website && !twitter) ? undefined : {}; 117 | if (extensions) { 118 | if (telegram) extensions.telegram = telegram; 119 | if (discord) extensions.discord = discord; 120 | if (twitter) extensions.twitter = twitter; 121 | if (website) extensions.website = website; 122 | } 123 | const str = JSON.stringify({ 124 | name, 125 | symbol, 126 | description, 127 | image, 128 | creator, 129 | extensions 130 | }, null, 4); 131 | const bytes = new TextEncoder().encode(str); 132 | const blob = new Blob([bytes], { 133 | type: "application/json;charset=utf-8" 134 | }); 135 | const metaFile = await uploadFile(new File([blob], 'metadata.json', { type: "application/json;charset=utf-8" }), 'metadata.json', "application/json;charset=utf-8", net === "devnet" ? "token-asset-devnet" : 'token-asset') 136 | if (!metaFile) return; 137 | const metadata: TokenMetadata = { 138 | mint: mint.publicKey, 139 | name, 140 | symbol, 141 | uri: metaFile, 142 | additionalMetadata: [["description", "Only Possible On Solana"]], 143 | }; 144 | const mintLen = getMintLen([ 145 | ExtensionType.MetadataPointer 146 | ]); 147 | const metadataLen = TYPE_SIZE + LENGTH_SIZE + pack(metadata).length; 148 | 149 | const mainnet_rpcs = [ 150 | "https://api.mainnet-beta.solana.com", 151 | "https://solana.drpc.org", 152 | "https://go.getblock.io/4136d34f90a6488b84214ae26f0ed5f4", 153 | "https://solana-rpc.publicnode.com", 154 | "https://api.blockeden.xyz/solana/67nCBdZQSH9z3YqDDjdm", 155 | "https://solana.leorpc.com/?api_key=FREE", 156 | "https://endpoints.omniatech.io/v1/sol/mainnet/public", 157 | "https://solana.api.onfinality.io/public" 158 | ] 159 | 160 | try { 161 | const connection = new Connection(net === "devnet" ? "https://api.devnet.solana.com" : "https://mainnet.helius-rpc.com/?api-key=488f3679-3ae7-4e8f-baa9-60dc45f0a75c", "confirmed"); 162 | 163 | const lamports = await connection.getMinimumBalanceForRentExemption(mintLen + metadataLen); 164 | console.log("calculate lamport", lamports) 165 | const programId = TOKEN_2022_PROGRAM_ID; 166 | 167 | const ata = getAssociatedTokenAddressSync(mint.publicKey, payer, false, programId); 168 | console.log("ata", ata) 169 | 170 | const instructions_create = [ 171 | SystemProgram.createAccount({ 172 | fromPubkey: payer, 173 | newAccountPubkey: mint.publicKey, 174 | space: mintLen, 175 | lamports, 176 | programId 177 | }), 178 | createInitializeMetadataPointerInstruction(mint.publicKey, payer, mint.publicKey, programId), 179 | createInitializeMintInstruction(mint.publicKey, decimal, payer, null, programId), 180 | createInitializeInstruction({ 181 | programId, 182 | mint: mint.publicKey, 183 | metadata: mint.publicKey, 184 | name, 185 | symbol, 186 | uri: metaFile, 187 | mintAuthority: payer, 188 | updateAuthority: payer 189 | }) 190 | ] 191 | const instructions_mint = [ 192 | createAssociatedTokenAccountInstruction( 193 | payer, 194 | ata, 195 | payer, 196 | mint.publicKey, 197 | programId 198 | ), 199 | createMintToCheckedInstruction( 200 | mint.publicKey, 201 | ata, 202 | payer, 203 | supply * 10 ** decimal, 204 | decimal, 205 | [], 206 | programId 207 | ), 208 | createSetAuthorityInstruction( 209 | mint.publicKey, 210 | payer, 211 | AuthorityType.MintTokens, 212 | null, 213 | [], 214 | programId 215 | ) 216 | ] 217 | console.log("create instructions") 218 | const recentBlockhash = (await connection.getLatestBlockhash()).blockhash; 219 | console.log("recent blockhash", recentBlockhash) 220 | const transactionV0_create = new VersionedTransaction( 221 | new TransactionMessage({ 222 | payerKey: payer, 223 | recentBlockhash, 224 | instructions: instructions_create, 225 | }).compileToV0Message() 226 | ); 227 | transactionV0_create.sign([mint]) 228 | const transactionV0_mint = new VersionedTransaction( 229 | new TransactionMessage({ 230 | payerKey: payer, 231 | recentBlockhash, 232 | instructions: instructions_mint 233 | }).compileToV0Message() 234 | ); 235 | const provider = getSolanaProvider(); 236 | const signedTransactions = await provider.signAllTransactions([transactionV0_create, transactionV0_mint]) 237 | console.log("singed transaction", signedTransactions) 238 | for (const signedTransaction of signedTransactions) { 239 | const signature = await connection.sendTransaction(signedTransaction); 240 | await connection.confirmTransaction(signature); 241 | console.log(`Transaction confirmed with signature: ${signature}`); 242 | } 243 | saveToken(mint.publicKey.toBase58(), name, symbol, description, image, supply, decimal, net === "devnet"); 244 | download(mint); 245 | toast.success( 246 |

247 | Token mint success! Please check your wallet or 248 | click here. 249 |

250 | ) 251 | router.push("/tokens", {scroll: true}) 252 | } catch (e) { 253 | console.log("Error:", e); 254 | removeFile(image); 255 | removeFile(metaFile); 256 | toast.error("Token mint failed! Try again later.") 257 | } 258 | setSpinner(false); 259 | } 260 | 261 | useEffect(() => { 262 | if (!user) { 263 | router.push('/auth/signin?redirect=/create', {scroll: true}) 264 | } 265 | }, []) 266 | 267 | return ( 268 |
269 |
270 |
271 |
272 | 273 | setName(e.target.value)} /> 274 |
275 |
276 | 277 | setSymbol(e.target.value)} /> 278 |
279 |
280 | 281 | setDecimal(parseInt(e.target.value))} /> 282 |
283 |
284 | 285 | setSupply(parseInt(e.target.value))} /> 286 |
287 |
288 |
289 | 290 | 291 |
292 | {preview && } 296 |
297 |
298 |
299 | 300 |