├── src ├── pages │ ├── api │ │ ├── proxy.ts │ │ ├── hello.ts │ │ ├── auth │ │ │ ├── nonce.ts │ │ │ ├── user.ts │ │ │ └── verify.ts │ │ └── upload-to-ipfs.ts │ ├── _document.tsx │ ├── _app.tsx │ ├── 404.tsx │ ├── FAQ.tsx │ ├── about.tsx │ ├── home.tsx │ ├── index.tsx │ ├── dashboard.tsx │ └── profile │ │ └── [address].tsx ├── types │ ├── siwe.d.ts │ ├── contracts │ │ ├── factories │ │ │ ├── index.ts │ │ │ └── BondingCurveManager__factory.ts │ │ ├── index.ts │ │ └── common.ts │ └── heroicons.d.ts ├── components │ ├── notifications │ │ ├── Modal.tsx │ │ ├── PurchaseConfirmationPopup.tsx │ │ ├── HowItWorksPopup.tsx │ │ └── LiveNotifications.tsx │ ├── ui │ │ ├── Spinner.tsx │ │ ├── switch.tsx │ │ ├── SortOptions.tsx │ │ ├── SearchFilter.tsx │ │ └── ShareButton.tsx │ ├── layout │ │ ├── Layout.tsx │ │ ├── Footer.tsx │ │ └── Navbar.tsx │ ├── seo │ │ ├── OGPreview.tsx │ │ └── SEO.tsx │ ├── providers │ │ └── WebSocketProvider.tsx │ ├── PriceLiquidity.tsx │ ├── auth │ │ └── SiweAuth.tsx │ ├── tokens │ │ ├── TokenList.tsx │ │ └── TokenCard.tsx │ ├── TokenDetails │ │ ├── TokenInfo.tsx │ │ ├── TokenHolders.tsx │ │ ├── TransactionHistory.tsx │ │ └── Chats.tsx │ └── charts │ │ └── TradingViewChart.tsx ├── utils │ ├── chatUtils.ts │ ├── api.ts │ └── blockchainUtils.ts ├── styles │ └── globals.css ├── interface │ └── types.ts └── abi │ ├── ERC20.json │ └── BondingCurveManager.json ├── .eslintrc.json ├── public ├── chats │ ├── 1.png │ ├── 10.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ ├── 8.png │ └── 9.png ├── favicon.ico ├── seo │ ├── dash.jpg │ ├── home.jpg │ └── create.jpg ├── logo │ ├── wbone.png │ ├── ethereum.png │ ├── icon.svg │ └── logo.svg ├── vercel.svg └── next.svg ├── postcss.config.mjs ├── zz.js ├── .gitignore ├── next.config.mjs ├── tsconfig.json ├── tailwind.config.ts ├── package.json └── README.md /src/pages/api/proxy.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/types/siwe.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'siwe'; -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /public/chats/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d0sc4u/evm-pumpfun-token-launchpad/HEAD/public/chats/1.png -------------------------------------------------------------------------------- /public/chats/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d0sc4u/evm-pumpfun-token-launchpad/HEAD/public/chats/10.png -------------------------------------------------------------------------------- /public/chats/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d0sc4u/evm-pumpfun-token-launchpad/HEAD/public/chats/2.png -------------------------------------------------------------------------------- /public/chats/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d0sc4u/evm-pumpfun-token-launchpad/HEAD/public/chats/3.png -------------------------------------------------------------------------------- /public/chats/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d0sc4u/evm-pumpfun-token-launchpad/HEAD/public/chats/4.png -------------------------------------------------------------------------------- /public/chats/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d0sc4u/evm-pumpfun-token-launchpad/HEAD/public/chats/5.png -------------------------------------------------------------------------------- /public/chats/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d0sc4u/evm-pumpfun-token-launchpad/HEAD/public/chats/6.png -------------------------------------------------------------------------------- /public/chats/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d0sc4u/evm-pumpfun-token-launchpad/HEAD/public/chats/7.png -------------------------------------------------------------------------------- /public/chats/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d0sc4u/evm-pumpfun-token-launchpad/HEAD/public/chats/8.png -------------------------------------------------------------------------------- /public/chats/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d0sc4u/evm-pumpfun-token-launchpad/HEAD/public/chats/9.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d0sc4u/evm-pumpfun-token-launchpad/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/seo/dash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d0sc4u/evm-pumpfun-token-launchpad/HEAD/public/seo/dash.jpg -------------------------------------------------------------------------------- /public/seo/home.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d0sc4u/evm-pumpfun-token-launchpad/HEAD/public/seo/home.jpg -------------------------------------------------------------------------------- /public/logo/wbone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d0sc4u/evm-pumpfun-token-launchpad/HEAD/public/logo/wbone.png -------------------------------------------------------------------------------- /public/seo/create.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d0sc4u/evm-pumpfun-token-launchpad/HEAD/public/seo/create.jpg -------------------------------------------------------------------------------- /public/logo/ethereum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d0sc4u/evm-pumpfun-token-launchpad/HEAD/public/logo/ethereum.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/types/contracts/factories/index.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | export { BondingCurveManager__factory } from "./BondingCurveManager__factory"; 5 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from "next/document"; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/types/heroicons.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@@heroicons/react/24/outline' { 2 | import * as React from 'react'; 3 | 4 | export const Bars3Icon: React.FC>; 5 | export const XMarkIcon: React.FC>; 6 | 7 | // Add other icons here as needed 8 | } 9 | -------------------------------------------------------------------------------- /public/logo/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/types/contracts/index.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | export type { BondingCurveManager } from "./BondingCurveManager"; 5 | export * as factories from "./factories"; 6 | export { BondingCurveManager__factory } from "./factories/BondingCurveManager__factory"; 7 | -------------------------------------------------------------------------------- /src/pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from "next"; 3 | 4 | type Data = { 5 | name: string; 6 | }; 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse, 11 | ) { 12 | res.status(200).json({ name: "John Doe" }); 13 | } 14 | -------------------------------------------------------------------------------- /zz.js: -------------------------------------------------------------------------------- 1 | const { ethers } = require('ethers'); 2 | 3 | async function main() { 4 | const creationCode = '...'; 5 | 6 | const strippedCreationCode = creationCode.substring(2); 7 | const routerAddressHex = strippedCreationCode.slice(-40); 8 | const routerAddress = ethers.utils.getAddress('0x' + routerAddressHex); 9 | 10 | console.log('Router address used during deployment:', routerAddress); 11 | } 12 | 13 | main().catch((error) => { 14 | console.error(error); 15 | process.exit(1); 16 | }); 17 | //verify teh router address 18 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | images: { 5 | remotePatterns: [ 6 | { 7 | protocol: 'https', 8 | hostname: 'ipfs-chainsafe.dev', 9 | }, 10 | { 11 | protocol: 'https', 12 | hostname: '**', 13 | }, 14 | ], 15 | }, 16 | async rewrites() { 17 | return [ 18 | { 19 | source: '/api/:path*', 20 | destination: '/api/proxy', 21 | }, 22 | ]; 23 | }, 24 | }; 25 | 26 | export default nextConfig; 27 | -------------------------------------------------------------------------------- /src/pages/api/auth/nonce.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import { randomBytes } from 'crypto'; 3 | 4 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 5 | if (req.method !== 'GET') { 6 | return res.status(405).json({ error: 'Method not allowed' }); 7 | } 8 | 9 | /* 10 | Expires in 5 minutes 11 | */ 12 | const nonce = randomBytes(32).toString('hex'); 13 | res.setHeader('Set-Cookie', `siwe_nonce=${nonce}; HttpOnly; Path=/; Max-Age=300`); 14 | res.status(200).json({ nonce }); 15 | } 16 | -------------------------------------------------------------------------------- /src/pages/api/auth/user.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import { parse } from 'cookie'; 3 | 4 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 5 | if (req.method !== 'GET') { 6 | return res.status(405).json({ error: 'Method not allowed' }); 7 | } 8 | 9 | const cookies = parse(req.headers.cookie || ''); 10 | const address = cookies.siwe_session; 11 | 12 | if (address) { 13 | res.status(200).json({ address }); 14 | } else { 15 | res.status(401).json({ error: 'Not authenticated' }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "paths": { 16 | "@/*": ["./src/*"] 17 | } 18 | }, 19 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 20 | "exclude": ["node_modules"] 21 | } -------------------------------------------------------------------------------- /src/components/notifications/Modal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface ModalProps { 4 | isOpen: boolean; 5 | onClose: () => void; 6 | children: React.ReactNode; 7 | } 8 | 9 | const Modal: React.FC = ({ isOpen, onClose, children }) => { 10 | if (!isOpen) return null; 11 | 12 | return ( 13 |
14 |
15 | {children} 16 |
17 |
18 | ); 19 | }; 20 | 21 | export default Modal; -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/logo/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | Bondle. 15 | 16 | -------------------------------------------------------------------------------- /src/components/ui/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface SpinnerProps { 4 | size?: 'small' | 'medium' | 'large'; 5 | } 6 | 7 | const Spinner: React.FC = ({ size = 'medium' }) => { 8 | const sizeClasses = { 9 | small: 'w-4 h-4', 10 | medium: 'w-6 h-6', 11 | large: 'w-8 h-8' 12 | }; 13 | 14 | return ( 15 |
16 |
22 |
23 | ); 24 | }; 25 | 26 | export default Spinner; -------------------------------------------------------------------------------- /src/components/layout/Layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Navbar from './Navbar' 3 | import Footer from './Footer' 4 | import LiveNotifications from '../notifications/LiveNotifications' 5 | 6 | interface LayoutProps { 7 | children: React.ReactNode 8 | } 9 | 10 | const Layout: React.FC = ({ children }) => { 11 | return ( 12 |
13 |
14 | 15 | 16 |
17 |
18 | {children} 19 |
20 |
21 |
22 | ) 23 | } 24 | 25 | export default Layout -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: [ 5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 13 | "gradient-conic": 14 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 15 | }, 16 | animation: { 17 | crawl: 'crawl 20s linear infinite', 18 | }, 19 | keyframes: { 20 | crawl: { 21 | '0%': { transform: 'translateX(100%)' }, 22 | '100%': { transform: 'translateX(-100%)' }, 23 | }, 24 | }, 25 | }, 26 | }, 27 | plugins: [], 28 | }; 29 | 30 | export default config; -------------------------------------------------------------------------------- /src/utils/chatUtils.ts: -------------------------------------------------------------------------------- 1 | export function formatTimestamp(timestamp: string): string { 2 | const now = new Date(); 3 | const messageDate = new Date(timestamp); 4 | const diffInSeconds = Math.floor((now.getTime() - messageDate.getTime()) / 1000); 5 | 6 | if (diffInSeconds < 60) return `${diffInSeconds} sec ago`; 7 | if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)} min ago`; 8 | if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)} hour ago`; 9 | if (diffInSeconds < 2592000) return `${Math.floor(diffInSeconds / 86400)} day ago`; 10 | if (diffInSeconds < 31536000) return `${Math.floor(diffInSeconds / 2592000)} month ago`; 11 | return `${Math.floor(diffInSeconds / 31536000)} year ago`; 12 | } 13 | 14 | export function getRandomAvatarImage(): string { 15 | const randomNumber = Math.floor(Math.random() * 10) + 1; 16 | return `/chats/${randomNumber}.png`; 17 | } 18 | 19 | export function shortenAddress(address: string): string { 20 | return address.slice(2, 8); 21 | } -------------------------------------------------------------------------------- /src/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as SwitchPrimitives from "@radix-ui/react-switch" 3 | 4 | const Switch = React.forwardRef< 5 | React.ElementRef, 6 | React.ComponentPropsWithoutRef 7 | >(({ className, ...props }, ref) => ( 8 | 13 | 14 | 15 | )) 16 | Switch.displayName = SwitchPrimitives.Root.displayName 17 | 18 | export { Switch } -------------------------------------------------------------------------------- /src/pages/api/auth/verify.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import { SiweMessage } from 'siwe'; 3 | import { parse } from 'cookie'; 4 | 5 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 6 | if (req.method !== 'POST') { 7 | return res.status(405).json({ error: 'Method not allowed' }); 8 | } 9 | 10 | try { 11 | const { message, signature } = req.body; 12 | 13 | const siweMessage = new SiweMessage(message); 14 | const fields = await siweMessage.validate(signature); 15 | 16 | const cookies = parse(req.headers.cookie || ''); 17 | const storedNonce = cookies.siwe_nonce; 18 | 19 | if (fields.nonce !== storedNonce) { 20 | return res.status(422).json({ error: 'Invalid nonce' }); 21 | } 22 | 23 | const sessionExpiration = 5 * 24 * 60 * 60; 24 | res.setHeader('Set-Cookie', `siwe_session=${fields.address}; HttpOnly; Path=/; Max-Age=${sessionExpiration}`); 25 | 26 | res.status(200).json({ ok: true }); 27 | } catch (error) { 28 | console.error('Verification failed:', error); 29 | res.status(400).json({ error: 'Verification failed' }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/components/ui/SortOptions.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export type SortOption = 'all' | 'recentCreated' | 'ended' | 'bomper'; 4 | 5 | interface SortOptionsProps { 6 | onSort: (option: SortOption) => void; 7 | currentSort: SortOption; 8 | } 9 | 10 | const sortOptionMapping: { [key: string]: SortOption } = { 11 | 'All': 'all', 12 | 'Creation Time': 'recentCreated', 13 | 'Ended': 'ended', 14 | 'Bomper': 'bomper' 15 | }; 16 | 17 | const SortOptions: React.FC = ({ onSort, currentSort }) => { 18 | return ( 19 |
20 | {Object.keys(sortOptionMapping).map((option) => ( 21 | 32 | ))} 33 |
34 | ); 35 | }; 36 | 37 | export default SortOptions; -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '@/styles/globals.css' 2 | import type { AppProps } from 'next/app' 3 | import { WagmiConfig, createConfig, WagmiProvider } from 'wagmi' 4 | import { shibarium } from 'wagmi/chains' 5 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query' 6 | import { getDefaultConfig, RainbowKitProvider } from '@rainbow-me/rainbowkit' 7 | import '@rainbow-me/rainbowkit/styles.css' 8 | import { ToastContainer } from 'react-toastify'; 9 | import 'react-toastify/dist/ReactToastify.css'; 10 | import { WebSocketProvider } from '@/components/providers/WebSocketProvider'; 11 | 12 | 13 | 14 | const config = getDefaultConfig({ 15 | appName: "Pump Fun", 16 | projectId: "YOUR_PROJECT_ID", 17 | chains: [shibarium], 18 | ssr: true, 19 | }); 20 | 21 | 22 | const queryClient = new QueryClient() 23 | 24 | export default function App({ Component, pageProps }: AppProps) { 25 | return ( 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | ) 37 | } -------------------------------------------------------------------------------- /src/components/layout/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Link from 'next/link'; 3 | 4 | const Footer: React.FC = () => { 5 | return ( 6 |
7 |
8 |
9 |
10 |

11 | © {new Date().getFullYear()} Bondle. All rights reserved. 12 |

13 |
14 | 25 |
26 |
27 |
28 | ); 29 | }; 30 | 31 | export default Footer; -------------------------------------------------------------------------------- /src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Link from 'next/link'; 3 | import Layout from '@/components/layout/Layout'; 4 | import SEO from '@/components/seo/SEO'; 5 | import { Ban } from 'lucide-react'; 6 | 7 | const Custom404: React.FC = () => { 8 | return ( 9 | 10 | 14 |
15 | 16 |

Not Found

17 |

18 | Wait and try again later. It seems like what you are looking for doesn't exist or is still indexing. 19 |

20 | 21 | 24 | 25 |
26 |
27 | ); 28 | }; 29 | 30 | export default Custom404; -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/types/contracts/common.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | import type { Listener } from "@ethersproject/providers"; 5 | import type { Event, EventFilter } from "ethers"; 6 | 7 | export interface TypedEvent< 8 | TArgsArray extends Array = any, 9 | TArgsObject = any 10 | > extends Event { 11 | args: TArgsArray & TArgsObject; 12 | } 13 | 14 | export interface TypedEventFilter<_TEvent extends TypedEvent> 15 | extends EventFilter {} 16 | 17 | export interface TypedListener { 18 | (...listenerArg: [...__TypechainArgsArray, TEvent]): void; 19 | } 20 | 21 | type __TypechainArgsArray = T extends TypedEvent ? U : never; 22 | 23 | export interface OnEvent { 24 | ( 25 | eventFilter: TypedEventFilter, 26 | listener: TypedListener 27 | ): TRes; 28 | (eventName: string, listener: Listener): TRes; 29 | } 30 | 31 | export type MinEthersFactory = { 32 | deploy(...a: ARGS[]): Promise; 33 | }; 34 | 35 | export type GetContractTypeFromFactory = F extends MinEthersFactory< 36 | infer C, 37 | any 38 | > 39 | ? C 40 | : never; 41 | 42 | export type GetARGsTypeFromFactory = F extends MinEthersFactory 43 | ? Parameters 44 | : never; 45 | -------------------------------------------------------------------------------- /src/components/seo/OGPreview.tsx: -------------------------------------------------------------------------------- 1 | //just another test for Open Graph Preview in the ui 2 | 3 | import React from 'react'; 4 | import Head from 'next/head'; 5 | 6 | const OGPreview = () => { 7 | const [ogData, setOgData] = React.useState({ 8 | title: '', 9 | description: '', 10 | image: '', 11 | url: '', 12 | }); 13 | 14 | React.useEffect(() => { 15 | const title = document.querySelector('meta[property="og:title"]')?.getAttribute('content') || ''; 16 | const description = document.querySelector('meta[property="og:description"]')?.getAttribute('content') || ''; 17 | const image = document.querySelector('meta[property="og:image"]')?.getAttribute('content') || ''; 18 | const url = document.querySelector('meta[property="og:url"]')?.getAttribute('content') || ''; 19 | 20 | setOgData({ title, description, image, url }); 21 | }, []); 22 | 23 | return ( 24 |
25 |
Open Graph Preview
26 |
27 | {ogData.image && ( 28 | OG Image 29 | )} 30 |

{ogData.title}

31 |

{ogData.description}

32 |

{ogData.url}

33 |
34 |
35 | ); 36 | }; 37 | 38 | export default OGPreview; -------------------------------------------------------------------------------- /src/components/ui/SearchFilter.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'; 3 | 4 | interface SearchFilterProps { 5 | onSearch: (query: string) => void; 6 | } 7 | 8 | const SearchFilter: React.FC = ({ onSearch }) => { 9 | const [query, setQuery] = useState(''); 10 | 11 | const handleSearch = (e: React.FormEvent) => { 12 | e.preventDefault(); 13 | onSearch(query); 14 | }; 15 | 16 | return ( 17 |
18 |
19 | setQuery(e.target.value)} 24 | className="w-full py-2.5 pl-10 pr-24 text-[10px] sm:text-xs text-white bg-gray-800 border border-gray-700 rounded-full focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 transition-colors duration-200" 25 | /> 26 | 27 | 33 |
34 |
35 | ); 36 | }; 37 | 38 | export default SearchFilter; -------------------------------------------------------------------------------- /src/components/seo/SEO.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import { useRouter } from 'next/router'; 3 | 4 | interface SEOProps { 5 | title?: string; 6 | description?: string; 7 | image?: string; 8 | token?: { 9 | name: string; 10 | symbol: string; 11 | description: string; 12 | logo: string; 13 | }; 14 | } 15 | 16 | const SEO: React.FC = ({ title, description, image, token }) => { 17 | const router = useRouter(); 18 | const domain = process.env.NEXT_PUBLIC_DOMAIN || 'https://bondle.xyz'; 19 | 20 | const seo = { 21 | title: token ? `${token.name} (${token.symbol}) - Bondle` : title || 'Bondle - Explore and Trade Tokens', 22 | description: token?.description || description || 'Explore, create, and trade tokens on the Bondle platform', 23 | image: token?.logo || image || `${domain}/default-og-image.jpg`, 24 | url: `${domain}${router.asPath}`, 25 | }; 26 | 27 | return ( 28 | 29 | {seo.title} 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | ); 42 | }; 43 | 44 | export default SEO; -------------------------------------------------------------------------------- /src/components/providers/WebSocketProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react'; 2 | import { Token } from '@/interface/types'; 3 | 4 | interface WebSocketContextType { 5 | newTokens: Token[]; 6 | newTransactions: any[]; 7 | } 8 | 9 | const WebSocketContext = createContext(undefined); 10 | 11 | export const useWebSocket = () => { 12 | const context = useContext(WebSocketContext); 13 | if (context === undefined) { 14 | throw new Error('useWebSocket must be used within a WebSocketProvider'); 15 | } 16 | return context; 17 | }; 18 | 19 | export const WebSocketProvider: React.FC<{ children: ReactNode }> = ({ children }) => { 20 | const [newTokens, setNewTokens] = useState([]); 21 | const [newTransactions, setNewTransactions] = useState([]); 22 | 23 | useEffect(() => { 24 | const socket = new WebSocket(process.env.NEXT_PUBLIC_WS_BASE_URL as string); 25 | 26 | socket.onopen = () => { 27 | console.log('WebSocket connected'); 28 | }; 29 | 30 | socket.onmessage = (event) => { 31 | const data = JSON.parse(event.data); 32 | if (data.type === 'tokenCreated') { 33 | setNewTokens(prev => [data.data, ...prev]); 34 | } else if (data.type === 'tokensBought' || data.type === 'tokensSold') { 35 | setNewTransactions(prev => [data.data, ...prev]); 36 | } 37 | }; 38 | 39 | return () => { 40 | socket.close(); 41 | }; 42 | }, []); 43 | 44 | return ( 45 | 46 | {children} 47 | 48 | ); 49 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pump-fun-ui", 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 | "typechain": "typechain --target ethers-v5 --out-dir src/types/contracts './src/abis/*.json'" 11 | }, 12 | "dependencies": { 13 | "@headlessui/react": "^2.1.2", 14 | "@heroicons/react": "^2.1.4", 15 | "@radix-ui/react-switch": "^1.1.0", 16 | "@rainbow-me/rainbowkit": "^2.1.3", 17 | "@tanstack/react-query": "^5.50.1", 18 | "axios": "^1.7.2", 19 | "chart.js": "^4.4.3", 20 | "chartjs-adapter-date-fns": "^3.0.0", 21 | "chartjs-plugin-zoom": "^2.0.1", 22 | "date-fns": "^3.6.0", 23 | "ethers": "5", 24 | "formidable": "^3.5.1", 25 | "framer-motion": "^11.3.21", 26 | "lightweight-charts": "^4.1.7", 27 | "lucide-react": "^0.407.0", 28 | "next": "14.2.4", 29 | "next-auth": "^4.24.8", 30 | "next-http-proxy-middleware": "^1.2.6", 31 | "react": "^18", 32 | "react-chartjs-2": "^5.2.0", 33 | "react-dom": "^18", 34 | "react-hot-toast": "^2.4.1", 35 | "react-toastify": "^10.0.5", 36 | "recharts": "^2.12.7", 37 | "siwe": "^2.3.2", 38 | "use-debounce": "^10.0.1", 39 | "viem": "^2.17.2", 40 | "wagmi": "^2.10.9" 41 | }, 42 | "devDependencies": { 43 | "@typechain/ethers-v5": "^11.1.2", 44 | "@types/cookie": "^0.6.0", 45 | "@types/formidable": "^3.4.5", 46 | "@types/node": "^20", 47 | "@types/react": "^18", 48 | "@types/react-dom": "^18", 49 | "eslint": "^8", 50 | "eslint-config-next": "14.2.4", 51 | "postcss": "^8", 52 | "tailwindcss": "^3.4.1", 53 | "typechain": "^8.3.2", 54 | "typescript": "^5" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); 2 | 3 | @tailwind base; 4 | @tailwind components; 5 | @tailwind utilities; 6 | 7 | :root { 8 | --foreground-rgb: 255, 255, 255; 9 | --background-start-rgb: 13, 17, 23; 10 | --background-end-rgb: 22, 27, 34; 11 | --accent-color: 88, 166, 255; 12 | } 13 | 14 | body { 15 | color: rgb(var(--foreground-rgb)); 16 | background: linear-gradient( 17 | to bottom, 18 | transparent, 19 | rgb(var(--background-end-rgb)) 20 | ) 21 | rgb(var(--background-start-rgb)); 22 | font-family: 'Press Start 2P', cursive; 23 | } 24 | 25 | @layer base { 26 | h1, h2, h3, h4, h5, h6 { 27 | @apply font-bold mb-4; 28 | } 29 | 30 | h1 { 31 | @apply text-3xl md:text-4xl; 32 | } 33 | 34 | h2 { 35 | @apply text-2xl md:text-3xl; 36 | } 37 | 38 | h3 { 39 | @apply text-xl md:text-2xl; 40 | } 41 | } 42 | 43 | @layer components { 44 | .btn { 45 | @apply px-4 py-2 rounded-md font-bold transition-colors duration-200; 46 | } 47 | 48 | .btn-primary { 49 | @apply bg-blue-500 text-white hover:bg-blue-600; 50 | } 51 | 52 | .btn-secondary { 53 | @apply bg-gray-700 text-white hover:bg-gray-600; 54 | } 55 | 56 | .card { 57 | @apply bg-gray-800 rounded-lg shadow-lg p-6; 58 | } 59 | } 60 | 61 | @layer utilities { 62 | .text-balance { 63 | text-wrap: balance; 64 | } 65 | 66 | .neon-text { 67 | text-shadow: 0 0 5px rgb(var(--accent-color)), 0 0 10px rgb(var(--accent-color)), 0 0 15px rgb(var(--accent-color)); 68 | } 69 | 70 | .neon-border { 71 | box-shadow: 0 0 5px rgb(var(--accent-color)), 0 0 10px rgb(var(--accent-color)); 72 | } 73 | } 74 | 75 | 76 | .custom-scrollbar::-webkit-scrollbar { 77 | width: 8px; 78 | } 79 | 80 | .custom-scrollbar::-webkit-scrollbar-track { 81 | background: #2d3748; 82 | } 83 | 84 | .custom-scrollbar::-webkit-scrollbar-thumb { 85 | background-color: #4a5568; 86 | border-radius: 4px; 87 | } 88 | 89 | .custom-scrollbar::-webkit-scrollbar-thumb:hover { 90 | background-color: #718096; 91 | } -------------------------------------------------------------------------------- /src/interface/types.ts: -------------------------------------------------------------------------------- 1 | // types.ts 2 | 3 | export interface LiquidityEvent { 4 | id: string; 5 | ethAmount: string; 6 | tokenAmount: string; 7 | timestamp: string; 8 | } 9 | 10 | export interface Token { 11 | map: any; 12 | id: string; 13 | address: string; 14 | creatorAddress: string; 15 | name: string; 16 | symbol: string; 17 | logo: string; 18 | description: string; 19 | createdAt: string; 20 | updatedAt: string; 21 | _count: { 22 | liquidityEvents: number; 23 | }; 24 | } 25 | 26 | export interface TokenWithLiquidityEvents extends Token { 27 | liquidityEvents: LiquidityEvent[]; 28 | } 29 | 30 | export interface PaginatedResponse { 31 | [x: string]: any;//wad added 32 | tokens: never[]; 33 | data: T[]; 34 | totalCount: number; 35 | currentPage: number; 36 | totalPages: number; 37 | } 38 | 39 | // Add this new interface to your types file 40 | export interface TokenWithTransactions extends Token { 41 | id: string; 42 | youtube: string | undefined; 43 | discord: string | undefined; 44 | twitter: any; 45 | website: any; 46 | telegram: any; 47 | transactions: { 48 | data: Transaction[]; 49 | pagination: { 50 | currentPage: number; 51 | pageSize: number; 52 | totalCount: number; 53 | totalPages: number; 54 | } 55 | } 56 | } 57 | 58 | // Make sure you have a Transaction interface defined, if not, add it: 59 | export interface Transaction { 60 | id: string; 61 | type: string; 62 | senderAddress: string; 63 | recipientAddress: string; 64 | ethAmount: string; 65 | tokenAmount: string; 66 | tokenPrice: string; 67 | txHash: string; 68 | timestamp: string; 69 | // Add any other fields that your transaction object includes 70 | } 71 | 72 | 73 | export interface PriceResponse { 74 | price: string; 75 | } 76 | 77 | export interface HistoricalPrice { 78 | tokenPrice: string; 79 | timestamp: string; 80 | } 81 | 82 | export interface USDHistoricalPrice { 83 | tokenPriceUSD: string; 84 | timestamp: string; 85 | } 86 | 87 | export interface TokenHolder { 88 | address: string; 89 | balance: string; 90 | } 91 | 92 | export interface TransactionResponse extends Omit, 'data'> { 93 | transactions: Transaction[]; 94 | } -------------------------------------------------------------------------------- /src/pages/api/upload-to-ipfs.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next' 2 | import axios from 'axios' 3 | import formidable from 'formidable' 4 | import fs from 'fs' 5 | 6 | export const config = { 7 | api: { 8 | bodyParser: false, 9 | }, 10 | } 11 | 12 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 13 | if (req.method !== 'POST') { 14 | return res.status(405).json({ message: 'Method not allowed' }) 15 | } 16 | 17 | const form = formidable(); 18 | 19 | try { 20 | const [fields, files] = await form.parse(req); 21 | 22 | const file = files.file?.[0]; 23 | if (!file) { 24 | return res.status(400).json({ error: 'No file uploaded' }) 25 | } 26 | 27 | const fileContent = await fs.promises.readFile(file.filepath); 28 | 29 | const formData = new FormData(); 30 | formData.append('file', new Blob([fileContent]), file.originalFilename || 'unnamed_file'); 31 | 32 | const apiKey = process.env.CHAINSAFE_API_KEY; 33 | const bucketId = process.env.CHAINSAFE_BUCKET_ID; 34 | 35 | if (!apiKey || !bucketId) { 36 | console.error('Missing API key or bucket ID'); 37 | return res.status(500).json({ error: 'Server configuration error' }); 38 | } 39 | 40 | const response = await axios.post( 41 | `https://api.chainsafe.io/api/v1/bucket/${bucketId}/upload`, 42 | formData, 43 | { 44 | headers: { 45 | 'Authorization': `Bearer ${apiKey}`, 46 | 'Content-Type': 'multipart/form-data', 47 | }, 48 | } 49 | ) 50 | 51 | if (response.data.files_details && response.data.files_details.length > 0) { 52 | const cid = response.data.files_details[0].cid 53 | const url = `https://ipfs-chainsafe.dev/ipfs/${cid}` 54 | res.status(200).json({ url }) 55 | } else { 56 | res.status(500).json({ error: 'No CID found in the response' }) 57 | } 58 | 59 | } catch (error) { 60 | console.error('Error uploading to IPFS:', error) 61 | if (axios.isAxiosError(error)) { 62 | console.error('Axios error details:', error.response?.data) 63 | res.status(error.response?.status || 500).json({ error: error.response?.data || 'Failed to upload image to IPFS' }) 64 | } else { 65 | res.status(500).json({ error: 'Failed to upload image to IPFS' }) 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /src/components/PriceLiquidity.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useCurrentTokenPrice, useTokenLiquidity } from '@/utils/blockchainUtils'; 3 | import { formatAmount, formatAmountV2 } from '@/utils/blockchainUtils'; 4 | 5 | interface PriceLiquidityProps { 6 | address: `0x${string}`; 7 | } 8 | 9 | const PriceLiquidity: React.FC = ({ address }) => { 10 | const { data: currentPrice } = useCurrentTokenPrice(address); 11 | const { data: liquidityData } = useTokenLiquidity(address); 12 | 13 | const calculateProgress = (currentLiquidity: bigint): number => { 14 | const liquidityInEth = parseFloat(formatAmountV2(currentLiquidity.toString())); 15 | const target = 2500; 16 | const progress = (liquidityInEth / target) * 100; 17 | return Math.min(progress, 100); 18 | }; 19 | 20 | return ( 21 |
22 |
23 |

Current Price

24 |

25 | {currentPrice ? formatAmount(currentPrice.toString()) : 'Loading...'} BONE 26 |

27 |
28 |
29 |

Current Liquidity

30 |

31 | {liquidityData && liquidityData[2] ? `${formatAmountV2(liquidityData[2].toString())} BONE` : '0 BONE'} 32 |

33 | {liquidityData && liquidityData[2] && ( 34 | <> 35 |
36 |
40 |
43 | {calculateProgress(liquidityData[2]).toFixed(2)}% 44 |
45 |
46 | 47 | )} 48 |
49 |
50 | ); 51 | }; 52 | 53 | export default PriceLiquidity; -------------------------------------------------------------------------------- /src/components/notifications/PurchaseConfirmationPopup.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { X as XIcon } from 'lucide-react'; 3 | import { parseUnits } from 'viem'; 4 | 5 | interface PurchaseConfirmationPopupProps { 6 | onConfirm: (amount: bigint) => void; 7 | onCancel: () => void; 8 | tokenSymbol: string; 9 | } 10 | 11 | const PurchaseConfirmationPopup: React.FC = ({ onConfirm, onCancel, tokenSymbol }) => { 12 | const [purchaseAmount, setPurchaseAmount] = useState(''); 13 | 14 | const handleConfirm = () => { 15 | const amount = parseFloat(purchaseAmount); 16 | onConfirm(amount ? parseUnits(purchaseAmount, 18) : BigInt(0)); 17 | }; 18 | 19 | return ( 20 |
21 |
22 | 29 |

How many {tokenSymbol} do you want to buy?

30 |

31 | Tip: It's optional, but buying a small amount helps protect your coin from snipers 32 |

33 | setPurchaseAmount(e.target.value)} 37 | className="w-full py-2 px-3 bg-gray-700 border border-gray-600 rounded-md text-white mb-3 text-[8px]" 38 | placeholder={`0.0 (optional)`} 39 | /> 40 |
41 | 42 | 43 |
44 |

45 | Cost to deploy: ~1 BONE 46 |

47 |
48 |
49 | ); 50 | }; 51 | 52 | export default PurchaseConfirmationPopup; -------------------------------------------------------------------------------- /src/components/notifications/HowItWorksPopup.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { XMarkIcon, LightBulbIcon, CurrencyDollarIcon, ArrowTrendingUpIcon, BanknotesIcon, FireIcon } from '@heroicons/react/24/outline'; 3 | 4 | const HowItWorksPopup: React.FC = () => { 5 | const [isVisible, setIsVisible] = useState(false); 6 | 7 | useEffect(() => { 8 | const hasSeenPopup = localStorage.getItem('hasSeenHowItWorksPopup'); 9 | if (!hasSeenPopup) { 10 | setTimeout(() => setIsVisible(true), 1000); 11 | localStorage.setItem('hasSeenHowItWorksPopup', 'true'); 12 | } 13 | }, []); 14 | 15 | if (!isVisible) return null; 16 | 17 | const steps = [ 18 | { icon: LightBulbIcon, text: "Pick a coin you like" }, 19 | { icon: CurrencyDollarIcon, text: "Buy on the bonding curve" }, 20 | { icon: ArrowTrendingUpIcon, text: "Sell anytime for profits/losses" }, 21 | { icon: BanknotesIcon, text: "Curve reaches 2500 BONE" }, 22 | { icon: FireIcon, text: "BONE deposited in Chewyswap & burned" }, 23 | ]; 24 | 25 | return ( 26 |
27 |
28 |
29 | 36 |
37 |

How It Works

38 |

39 | Bondle ensures safe, fair-launch tokens with no presale or team allocation. 40 |

41 |
42 | {steps.map((step, index) => ( 43 |
44 |
45 | 46 |
47 |

{step.text}

48 |
49 | ))} 50 |
51 |
52 | 58 |
59 |
60 |
61 |
62 | ); 63 | }; 64 | 65 | export default HowItWorksPopup; 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EVM Launchpad - Fourmeme Forked 2 | 3 | Four Meme UI is a Next.js-based web application for interacting with the Four Meme platform. 4 | 5 | image 6 | image 7 | image 8 | image 9 | image 10 | 11 | 12 | ## Getting Started 13 | 14 | ### Prerequisites 15 | 16 | - Node.js (version 14 or later) 17 | - Yarn package manager 18 | 19 | ### Installation 20 | 21 | 1. Clone the repository: 22 | ```bash 23 | git clone https://github.com/0xopsdev/evm-launchpad-fourmeme-fork 24 | cd evm-launchpad-fourmeme-fork 25 | ``` 26 | 27 | 2. Install dependencies: 28 | ```bash 29 | yarn install 30 | ``` 31 | 32 | ### Running the Application 33 | 34 | 1. Set up your environment variables (see [Environment Variables](#environment-variables) section). 35 | 36 | 2. Start the development server: 37 | ```bash 38 | yarn dev 39 | ``` 40 | 41 | 3. Open your browser and navigate to `http://localhost:3000`. 42 | 43 | ## Architecture Overview 44 | 45 | Pump Fun UI is built using the following technologies and frameworks: 46 | 47 | - Next.js: React framework for server-side rendering and static site generation 48 | - React: JavaScript library for building user interfaces 49 | - Tailwind CSS: Utility-first CSS framework for styling 50 | - Ethers.js: Library for interacting with Ethereum 51 | - RainbowKit: Ethereum wallet connection library 52 | - Wagmi: React hooks for EVM chains 53 | - lightweight-charts: Charting libraries for data visualization 54 | 55 | The application follows a component-based architecture, with reusable UI components and hooks for managing state and interactions with the blockchain. 56 | 57 | ## Environment Variables 58 | 59 | Create a `.env.local` file in the root directory with the following variables: 60 | 61 | ``` 62 | NEXT_PUBLIC_API_BASE_URL=backend-url 63 | NEXT_PUBLIC_BONDING_CURVE_MANAGER_ADDRESS="contract-address" 64 | NEXT_PUBLIC_WS_BASE_URL=https://backend-url 65 | CHAINSAFE_API_KEY=your_chainsafe_api_key 66 | CHAINSAFE_BUCKET_ID=your_chainsafe_bucket_id 67 | ``` 68 | 69 | ### 🚀 Looking to build a platform like four.meme? 70 | 71 | I've made the UI open-source, but the backend and smart contract are closed-source. If you're interested in creating a full-fledged four.meme-like platform, let's collaborate! [Contact me on Telegram](https://t.me/d0sc4u) for more details. 72 | I can also provide the fourmeme-fork contract and backend so that you can get the entire set of platform from me. 73 | 74 | # 👨‍💻 Author 75 | DM me on telegram for full working version of this project 76 | ### 📞 Telegram: [d0sc4u](https://t.me/d0sc4u) 77 | https://t.me/d0sc4u 78 | 79 | -------------------------------------------------------------------------------- /src/components/auth/SiweAuth.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useAccount, useSignMessage } from 'wagmi'; 3 | import { SiweMessage } from 'siwe'; 4 | import { toast } from 'react-toastify'; 5 | 6 | interface SiweAuthProps { 7 | onAuthSuccess: () => void; 8 | } 9 | 10 | const SiweAuth: React.FC = ({ onAuthSuccess }) => { 11 | const { address } = useAccount(); 12 | const { signMessageAsync } = useSignMessage(); 13 | const [loading, setLoading] = useState(false); 14 | 15 | const handleSignIn = async () => { 16 | try { 17 | setLoading(true); 18 | 19 | const nonceRes = await fetch('/api/auth/nonce'); 20 | const { nonce } = await nonceRes.json(); 21 | 22 | const message = new SiweMessage({ 23 | domain: process.env.NEXT_PUBLIC_DOMAIN || window.location.host, 24 | address: address, 25 | statement: 'Sign in to post a message', 26 | uri: window.location.origin, 27 | version: '1', 28 | chainId: 1, 29 | nonce: nonce 30 | }); 31 | const messageToSign = message.prepareMessage(); 32 | const signature = await signMessageAsync({ message: messageToSign }); 33 | 34 | const verifyRes = await fetch('/api/auth/verify', { 35 | method: 'POST', 36 | headers: { 37 | 'Content-Type': 'application/json', 38 | }, 39 | body: JSON.stringify({ message, signature }), 40 | }); 41 | 42 | if (!verifyRes.ok) throw new Error('Error verifying message'); 43 | 44 | // Notify parent component of successful authentication 45 | onAuthSuccess(); 46 | toast.success('Successfully signed in!', { 47 | position: "top-right", 48 | autoClose: 3000, 49 | hideProgressBar: false, 50 | closeOnClick: true, 51 | pauseOnHover: true, 52 | draggable: true, 53 | }); 54 | } catch (error) { 55 | console.error(error); 56 | toast.error('Failed to sign. try again.', { 57 | position: "top-right", 58 | autoClose: 3000, 59 | hideProgressBar: false, 60 | closeOnClick: true, 61 | pauseOnHover: true, 62 | draggable: true, 63 | }); 64 | } finally { 65 | setLoading(false); 66 | } 67 | }; 68 | 69 | if (!address) return null; 70 | 71 | return ( 72 |
73 | 90 |
91 | ); 92 | }; 93 | 94 | export default SiweAuth; 95 | -------------------------------------------------------------------------------- /src/components/tokens/TokenList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TokenCard from './TokenCard'; 3 | import { Token, TokenWithLiquidityEvents } from '@/interface/types'; 4 | import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/outline'; 5 | 6 | interface TokenListProps { 7 | tokens: (Token | TokenWithLiquidityEvents)[]; 8 | currentPage: number; 9 | totalPages: number; 10 | onPageChange: (page: number) => void; 11 | isEnded: boolean; 12 | } 13 | 14 | const TokenList: React.FC = ({ tokens, currentPage, totalPages, onPageChange, isEnded }) => { 15 | const renderPageNumbers = () => { 16 | const pageNumbers = []; 17 | const maxVisiblePages = 5; 18 | 19 | if (totalPages <= maxVisiblePages) { 20 | for (let i = 1; i <= totalPages; i++) { 21 | pageNumbers.push(i); 22 | } 23 | } else { 24 | if (currentPage <= 3) { 25 | for (let i = 1; i <= 4; i++) { 26 | pageNumbers.push(i); 27 | } 28 | pageNumbers.push('...'); 29 | pageNumbers.push(totalPages); 30 | } else if (currentPage >= totalPages - 2) { 31 | pageNumbers.push(1); 32 | pageNumbers.push('...'); 33 | for (let i = totalPages - 3; i <= totalPages; i++) { 34 | pageNumbers.push(i); 35 | } 36 | } else { 37 | pageNumbers.push(1); 38 | pageNumbers.push('...'); 39 | for (let i = currentPage - 1; i <= currentPage + 1; i++) { 40 | pageNumbers.push(i); 41 | } 42 | pageNumbers.push('...'); 43 | pageNumbers.push(totalPages); 44 | } 45 | } 46 | 47 | return pageNumbers; 48 | }; 49 | 50 | return ( 51 |
52 |
53 | {tokens.map((token) => ( 54 | 55 | ))} 56 |
57 | 58 | {totalPages > 1 && ( 59 |
60 | 67 | 68 | {renderPageNumbers().map((page, index) => ( 69 | 80 | ))} 81 | 82 | 89 |
90 | )} 91 |
92 | ); 93 | }; 94 | 95 | export default TokenList; -------------------------------------------------------------------------------- /src/pages/FAQ.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Layout from '@/components/layout/Layout'; 3 | import { ChevronDownIcon } from '@heroicons/react/24/solid'; 4 | 5 | const FAQPage: React.FC = () => { 6 | const [openIndex, setOpenIndex] = useState(null); 7 | 8 | const faqs = [ 9 | { 10 | question: "What is a bonding curve?", 11 | answer: "A bonding curve is a mathematical function that defines the relationship between a token's price and its supply. It creates a dynamic pricing mechanism that automatically adjusts based on demand.\n\nKey points:\n• As supply increases, price increases\n• As supply decreases, price decreases\n• This creates a dynamic pricing mechanism that automatically adjusts based on demand" 12 | }, 13 | { 14 | question: "How do I create a token?", 15 | answer: "To create a token on Bondle:\n\n1. Go to 'Create Token' page\n2. Fill in token details (name, symbol, etc.)\n3. Upload an image (optional)\n4. Add social links (optional)\n5. Review details\n6. Pay small fee in BONE\n7. Wait for confirmation\n\nYour token will then be live and tradable!" 16 | }, 17 | { 18 | question: "How is the token price determined?", 19 | answer: "Token price is determined dynamically by the bonding curve.\n\n• Buying tokens: Price increases\n• Selling tokens: Price decreases\n\nThis creates a fair and transparent pricing mechanism reflecting real-time supply and demand." 20 | }, 21 | { 22 | question: "Can I sell my tokens at any time?", 23 | answer: "Yes, you can sell your tokens back to the contract at any time.\n\n• Sell price: Determined by current position on the bonding curve\n• Ensures continuous liquidity\n• Allows you to exit your position whenever you choose" 24 | }, 25 | { 26 | question: "Is there a fee for buying or selling tokens?", 27 | answer: "Yes, there's a small fee (typically 1%) for buying and selling.\n\nPurposes of the fee:\n1. Incentivize long-term holding\n2. Prevent market manipulation\n3. Contribute to platform sustainability\n4. Potentially reward token holders or fund development" 28 | } 29 | ]; 30 | 31 | const toggleFAQ = (index: number) => { 32 | setOpenIndex(openIndex === index ? null : index); 33 | }; 34 | 35 | return ( 36 | 37 |
38 |

Frequently Asked Questions

39 | 40 |
41 | {faqs.map((faq, index) => ( 42 |
43 | 53 |
57 |

{faq.answer}

58 |
59 |
60 | ))} 61 |
62 |
63 |
64 | ); 65 | }; 66 | 67 | export default FAQPage; -------------------------------------------------------------------------------- /src/components/TokenDetails/TokenInfo.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Link from 'next/link'; 3 | import { ExternalLinkIcon, MessageCircleIcon, GlobeIcon, TwitterIcon, YoutubeIcon, Info } from 'lucide-react'; 4 | import { TokenWithTransactions } from '@/interface/types'; 5 | import { formatTimestamp, shortenAddress, formatAddressV2 } from '@/utils/blockchainUtils'; 6 | 7 | interface TokenInfoProps { 8 | tokenInfo: TokenWithTransactions; 9 | } 10 | 11 | const TokenInfo: React.FC = ({ tokenInfo }) => { 12 | const [showTokenInfo, setShowTokenInfo] = useState(false); 13 | 14 | return ( 15 |
16 |
setShowTokenInfo(!showTokenInfo)} 19 | > 20 |

Token Information

21 | 22 |
23 | {showTokenInfo && ( 24 |
25 |
26 | 27 | 33 | 39 | 43 |
44 |
45 |
46 | Description: 47 | {tokenInfo?.description || 'Loading...'} 48 |
49 |
50 | Socials: 51 |
52 | } /> 53 | } /> 54 | } /> 55 | } /> 56 | } /> 57 |
58 |
59 |
60 |
61 | )} 62 |
63 | ); 64 | }; 65 | 66 | const InfoItem: React.FC<{ label: string; value?: string; link?: string; isExternal?: boolean }> = ({ label, value, link, isExternal }) => ( 67 |
68 | {label}: 69 | 70 | {link ? ( 71 | isExternal ? ( 72 | 73 | {value} 74 | 75 | ) : ( 76 | 77 | {value} 78 | 79 | ) 80 | ) : ( 81 | value 82 | )} 83 | 84 |
85 | ); 86 | 87 | const SocialLink: React.FC<{ href?: string; icon: React.ReactNode }> = ({ href, icon }) => ( 88 | href ? ( 89 | 90 | {icon} 91 | 92 | ) : null 93 | ); 94 | 95 | export default TokenInfo; 96 | -------------------------------------------------------------------------------- /src/pages/about.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Layout from '@/components/layout/Layout'; 3 | import { CubeTransparentIcon, ChartBarIcon, CurrencyDollarIcon, ArrowPathIcon, BeakerIcon } from '@heroicons/react/24/outline'; 4 | 5 | const AboutPage: React.FC = () => { 6 | return ( 7 | 8 |
9 |

About Bondle

10 | 11 |
12 |
13 |

What is Bondle?

14 |
15 |

16 | Bondle is a revolutionary decentralized platform that empowers individuals to create and trade tokens using innovative bonding curve technology. 17 |

18 |

19 | Our mission is to democratize token creation and provide a dynamic, fair trading environment for the crypto community. 20 |

21 |

22 | Our mission is to democratize token creation for the crypto community. 23 |

24 |

25 | We provide a dynamic and fair trading environment using bonding curve technology. 26 |

27 |
28 |
29 |
30 | 31 |
32 |

How It Works

33 |
34 | {[ 35 | { icon: CubeTransparentIcon, title: "Token Creation", description: "Users easily create tokens by setting a name, symbol, and description." }, 36 | { icon: ChartBarIcon, title: "Bonding Curve", description: "Each token's price is determined by its unique bonding curve." }, 37 | { icon: CurrencyDollarIcon, title: "Trading", description: "Users can buy and sell tokens, with prices dynamically adjusted by the curve." }, 38 | { icon: ArrowPathIcon, title: "Liquidity Pool", description: "Each token has its own liquidity pool that grows and shrinks with trades." }, 39 | { icon: BeakerIcon, title: "Customization", description: "Various curve shapes allow for different token economics." }, 40 | ].map((item, index) => ( 41 |
42 | 43 |

{item.title}

44 |

{item.description}

45 |
46 | ))} 47 |
48 |
49 | 50 |
51 |

Benefits of Bonding Curves

52 |
53 |
    54 | {[ 55 | { title: "Continuous Liquidity", description: "Tokens can always be bought or sold, ensuring a fluid market." }, 56 | { title: "Algorithmic Price Discovery", description: "Market price is determined automatically based on supply and demand." }, 57 | { title: "Incentivized Participation", description: "Early supporters benefit from potential price appreciation as demand grows." }, 58 | { title: "Flexible Token Economics", description: "Different curve shapes allow for various economic models to suit project needs." }, 59 | ].map((item, index) => ( 60 |
  • 61 | 62 | 63 | 64 |
    65 | {item.title}: {item.description} 66 |
    67 |
  • 68 | ))} 69 |
70 |
71 |
72 |
73 |
74 | ); 75 | }; 76 | 77 | export default AboutPage; -------------------------------------------------------------------------------- /src/components/notifications/LiveNotifications.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | import Image from 'next/image'; 3 | import { useWebSocket } from '@/components/providers/WebSocketProvider'; 4 | import { formatAmountV2 } from '@/utils/blockchainUtils'; 5 | 6 | interface Notification { 7 | message: string; 8 | type: 'buy' | 'sell' | 'tokenCreated'; 9 | logo?: string; 10 | } 11 | 12 | const LiveNotifications: React.FC = () => { 13 | const [currentNotification, setCurrentNotification] = useState(null); 14 | const [isVisible, setIsVisible] = useState(false); 15 | const containerRef = useRef(null); 16 | const animationRef = useRef(null); 17 | const timeoutRef = useRef(null); 18 | const { newTokens, newTransactions } = useWebSocket(); 19 | 20 | const createNotificationMessage = (data: { type: string; data: any }): Notification => { 21 | const addressEnd = data.data.senderAddress?.slice(-6) || data.data.creatorAddress?.slice(-6) || 'Unknown'; 22 | 23 | switch (data.type) { 24 | case 'buy': 25 | return { 26 | message: `${addressEnd} Bought ${formatAmountV2(data.data.ethAmount)} BONE of ${data.data.symbol}`, 27 | type: 'buy', 28 | logo: data.data.logo 29 | }; 30 | case 'sell': 31 | return { 32 | message: `${addressEnd} Sold ${formatAmountV2(data.data.ethAmount)} BONE of ${data.data.symbol}`, 33 | type: 'sell', 34 | logo: data.data.logo 35 | }; 36 | case 'tokenCreated': 37 | return { 38 | message: `${data.data.symbol} Created by ${addressEnd}`, 39 | type: 'tokenCreated', 40 | logo: data.data.logo 41 | }; 42 | default: 43 | console.error('Unknown notification type:', data.type); 44 | return { 45 | message: 'New activity', 46 | type: 'buy', 47 | logo: undefined 48 | }; 49 | } 50 | }; 51 | 52 | const closeNotification = () => { 53 | if (timeoutRef.current) { 54 | clearTimeout(timeoutRef.current); 55 | } 56 | timeoutRef.current = setTimeout(() => { 57 | setIsVisible(false); 58 | setCurrentNotification(null); 59 | }, 3000); // 3 seconds delay 60 | }; 61 | 62 | useEffect(() => { 63 | const handleNewNotification = (notification: Notification) => { 64 | setCurrentNotification(notification); 65 | setIsVisible(true); 66 | if (timeoutRef.current) { 67 | clearTimeout(timeoutRef.current); 68 | timeoutRef.current = null; 69 | } 70 | }; 71 | 72 | if (newTokens.length > 0) { 73 | console.log(newTokens) 74 | const notification = createNotificationMessage({ type: 'tokenCreated', data: newTokens[0] }); 75 | handleNewNotification(notification); 76 | } 77 | 78 | if (newTransactions.length > 0) { 79 | console.log(newTransactions) 80 | const notification = createNotificationMessage({ type: newTransactions[0].type, data: newTransactions[0] }); 81 | handleNewNotification(notification); 82 | } 83 | }, [newTokens, newTransactions]); 84 | 85 | useEffect(() => { 86 | if (containerRef.current && currentNotification && isVisible) { 87 | const container = containerRef.current; 88 | const totalWidth = container.scrollWidth; 89 | const viewportWidth = container.offsetWidth; 90 | const duration = 15000; // 15 seconds for a complete cycle 91 | 92 | if (animationRef.current) { 93 | animationRef.current.cancel(); 94 | } 95 | 96 | animationRef.current = container.animate( 97 | [ 98 | { transform: 'translateX(100%)' }, 99 | { transform: `translateX(-${totalWidth - viewportWidth}px)` } 100 | ], 101 | { 102 | duration: duration, 103 | easing: 'linear' 104 | } 105 | ); 106 | 107 | animationRef.current.onfinish = closeNotification; 108 | 109 | return () => { 110 | if (animationRef.current) { 111 | animationRef.current.cancel(); 112 | } 113 | if (timeoutRef.current) { 114 | clearTimeout(timeoutRef.current); 115 | } 116 | }; 117 | } 118 | }, [currentNotification, isVisible]); 119 | 120 | if (!isVisible || !currentNotification) return null; 121 | 122 | return ( 123 |
124 |
125 |
126 | {currentNotification.message} 127 | {currentNotification.logo && ( 128 | Token Logo 135 | )} 136 |
137 |
138 |
139 | ); 140 | }; 141 | 142 | export default LiveNotifications; -------------------------------------------------------------------------------- /src/components/ui/ShareButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Share2Icon, X } from 'lucide-react'; 3 | import { 4 | TwitterIcon, 5 | FacebookIcon, 6 | InstagramIcon, 7 | LinkedinIcon, 8 | Send as TelegramIcon, 9 | MessageCircle as DiscordIcon 10 | } from 'lucide-react'; 11 | 12 | interface TokenInfo { 13 | name: string; 14 | description: string; 15 | logo: string; 16 | } 17 | 18 | interface ShareModalProps { 19 | tokenInfo: TokenInfo; 20 | isOpen: boolean; 21 | onClose: () => void; 22 | } 23 | 24 | interface SocialPlatform { 25 | name: string; 26 | icon: React.ElementType; 27 | color: string; 28 | getShareUrl: (shareText: string, shareUrl: string, logoUrl: string) => string; 29 | } 30 | 31 | const ShareModal: React.FC = ({ tokenInfo, isOpen, onClose }) => { 32 | const [mounted, setMounted] = useState(false); 33 | 34 | useEffect(() => { 35 | setMounted(true); 36 | }, []); 37 | 38 | if (!mounted || !isOpen) return null; 39 | 40 | const shareUrl = typeof window !== 'undefined' ? window.location.href : ''; 41 | const shareText = `Check out ${tokenInfo.name} on our platform!\n\n${tokenInfo.description}\n\n`; 42 | 43 | const socialPlatforms: SocialPlatform[] = [ 44 | { 45 | name: 'Twitter', 46 | icon: TwitterIcon, 47 | color: 'bg-[#1DA1F2]', 48 | getShareUrl: (text, url, logo) => `https://twitter.com/intent/tweet?text=${encodeURIComponent(text)}&url=${encodeURIComponent(url)}&image=${encodeURIComponent(logo)}` 49 | }, 50 | { 51 | name: 'Facebook', 52 | icon: FacebookIcon, 53 | color: 'bg-[#4267B2]', 54 | getShareUrl: (text, url, logo) => `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}` 55 | }, 56 | { 57 | name: 'Telegram', 58 | icon: TelegramIcon, 59 | color: 'bg-[#0088cc]', 60 | getShareUrl: (text, url, logo) => `https://t.me/share/url?url=${encodeURIComponent(url)}&text=${encodeURIComponent(text)}` 61 | }, 62 | { 63 | name: 'LinkedIn', 64 | icon: LinkedinIcon, 65 | color: 'bg-[#0077B5]', 66 | getShareUrl: (text, url, logo) => `https://www.linkedin.com/shareArticle?mini=true&url=${encodeURIComponent(url)}&title=${encodeURIComponent(tokenInfo.name)}&summary=${encodeURIComponent(text)}` 67 | }, 68 | { 69 | name: 'Instagram', 70 | icon: InstagramIcon, 71 | color: 'bg-gradient-to-r from-[#833AB4] via-[#FD1D1D] to-[#FCAF45]', 72 | getShareUrl: (text, url, logo) => `https://www.instagram.com/` 73 | }, 74 | { 75 | name: 'Discord', 76 | icon: DiscordIcon, 77 | color: 'bg-[#7289DA]', 78 | getShareUrl: (text, url, logo) => `https://discord.com/` 79 | } 80 | ]; 81 | 82 | return ( 83 |
84 |
85 |
86 |

Share {tokenInfo.name} on

87 | 94 |
95 |
96 | {`${tokenInfo.name} 101 |
102 |
103 | {socialPlatforms.map((platform) => ( 104 | 112 | 113 | 114 | ))} 115 |
116 |
117 |
118 | ); 119 | }; 120 | 121 | interface ShareButtonProps { 122 | tokenInfo: TokenInfo; 123 | className?: string; 124 | } 125 | 126 | const ShareButton: React.FC = ({ tokenInfo, className }) => { 127 | const [isModalOpen, setIsModalOpen] = useState(false); 128 | 129 | return ( 130 | <> 131 |
132 | 139 |
140 | setIsModalOpen(false)} 144 | /> 145 | 146 | ); 147 | }; 148 | 149 | export default ShareButton; -------------------------------------------------------------------------------- /src/components/TokenDetails/TokenHolders.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ChevronLeftIcon, ChevronRightIcon, ExternalLinkIcon } from 'lucide-react'; 3 | import { TokenHolder } from '@/interface/types'; 4 | import { formatAmountV3, shortenAddress } from '@/utils/blockchainUtils'; 5 | 6 | interface TokenHoldersProps { 7 | tokenHolders: TokenHolder[]; 8 | currentPage: number; 9 | totalPages: number; 10 | tokenSymbol: string; 11 | creatorAddress: string; 12 | onPageChange: (page: number) => void; 13 | } 14 | 15 | const TokenHolders: React.FC = ({ 16 | tokenHolders, 17 | currentPage, 18 | totalPages, 19 | tokenSymbol, 20 | creatorAddress, 21 | onPageChange, 22 | }) => { 23 | return ( 24 |
25 |

Token Holders

26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | {/* Bonding Curve Manager as the first entry */} 36 | 37 | 40 | 41 | 42 | {tokenHolders.map((holder, index) => ( 43 | 44 | 65 | 66 | 67 | ))} 68 | 69 |
AddressAmount
38 |
Bonding Curve
39 |
Alpha
45 | {holder.address === creatorAddress ? ( 46 | 52 | Creator 53 | 54 | ) : ( 55 | 61 | {shortenAddress(holder.address)} 62 | 63 | )} 64 | {formatAmountV3(holder.balance)}
70 |
71 | {tokenHolders.length === 0 &&

No token holder data available

} 72 | 73 | {/* Updated Pagination for token holders */} 74 | {tokenHolders.length > 0 && ( 75 |
76 | 83 |
84 | {[...Array(totalPages)].map((_, index) => { 85 | const page = index + 1; 86 | if ( 87 | page === 1 || 88 | page === totalPages || 89 | (page >= currentPage - 1 && page <= currentPage + 1) 90 | ) { 91 | return ( 92 | 103 | ); 104 | } else if ( 105 | page === currentPage - 2 || 106 | page === currentPage + 2 107 | ) { 108 | return ( 109 | 110 | ... 111 | 112 | ); 113 | } 114 | return null; 115 | })} 116 |
117 | 124 |
125 | )} 126 |
127 | ); 128 | }; 129 | 130 | export default TokenHolders; -------------------------------------------------------------------------------- /src/components/TokenDetails/TransactionHistory.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { ChevronLeftIcon, ChevronRightIcon, ChevronDownIcon, ExternalLinkIcon } from 'lucide-react'; 3 | import { formatTimestamp, formatAmountV3, shortenAddress } from '@/utils/blockchainUtils'; 4 | import { Transaction } from '@/interface/types'; 5 | 6 | interface TransactionHistoryProps { 7 | transactions: Transaction[]; 8 | transactionPage: number; 9 | totalTransactionPages: number; 10 | tokenSymbol: string; 11 | handlePageChange: (newPage: number) => void; 12 | } 13 | 14 | const TransactionHistory: React.FC = ({ 15 | transactions, 16 | transactionPage, 17 | totalTransactionPages, 18 | tokenSymbol, 19 | handlePageChange, 20 | }) => { 21 | const [expandedRow, setExpandedRow] = useState(null); 22 | 23 | const toggleRow = (id: string) => { 24 | setExpandedRow(expandedRow === id ? null : id); 25 | }; 26 | 27 | return ( 28 |
29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | {transactions.map((tx) => ( 43 | 44 | toggleRow(tx.id)} 47 | > 48 | 49 | 50 | 51 | 52 | 53 | 58 | 61 | 62 | {expandedRow === tx.id && ( 63 | 64 | 78 | 79 | )} 80 | 81 | ))} 82 | 83 |
MakerTypeBONE{tokenSymbol}DateTx
{shortenAddress(tx.senderAddress)}{tx.type}{formatAmountV3(tx.ethAmount)}{formatAmountV3(tx.tokenAmount)}{formatTimestamp(tx.timestamp)} 54 | 55 | {tx.txHash.slice(-8)} 56 | 57 | 59 | 60 |
65 |
66 |

Maker: {shortenAddress(tx.senderAddress)}

67 |

Date: {formatTimestamp(tx.timestamp)}

68 | 74 | View Tx 75 | 76 |
77 |
84 |
85 | {transactions.length === 0 &&

No transaction history available

} 86 | 87 | {/* Updated Pagination for transactions */} 88 | {transactions.length > 0 && ( 89 |
90 | 97 |
98 | {[...Array(totalTransactionPages)].map((_, index) => { 99 | const page = index + 1; 100 | if ( 101 | page === 1 || 102 | page === totalTransactionPages || 103 | (page >= transactionPage - 1 && page <= transactionPage + 1) 104 | ) { 105 | return ( 106 | 117 | ); 118 | } else if ( 119 | page === transactionPage - 2 || 120 | page === transactionPage + 2 121 | ) { 122 | return ( 123 | 124 | ... 125 | 126 | ); 127 | } 128 | return null; 129 | })} 130 |
131 | 138 |
139 | )} 140 |
141 | ); 142 | }; 143 | 144 | export default TransactionHistory; -------------------------------------------------------------------------------- /src/pages/home.tsx: -------------------------------------------------------------------------------- 1 | // pages/index.tsx (future) 2 | import React from 'react'; 3 | import Link from 'next/link'; 4 | import Image from 'next/image'; 5 | import { motion } from 'framer-motion'; 6 | import { ArrowRightIcon, CubeTransparentIcon, ChartBarIcon, CurrencyDollarIcon } from '@heroicons/react/24/outline'; 7 | import Layout from '@/components/layout/Layout'; 8 | import SEO from '@/components/seo/SEO'; 9 | 10 | const FeatureCard: React.FC<{ icon: React.ElementType; title: string; description: string }> = ({ icon: Icon, title, description }) => ( 11 | 16 | 17 |

{title}

18 |

{description}

19 |
20 | ); 21 | 22 | const HomePage: React.FC = () => { 23 | return ( 24 | 25 | 30 |
31 | {/* Hero Section */} 32 |
33 |
34 |
35 | 41 | Welcome to Bondle 42 | 43 | 49 | Create, trade, and grow your tokens with bonding curves 50 | 51 | 56 | 57 | Enter App 58 | 61 |
62 |
63 |
64 | Hero background 65 |
66 |
67 |
68 | 69 | {/* Features Section */} 70 |
71 |
72 |

Why Choose Bondle?

73 |
74 | 79 | 84 | 89 |
90 |
91 |
92 | 93 | {/* How It Works Section */} 94 |
95 |
96 |

How It Works

97 |
98 | {[ 99 | "Create your token", 100 | "Set bonding curve parameters", 101 | "Users buy and sell tokens", 102 | "Watch your token grow" 103 | ].map((step, index) => ( 104 | 110 |
111 | {index + 1} 112 |
113 |

{step}

114 |
115 | ))} 116 |
117 |
118 |
119 | 120 | {/* Call to Action */} 121 |
122 |
123 |

Ready to revolutionize token trading?

124 |

Join Bondle today and experience the future of decentralized finance.

125 | 126 | Launch Your Token Now 127 |
130 |
131 |
132 |
133 | ); 134 | }; 135 | 136 | export default HomePage; -------------------------------------------------------------------------------- /src/components/layout/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import Link from 'next/link' 3 | import { ConnectButton } from '@rainbow-me/rainbowkit' 4 | import { Bars3Icon, XMarkIcon } from '@heroicons/react/24/outline' 5 | import { shortenAddress } from '@/utils/blockchainUtils' 6 | import { useAccount } from 'wagmi' 7 | 8 | const CustomConnectButton = () => { 9 | return ( 10 | 11 | {({ 12 | account, 13 | chain, 14 | openAccountModal, 15 | openChainModal, 16 | openConnectModal, 17 | mounted, 18 | }) => { 19 | const ready = mounted 20 | const connected = ready && account && chain 21 | 22 | return ( 23 |
33 | {(() => { 34 | if (!connected) { 35 | return ( 36 | 39 | ) 40 | } 41 | 42 | if (chain.unsupported) { 43 | return ( 44 | 47 | ) 48 | } 49 | 50 | return ( 51 |
52 | 79 | 80 | 86 |
87 | ) 88 | })()} 89 |
90 | ) 91 | }} 92 |
93 | ) 94 | } 95 | 96 | const Navbar: React.FC = () => { 97 | const [isOpen, setIsOpen] = useState(false) 98 | const { address } = useAccount() 99 | const [showPopup, setShowPopup] = useState(false) 100 | 101 | const handleProfileClick = (e: React.MouseEvent) => { 102 | if (!address) { 103 | e.preventDefault() 104 | setShowPopup(true) 105 | setTimeout(() => setShowPopup(false), 3000) // Hide popup after 3 seconds 106 | } 107 | } 108 | 109 | return ( 110 | 187 | ) 188 | } 189 | 190 | export default Navbar -------------------------------------------------------------------------------- /src/components/tokens/TokenCard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import Link from 'next/link'; 3 | import Image from 'next/image'; 4 | import { CurrencyDollarIcon, UserIcon, ClockIcon, TagIcon } from '@heroicons/react/24/outline'; 5 | import { Token, TokenWithLiquidityEvents } from '@/interface/types'; 6 | import { useTokenLiquidity, formatAmount, formatTimestamp, formatAmountV2 } from '@/utils/blockchainUtils'; 7 | import Spinner from '@/components/ui/Spinner'; 8 | import { useRouter } from 'next/router'; 9 | 10 | interface TokenCardProps { 11 | token: Token | TokenWithLiquidityEvents; 12 | isEnded: boolean; 13 | } 14 | 15 | const TokenCard: React.FC = ({ token, isEnded }) => { 16 | const [isLoading, setIsLoading] = useState(false); 17 | const router = useRouter(); 18 | const [currentLiquidity, setCurrentLiquidity] = useState('0'); 19 | const tokenAddress = token.address as `0x${string}`; 20 | const { data: liquidityData } = useTokenLiquidity(tokenAddress); 21 | 22 | useEffect(() => { 23 | if (liquidityData && liquidityData[2]) { 24 | setCurrentLiquidity(liquidityData[2].toString()); 25 | } 26 | }, [liquidityData]); 27 | 28 | const isTokenWithLiquidity = (token: Token | TokenWithLiquidityEvents): token is TokenWithLiquidityEvents => { 29 | return 'liquidityEvents' in token && token.liquidityEvents.length > 0; 30 | }; 31 | 32 | const handleClick = () => { 33 | setIsLoading(true); 34 | }; 35 | 36 | const handleViewCharts = (e: React.MouseEvent) => { 37 | e.preventDefault(); 38 | setIsLoading(true); 39 | router.push(`/token/${token.address}`); 40 | }; 41 | 42 | const handleProfileClick = (e: React.MouseEvent) => { 43 | e.preventDefault(); 44 | e.stopPropagation(); 45 | router.push(`/profile/${token.creatorAddress}`); 46 | }; 47 | 48 | if (isEnded && isTokenWithLiquidity(token)) { 49 | const liquidityEvent = token.liquidityEvents[0]; 50 | const uniswapLink = `https://chewyswap.dog/swap/?outputCurrency=${token.address}&chain=shibarium`; 51 | 52 | return ( 53 |
54 | {isLoading && ( 55 |
56 | 57 |
58 | )} 59 |
60 |
61 | {`${token.name} 62 |
63 |
64 |

{token.name}

65 |
66 | 67 | Listed on Chewswap 68 |
69 |
70 |
71 |
72 |
73 | Initial Liquidity Added 74 |
75 |
76 | {token.symbol} 77 | {formatAmountV2(liquidityEvent.tokenAmount)} 78 |
79 |
80 | BONE 81 | {formatAmountV2(liquidityEvent.ethAmount)} 82 |
83 |
84 | Listed 85 | {formatTimestamp(liquidityEvent.timestamp)} 86 |
87 |
88 | {/* Current Liquidity 89 | {formatAmount(currentLiquidity)} */} 90 |
91 |
92 | 109 |
110 | ); 111 | } 112 | 113 | return ( 114 | 115 |
116 | {isLoading && ( 117 |
118 | 119 |
120 | )} 121 |
122 | {token.name} 123 |
124 |
125 |
126 |
127 | {token.name} 128 |
129 |
130 |

{token.name}

131 |

{token.symbol}

132 |
133 |
134 |
135 |
136 | 137 | 138 | Liquidity:{'\u00A0'} 139 | BONE 146 | {formatAmountV2(currentLiquidity)} 147 | 148 |
149 |
150 | 151 | 152 | Deployed by{' '} 153 | 157 | {token.creatorAddress ? `${token.creatorAddress.slice(-6)}` : 'Unknown'} 158 | 159 | 160 |
161 |
162 | 163 | Created {formatTimestamp(token.createdAt)} 164 |
165 |
166 |
167 |
168 | 169 | ); 170 | }; 171 | 172 | export default TokenCard; 173 | -------------------------------------------------------------------------------- /src/abi/ERC20.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "string", 6 | "name": "name", 7 | "type": "string" 8 | }, 9 | { 10 | "internalType": "string", 11 | "name": "symbol", 12 | "type": "string" 13 | } 14 | ], 15 | "stateMutability": "nonpayable", 16 | "type": "constructor" 17 | }, 18 | { 19 | "anonymous": false, 20 | "inputs": [ 21 | { 22 | "indexed": true, 23 | "internalType": "address", 24 | "name": "owner", 25 | "type": "address" 26 | }, 27 | { 28 | "indexed": true, 29 | "internalType": "address", 30 | "name": "spender", 31 | "type": "address" 32 | }, 33 | { 34 | "indexed": false, 35 | "internalType": "uint256", 36 | "name": "value", 37 | "type": "uint256" 38 | } 39 | ], 40 | "name": "Approval", 41 | "type": "event" 42 | }, 43 | { 44 | "anonymous": false, 45 | "inputs": [ 46 | { 47 | "indexed": true, 48 | "internalType": "address", 49 | "name": "previousOwner", 50 | "type": "address" 51 | }, 52 | { 53 | "indexed": true, 54 | "internalType": "address", 55 | "name": "newOwner", 56 | "type": "address" 57 | } 58 | ], 59 | "name": "OwnershipTransferred", 60 | "type": "event" 61 | }, 62 | { 63 | "anonymous": false, 64 | "inputs": [ 65 | { 66 | "indexed": true, 67 | "internalType": "address", 68 | "name": "from", 69 | "type": "address" 70 | }, 71 | { 72 | "indexed": true, 73 | "internalType": "address", 74 | "name": "to", 75 | "type": "address" 76 | }, 77 | { 78 | "indexed": false, 79 | "internalType": "uint256", 80 | "name": "value", 81 | "type": "uint256" 82 | } 83 | ], 84 | "name": "Transfer", 85 | "type": "event" 86 | }, 87 | { 88 | "inputs": [ 89 | { 90 | "internalType": "address", 91 | "name": "owner", 92 | "type": "address" 93 | }, 94 | { 95 | "internalType": "address", 96 | "name": "spender", 97 | "type": "address" 98 | } 99 | ], 100 | "name": "allowance", 101 | "outputs": [ 102 | { 103 | "internalType": "uint256", 104 | "name": "", 105 | "type": "uint256" 106 | } 107 | ], 108 | "stateMutability": "view", 109 | "type": "function" 110 | }, 111 | { 112 | "inputs": [ 113 | { 114 | "internalType": "address", 115 | "name": "spender", 116 | "type": "address" 117 | }, 118 | { 119 | "internalType": "uint256", 120 | "name": "amount", 121 | "type": "uint256" 122 | } 123 | ], 124 | "name": "approve", 125 | "outputs": [ 126 | { 127 | "internalType": "bool", 128 | "name": "", 129 | "type": "bool" 130 | } 131 | ], 132 | "stateMutability": "nonpayable", 133 | "type": "function" 134 | }, 135 | { 136 | "inputs": [ 137 | { 138 | "internalType": "address", 139 | "name": "account", 140 | "type": "address" 141 | } 142 | ], 143 | "name": "balanceOf", 144 | "outputs": [ 145 | { 146 | "internalType": "uint256", 147 | "name": "", 148 | "type": "uint256" 149 | } 150 | ], 151 | "stateMutability": "view", 152 | "type": "function" 153 | }, 154 | { 155 | "inputs": [ 156 | { 157 | "internalType": "uint256", 158 | "name": "amount", 159 | "type": "uint256" 160 | } 161 | ], 162 | "name": "burn", 163 | "outputs": [], 164 | "stateMutability": "nonpayable", 165 | "type": "function" 166 | }, 167 | { 168 | "inputs": [ 169 | { 170 | "internalType": "address", 171 | "name": "account", 172 | "type": "address" 173 | }, 174 | { 175 | "internalType": "uint256", 176 | "name": "amount", 177 | "type": "uint256" 178 | } 179 | ], 180 | "name": "burnFrom", 181 | "outputs": [], 182 | "stateMutability": "nonpayable", 183 | "type": "function" 184 | }, 185 | { 186 | "inputs": [], 187 | "name": "decimals", 188 | "outputs": [ 189 | { 190 | "internalType": "uint8", 191 | "name": "", 192 | "type": "uint8" 193 | } 194 | ], 195 | "stateMutability": "view", 196 | "type": "function" 197 | }, 198 | { 199 | "inputs": [ 200 | { 201 | "internalType": "address", 202 | "name": "spender", 203 | "type": "address" 204 | }, 205 | { 206 | "internalType": "uint256", 207 | "name": "subtractedValue", 208 | "type": "uint256" 209 | } 210 | ], 211 | "name": "decreaseAllowance", 212 | "outputs": [ 213 | { 214 | "internalType": "bool", 215 | "name": "", 216 | "type": "bool" 217 | } 218 | ], 219 | "stateMutability": "nonpayable", 220 | "type": "function" 221 | }, 222 | { 223 | "inputs": [ 224 | { 225 | "internalType": "address", 226 | "name": "spender", 227 | "type": "address" 228 | }, 229 | { 230 | "internalType": "uint256", 231 | "name": "addedValue", 232 | "type": "uint256" 233 | } 234 | ], 235 | "name": "increaseAllowance", 236 | "outputs": [ 237 | { 238 | "internalType": "bool", 239 | "name": "", 240 | "type": "bool" 241 | } 242 | ], 243 | "stateMutability": "nonpayable", 244 | "type": "function" 245 | }, 246 | { 247 | "inputs": [ 248 | { 249 | "internalType": "address", 250 | "name": "to", 251 | "type": "address" 252 | }, 253 | { 254 | "internalType": "uint256", 255 | "name": "amount", 256 | "type": "uint256" 257 | } 258 | ], 259 | "name": "mint", 260 | "outputs": [], 261 | "stateMutability": "nonpayable", 262 | "type": "function" 263 | }, 264 | { 265 | "inputs": [], 266 | "name": "name", 267 | "outputs": [ 268 | { 269 | "internalType": "string", 270 | "name": "", 271 | "type": "string" 272 | } 273 | ], 274 | "stateMutability": "view", 275 | "type": "function" 276 | }, 277 | { 278 | "inputs": [], 279 | "name": "owner", 280 | "outputs": [ 281 | { 282 | "internalType": "address", 283 | "name": "", 284 | "type": "address" 285 | } 286 | ], 287 | "stateMutability": "view", 288 | "type": "function" 289 | }, 290 | { 291 | "inputs": [], 292 | "name": "renounceOwnership", 293 | "outputs": [], 294 | "stateMutability": "nonpayable", 295 | "type": "function" 296 | }, 297 | { 298 | "inputs": [], 299 | "name": "symbol", 300 | "outputs": [ 301 | { 302 | "internalType": "string", 303 | "name": "", 304 | "type": "string" 305 | } 306 | ], 307 | "stateMutability": "view", 308 | "type": "function" 309 | }, 310 | { 311 | "inputs": [], 312 | "name": "totalSupply", 313 | "outputs": [ 314 | { 315 | "internalType": "uint256", 316 | "name": "", 317 | "type": "uint256" 318 | } 319 | ], 320 | "stateMutability": "view", 321 | "type": "function" 322 | }, 323 | { 324 | "inputs": [ 325 | { 326 | "internalType": "address", 327 | "name": "to", 328 | "type": "address" 329 | }, 330 | { 331 | "internalType": "uint256", 332 | "name": "amount", 333 | "type": "uint256" 334 | } 335 | ], 336 | "name": "transfer", 337 | "outputs": [ 338 | { 339 | "internalType": "bool", 340 | "name": "", 341 | "type": "bool" 342 | } 343 | ], 344 | "stateMutability": "nonpayable", 345 | "type": "function" 346 | }, 347 | { 348 | "inputs": [ 349 | { 350 | "internalType": "address", 351 | "name": "from", 352 | "type": "address" 353 | }, 354 | { 355 | "internalType": "address", 356 | "name": "to", 357 | "type": "address" 358 | }, 359 | { 360 | "internalType": "uint256", 361 | "name": "amount", 362 | "type": "uint256" 363 | } 364 | ], 365 | "name": "transferFrom", 366 | "outputs": [ 367 | { 368 | "internalType": "bool", 369 | "name": "", 370 | "type": "bool" 371 | } 372 | ], 373 | "stateMutability": "nonpayable", 374 | "type": "function" 375 | }, 376 | { 377 | "inputs": [ 378 | { 379 | "internalType": "address", 380 | "name": "newOwner", 381 | "type": "address" 382 | } 383 | ], 384 | "name": "transferOwnership", 385 | "outputs": [], 386 | "stateMutability": "nonpayable", 387 | "type": "function" 388 | } 389 | ] -------------------------------------------------------------------------------- /src/components/charts/TradingViewChart.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from 'react'; 2 | import { createChart, CrosshairMode, IChartApi, Time } from 'lightweight-charts'; 3 | import Image from 'next/image'; 4 | import { formatAmountV3 } from '@/utils/blockchainUtils'; 5 | import Spinner from '@/components/ui/Spinner'; 6 | 7 | // TODO: add different chart types (bars, line, area, etc) 8 | 9 | interface ChartDataPoint { 10 | time: number; 11 | open: number; 12 | high: number; 13 | low: number; 14 | close: number; 15 | } 16 | 17 | interface PriceChartProps { 18 | data: ChartDataPoint[]; 19 | liquidityEvents: any; 20 | tokenInfo: any; 21 | } 22 | 23 | const PriceChart: React.FC = ({ data, liquidityEvents, tokenInfo }) => { 24 | const chartContainerRef = useRef(null); 25 | const [chart, setChart] = useState(null); 26 | const [showUniswapInfo, setShowUniswapInfo] = useState(null); 27 | 28 | useEffect(() => { 29 | if (liquidityEvents) { 30 | setShowUniswapInfo(liquidityEvents.liquidityEvents.length > 0); 31 | } 32 | }, [liquidityEvents]); 33 | 34 | useEffect(() => { 35 | if (chartContainerRef.current && data.length >= 2 && showUniswapInfo === false) { 36 | const newChart: IChartApi = createChart(chartContainerRef.current, { 37 | width: chartContainerRef.current.clientWidth, 38 | height: 500, 39 | layout: { 40 | background: { color: '#1f2937' }, 41 | textColor: '#d1d5db', 42 | }, 43 | grid: { 44 | vertLines: { color: 'rgba(255, 255, 255, 0.1)' }, 45 | horzLines: { color: 'rgba(255, 255, 255, 0.1)' }, 46 | }, 47 | rightPriceScale: { 48 | borderColor: 'rgba(255, 255, 255, 0.2)', 49 | visible: true, 50 | borderVisible: true, 51 | alignLabels: true, 52 | scaleMargins: { 53 | top: 0.1, 54 | bottom: 0.1, 55 | }, 56 | autoScale: false, 57 | }, 58 | timeScale: { 59 | borderColor: 'rgba(255, 255, 255, 0.2)', 60 | timeVisible: true, 61 | secondsVisible: false, 62 | }, 63 | crosshair: { 64 | mode: CrosshairMode.Normal, 65 | }, 66 | watermark: { 67 | color: 'rgba(255, 255, 255, 0.1)', 68 | visible: true, 69 | text: 'Bondle.xyz', 70 | fontSize: 28, 71 | horzAlign: 'center', 72 | vertAlign: 'center', 73 | }, 74 | }); 75 | 76 | const candleSeries = newChart.addCandlestickSeries({ 77 | upColor: '#26a69a', 78 | downColor: '#ef5350', 79 | borderVisible: false, 80 | wickUpColor: '#26a69a', 81 | wickDownColor: '#ef5350' 82 | }); 83 | 84 | const enhancedChartData = enhanceSmallCandles(data); 85 | candleSeries.setData(enhancedChartData.map(item => ({ 86 | time: item.time as Time, 87 | open: item.open, 88 | high: item.high, 89 | low: item.low, 90 | close: item.close 91 | }))); 92 | 93 | candleSeries.applyOptions({ 94 | priceFormat: { 95 | type: 'custom', 96 | formatter: formatPrice, 97 | minMove: 1e-9, 98 | }, 99 | }); 100 | 101 | const prices = enhancedChartData.flatMap(item => [item.open, item.high, item.low, item.close]); 102 | const minPrice = Math.min(...prices); 103 | const maxPrice = Math.max(...prices); 104 | 105 | const zoomFactor = 0.8; 106 | const priceRange = maxPrice - minPrice; 107 | const zoomedMinPrice = Math.max(0, minPrice - priceRange * (1 - zoomFactor) / 2); 108 | const zoomedMaxPrice = maxPrice + priceRange * (1 - zoomFactor) / 2; 109 | 110 | newChart.priceScale('right').applyOptions({ 111 | autoScale: false, 112 | scaleMargins: { 113 | top: 0.1, 114 | bottom: 0.1, 115 | }, 116 | }); 117 | 118 | newChart.timeScale().setVisibleRange({ 119 | from: enhancedChartData[0].time as Time, 120 | to: enhancedChartData[enhancedChartData.length - 1].time as Time, 121 | }); 122 | 123 | setChart(newChart); 124 | 125 | return () => { 126 | newChart.remove(); 127 | }; 128 | } 129 | }, [data, showUniswapInfo]); 130 | 131 | useEffect(() => { 132 | const handleResize = () => { 133 | if (chart && chartContainerRef.current) { 134 | chart.applyOptions({ width: chartContainerRef.current.clientWidth }); 135 | } 136 | }; 137 | 138 | window.addEventListener('resize', handleResize); 139 | 140 | return () => { 141 | window.removeEventListener('resize', handleResize); 142 | }; 143 | }, [chart]); 144 | 145 | if (showUniswapInfo === null) { 146 | return ( 147 |
148 | 149 |
150 | ); 151 | } 152 | 153 | if (showUniswapInfo && liquidityEvents.liquidityEvents.length > 0) { 154 | const event = liquidityEvents.liquidityEvents[0]; 155 | return ( 156 |
157 | {tokenInfo.name} 158 |

{tokenInfo.name} Listed on Chewyswap

159 |
160 |
161 |
162 |

Token

163 |

{formatAmountV3(event.tokenAmount)} {tokenInfo.symbol}

164 |
165 |
166 |

BONE

167 |

{formatAmountV3(event.ethAmount)} BONE

168 |
169 |
170 | 188 |
189 | ); 190 | } 191 | 192 | if (data.length < 2) { 193 | return ( 194 |
195 |

Not enough data to display chart

196 |
197 | ); 198 | } 199 | 200 | return ( 201 |
202 | ); 203 | }; 204 | 205 | function enhanceSmallCandles(data: ChartDataPoint[]): ChartDataPoint[] { 206 | const minCandleSize = 1e-9; 207 | return data.map(item => { 208 | const bodySize = Math.abs(item.open - item.close); 209 | if (bodySize < minCandleSize) { 210 | const midPoint = (item.open + item.close) / 2; 211 | const adjustment = minCandleSize / 2; 212 | return { 213 | ...item, 214 | open: midPoint - adjustment, 215 | close: midPoint + adjustment, 216 | high: Math.max(item.high, midPoint + adjustment), 217 | low: Math.min(item.low, midPoint - adjustment) 218 | }; 219 | } 220 | return item; 221 | }); 222 | } 223 | 224 | function formatPrice(price: number) { 225 | return price.toFixed(9); 226 | } 227 | 228 | export default PriceChart; -------------------------------------------------------------------------------- /src/utils/api.ts: -------------------------------------------------------------------------------- 1 | // api.ts 2 | 3 | import axios from 'axios'; 4 | import { Token, TokenWithLiquidityEvents, PaginatedResponse, LiquidityEvent, TokenWithTransactions, PriceResponse, HistoricalPrice, USDHistoricalPrice, TokenHolder, TransactionResponse } from '@/interface/types'; 5 | import { ethers } from 'ethers'; 6 | 7 | 8 | const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL; 9 | 10 | export async function getAllTokens(page: number = 1, pageSize: number = 13): Promise> { 11 | const response = await axios.get(`${API_BASE_URL}/api/tokens`, { 12 | params: { page, pageSize } 13 | }); 14 | return response.data; 15 | } 16 | 17 | 18 | export async function getRecentTokens(page: number = 1, pageSize: number = 20, hours: number = 1): Promise | null> { 19 | try { 20 | const response = await axios.get(`${API_BASE_URL}/api/tokens/recent`, { 21 | params: { page, pageSize, hours } 22 | }); 23 | return response.data; 24 | } catch (error) { 25 | if (axios.isAxiosError(error) && error.response?.status === 404) { 26 | // Return null to indicate no recent tokens found 27 | return null; 28 | } 29 | throw error; // Re-throw other errors 30 | } 31 | } 32 | 33 | export async function searchTokens( 34 | query: string, 35 | page: number = 1, 36 | pageSize: number = 20 37 | ): Promise> { 38 | try { 39 | const response = await axios.get(`${API_BASE_URL}/api/tokens/search`, { 40 | params: { q: query, page, pageSize } 41 | }); 42 | return response.data; 43 | } catch (error) { 44 | console.error('Error searching tokens:', error); 45 | throw new Error('Failed to search tokens'); 46 | } 47 | } 48 | 49 | export async function getTokensWithLiquidity(page: number = 1, pageSize: number = 20): Promise> { 50 | const response = await axios.get(`${API_BASE_URL}/api/tokens/with-liquidityEvent`, { 51 | params: { page, pageSize } 52 | }); 53 | return response.data; 54 | } 55 | 56 | export async function getTokenByAddress(address: string): Promise { 57 | const response = await axios.get(`${API_BASE_URL}/api/tokens/address/${address}`); 58 | return response.data; 59 | } 60 | 61 | export async function getTokenLiquidityEvents(tokenId: string, page: number = 1, pageSize: number = 20): Promise> { 62 | const response = await axios.get(`${API_BASE_URL}/api/liquidity/token/${tokenId}`, { 63 | params: { page, pageSize } 64 | }); 65 | return response.data; 66 | } 67 | 68 | export async function getTokenInfoAndTransactions( 69 | address: string, 70 | transactionPage: number = 1, 71 | transactionPageSize: number = 10 72 | ): Promise { 73 | const response = await axios.get(`${API_BASE_URL}/api/tokens/address/${address}/info-and-transactions`, { 74 | params: { transactionPage, transactionPageSize } 75 | }); 76 | return response.data; 77 | } 78 | 79 | 80 | //historical price 81 | export async function getHistoricalPriceData(address: string): Promise { 82 | const response = await axios.get(`${API_BASE_URL}/api/tokens/address/${address}/historical-prices`); 83 | return response.data; 84 | } 85 | 86 | //eth price usd 87 | export async function getCurrentPrice(): Promise { 88 | try { 89 | const response = await axios.get(`${API_BASE_URL}/api/price`); 90 | return response.data.price; 91 | } catch (error) { 92 | console.error('Error fetching current price:', error); 93 | throw new Error('Failed to fetch current price'); 94 | } 95 | } 96 | 97 | 98 | export async function getTokenUSDPriceHistory(address: string): Promise { 99 | try { 100 | const [ethPrice, historicalPrices] = await Promise.all([ 101 | getCurrentPrice(), 102 | getHistoricalPriceData(address) 103 | ]); 104 | 105 | return historicalPrices.map((price: HistoricalPrice) => { 106 | const tokenPriceInWei = ethers.BigNumber.from(price.tokenPrice); 107 | const tokenPriceInETH = ethers.utils.formatEther(tokenPriceInWei); 108 | const tokenPriceUSD = parseFloat(tokenPriceInETH) * parseFloat(ethPrice); 109 | 110 | return { 111 | tokenPriceUSD: tokenPriceUSD.toFixed(9), // Adjust decimal places as needed 112 | timestamp: price.timestamp 113 | }; 114 | }); 115 | } catch (error) { 116 | console.error('Error calculating USD price history:', error); 117 | throw new Error('Failed to calculate USD price history'); 118 | } 119 | } 120 | 121 | 122 | export async function updateToken( 123 | address: string, 124 | data: { 125 | logo?: string; 126 | description?: string; 127 | website?: string; 128 | telegram?: string; 129 | discord?: string; 130 | twitter?: string; 131 | youtube?: string; 132 | } 133 | ): Promise { 134 | try { 135 | const response = await axios.patch(`${API_BASE_URL}/api/tokens/update/${address}`, data); 136 | return response.data; 137 | } catch (error) { 138 | console.error('Error updating token:', error); 139 | throw new Error('Failed to update token'); 140 | } 141 | } 142 | 143 | // get all transaction associated with a particular address 144 | export async function getTransactionsByAddress( 145 | address: string, 146 | page: number = 1, 147 | pageSize: number = 10 148 | ): Promise { 149 | try { 150 | const response = await axios.get(`${API_BASE_URL}/api/transactions/address/${address}`, { 151 | params: { page, pageSize } 152 | }); 153 | return response.data; 154 | } catch (error) { 155 | console.error('Error fetching transactions:', error); 156 | throw new Error('Failed to fetch transactions'); 157 | } 158 | } 159 | 160 | // POST /chats: Add a new chat message with optional reply_to 161 | export async function addChatMessage( 162 | user: string, 163 | token: string, 164 | message: string, 165 | replyTo?: number 166 | ): Promise<{ id: number }> { 167 | try { 168 | const response = await axios.post(`${API_BASE_URL}/chats`, { 169 | user, 170 | token, 171 | message, 172 | reply_to: replyTo // Optional: ID of the message being replied to 173 | }); 174 | return response.data; 175 | } catch (error) { 176 | console.error('Error adding chat message:', error); 177 | throw new Error('Failed to add chat message'); 178 | } 179 | } 180 | 181 | // GET /chats: Get chat messages for a specific token 182 | export async function getChatMessages(token: string): Promise> { 190 | try { 191 | const response = await axios.get(`${API_BASE_URL}/chats`, { 192 | params: { token } 193 | }); 194 | return response.data; 195 | } catch (error) { 196 | console.error('Error fetching chat messages:', error); 197 | throw new Error('Failed to fetch chat messages'); 198 | } 199 | } 200 | 201 | //get all token address 202 | export async function getAllTokenAddresses(): Promise> { 203 | try { 204 | const response = await axios.get(`${API_BASE_URL}/api/tokens/addresses`); 205 | return response.data; 206 | } catch (error) { 207 | console.error('Error fetching token addresses and symbols:', error); 208 | throw new Error('Failed to fetch token addresses and symbols'); 209 | } 210 | } 211 | 212 | export async function getTokensByCreator( 213 | creatorAddress: string, 214 | page: number = 1, 215 | pageSize: number = 20 216 | ): Promise> { 217 | try { 218 | const response = await axios.get(`${API_BASE_URL}/api/tokens/creator/${creatorAddress}`, { 219 | params: { page, pageSize } 220 | }); 221 | return response.data; 222 | } catch (error) { 223 | console.error('Error fetching tokens by creator:', error); 224 | throw new Error('Failed to fetch tokens by creator'); 225 | } 226 | } 227 | 228 | 229 | //blockexplorer Get token Holders 230 | export async function getTokenHolders(tokenAddress: string): Promise { 231 | try { 232 | const response = await axios.get(`https://www.shibariumscan.io/api/v2/tokens/${tokenAddress}/holders`); 233 | const data = response.data; 234 | 235 | return data.items.map((item: any) => { 236 | return { 237 | address: item.address.hash, 238 | balance: item.value 239 | }; 240 | }); 241 | } catch (error) { 242 | console.error('Error fetching token holders:', error); 243 | throw new Error('Failed to fetch token holders'); 244 | } 245 | } 246 | 247 | -------------------------------------------------------------------------------- /src/abi/BondingCurveManager.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "address", 6 | "name": "_uniRouter", 7 | "type": "address" 8 | } 9 | ], 10 | "stateMutability": "nonpayable", 11 | "type": "constructor" 12 | }, 13 | { 14 | "inputs": [], 15 | "name": "FailedToSendEth", 16 | "type": "error" 17 | }, 18 | { 19 | "inputs": [], 20 | "name": "IncorrectCreationFee", 21 | "type": "error" 22 | }, 23 | { 24 | "inputs": [], 25 | "name": "NoFeesToWithdraw", 26 | "type": "error" 27 | }, 28 | { 29 | "inputs": [], 30 | "name": "TokenAlreadyListed", 31 | "type": "error" 32 | }, 33 | { 34 | "inputs": [], 35 | "name": "TokenDoesNotExist", 36 | "type": "error" 37 | }, 38 | { 39 | "inputs": [], 40 | "name": "ZeroEthSent", 41 | "type": "error" 42 | }, 43 | { 44 | "inputs": [], 45 | "name": "ZeroTokenAmount", 46 | "type": "error" 47 | }, 48 | { 49 | "anonymous": false, 50 | "inputs": [ 51 | { 52 | "indexed": true, 53 | "internalType": "address", 54 | "name": "token", 55 | "type": "address" 56 | }, 57 | { 58 | "indexed": false, 59 | "internalType": "uint256", 60 | "name": "ethAmount", 61 | "type": "uint256" 62 | }, 63 | { 64 | "indexed": false, 65 | "internalType": "uint256", 66 | "name": "tokenAmount", 67 | "type": "uint256" 68 | } 69 | ], 70 | "name": "LiquidityAdded", 71 | "type": "event" 72 | }, 73 | { 74 | "anonymous": false, 75 | "inputs": [ 76 | { 77 | "indexed": true, 78 | "internalType": "address", 79 | "name": "previousOwner", 80 | "type": "address" 81 | }, 82 | { 83 | "indexed": true, 84 | "internalType": "address", 85 | "name": "newOwner", 86 | "type": "address" 87 | } 88 | ], 89 | "name": "OwnershipTransferred", 90 | "type": "event" 91 | }, 92 | { 93 | "anonymous": false, 94 | "inputs": [ 95 | { 96 | "indexed": true, 97 | "internalType": "address", 98 | "name": "tokenAddress", 99 | "type": "address" 100 | }, 101 | { 102 | "indexed": true, 103 | "internalType": "address", 104 | "name": "creator", 105 | "type": "address" 106 | }, 107 | { 108 | "indexed": false, 109 | "internalType": "string", 110 | "name": "name", 111 | "type": "string" 112 | }, 113 | { 114 | "indexed": false, 115 | "internalType": "string", 116 | "name": "symbol", 117 | "type": "string" 118 | } 119 | ], 120 | "name": "TokenCreated", 121 | "type": "event" 122 | }, 123 | { 124 | "anonymous": false, 125 | "inputs": [ 126 | { 127 | "indexed": true, 128 | "internalType": "address", 129 | "name": "token", 130 | "type": "address" 131 | }, 132 | { 133 | "indexed": true, 134 | "internalType": "address", 135 | "name": "buyer", 136 | "type": "address" 137 | }, 138 | { 139 | "indexed": false, 140 | "internalType": "uint256", 141 | "name": "ethAmount", 142 | "type": "uint256" 143 | }, 144 | { 145 | "indexed": false, 146 | "internalType": "uint256", 147 | "name": "tokenAmount", 148 | "type": "uint256" 149 | } 150 | ], 151 | "name": "TokensBought", 152 | "type": "event" 153 | }, 154 | { 155 | "anonymous": false, 156 | "inputs": [ 157 | { 158 | "indexed": true, 159 | "internalType": "address", 160 | "name": "token", 161 | "type": "address" 162 | }, 163 | { 164 | "indexed": true, 165 | "internalType": "address", 166 | "name": "seller", 167 | "type": "address" 168 | }, 169 | { 170 | "indexed": false, 171 | "internalType": "uint256", 172 | "name": "tokenAmount", 173 | "type": "uint256" 174 | }, 175 | { 176 | "indexed": false, 177 | "internalType": "uint256", 178 | "name": "ethAmount", 179 | "type": "uint256" 180 | } 181 | ], 182 | "name": "TokensSold", 183 | "type": "event" 184 | }, 185 | { 186 | "inputs": [ 187 | { 188 | "internalType": "address", 189 | "name": "tokenAddress", 190 | "type": "address" 191 | } 192 | ], 193 | "name": "buy", 194 | "outputs": [], 195 | "stateMutability": "payable", 196 | "type": "function" 197 | }, 198 | { 199 | "inputs": [ 200 | { 201 | "internalType": "address", 202 | "name": "tokenAddress", 203 | "type": "address" 204 | }, 205 | { 206 | "internalType": "uint256", 207 | "name": "ethAmount", 208 | "type": "uint256" 209 | } 210 | ], 211 | "name": "calculateCurvedBuyReturn", 212 | "outputs": [ 213 | { 214 | "internalType": "uint256", 215 | "name": "", 216 | "type": "uint256" 217 | } 218 | ], 219 | "stateMutability": "view", 220 | "type": "function" 221 | }, 222 | { 223 | "inputs": [ 224 | { 225 | "internalType": "address", 226 | "name": "tokenAddress", 227 | "type": "address" 228 | }, 229 | { 230 | "internalType": "uint256", 231 | "name": "tokenAmount", 232 | "type": "uint256" 233 | } 234 | ], 235 | "name": "calculateCurvedSellReturn", 236 | "outputs": [ 237 | { 238 | "internalType": "uint256", 239 | "name": "", 240 | "type": "uint256" 241 | } 242 | ], 243 | "stateMutability": "view", 244 | "type": "function" 245 | }, 246 | { 247 | "inputs": [ 248 | { 249 | "internalType": "string", 250 | "name": "name", 251 | "type": "string" 252 | }, 253 | { 254 | "internalType": "string", 255 | "name": "symbol", 256 | "type": "string" 257 | } 258 | ], 259 | "name": "create", 260 | "outputs": [], 261 | "stateMutability": "payable", 262 | "type": "function" 263 | }, 264 | { 265 | "inputs": [ 266 | { 267 | "internalType": "address", 268 | "name": "tokenAddress", 269 | "type": "address" 270 | } 271 | ], 272 | "name": "getCurrentTokenPrice", 273 | "outputs": [ 274 | { 275 | "internalType": "uint256", 276 | "name": "", 277 | "type": "uint256" 278 | } 279 | ], 280 | "stateMutability": "view", 281 | "type": "function" 282 | }, 283 | { 284 | "inputs": [ 285 | { 286 | "internalType": "address", 287 | "name": "tokenAddress", 288 | "type": "address" 289 | } 290 | ], 291 | "name": "getTokenEthBalance", 292 | "outputs": [ 293 | { 294 | "internalType": "uint256", 295 | "name": "", 296 | "type": "uint256" 297 | } 298 | ], 299 | "stateMutability": "view", 300 | "type": "function" 301 | }, 302 | { 303 | "inputs": [], 304 | "name": "owner", 305 | "outputs": [ 306 | { 307 | "internalType": "address", 308 | "name": "", 309 | "type": "address" 310 | } 311 | ], 312 | "stateMutability": "view", 313 | "type": "function" 314 | }, 315 | { 316 | "inputs": [], 317 | "name": "renounceOwnership", 318 | "outputs": [], 319 | "stateMutability": "nonpayable", 320 | "type": "function" 321 | }, 322 | { 323 | "inputs": [ 324 | { 325 | "internalType": "address", 326 | "name": "tokenAddress", 327 | "type": "address" 328 | }, 329 | { 330 | "internalType": "uint256", 331 | "name": "tokenAmount", 332 | "type": "uint256" 333 | } 334 | ], 335 | "name": "sell", 336 | "outputs": [], 337 | "stateMutability": "nonpayable", 338 | "type": "function" 339 | }, 340 | { 341 | "inputs": [ 342 | { 343 | "internalType": "uint256", 344 | "name": "", 345 | "type": "uint256" 346 | } 347 | ], 348 | "name": "tokenList", 349 | "outputs": [ 350 | { 351 | "internalType": "address", 352 | "name": "", 353 | "type": "address" 354 | } 355 | ], 356 | "stateMutability": "view", 357 | "type": "function" 358 | }, 359 | { 360 | "inputs": [ 361 | { 362 | "internalType": "address", 363 | "name": "", 364 | "type": "address" 365 | } 366 | ], 367 | "name": "tokens", 368 | "outputs": [ 369 | { 370 | "internalType": "contract BondingCurveToken", 371 | "name": "token", 372 | "type": "address" 373 | }, 374 | { 375 | "internalType": "bool", 376 | "name": "isListed", 377 | "type": "bool" 378 | }, 379 | { 380 | "internalType": "uint256", 381 | "name": "ethBalance", 382 | "type": "uint256" 383 | } 384 | ], 385 | "stateMutability": "view", 386 | "type": "function" 387 | }, 388 | { 389 | "inputs": [ 390 | { 391 | "internalType": "address", 392 | "name": "newOwner", 393 | "type": "address" 394 | } 395 | ], 396 | "name": "transferOwnership", 397 | "outputs": [], 398 | "stateMutability": "nonpayable", 399 | "type": "function" 400 | }, 401 | { 402 | "inputs": [], 403 | "name": "withdrawFees", 404 | "outputs": [], 405 | "stateMutability": "nonpayable", 406 | "type": "function" 407 | }, 408 | { 409 | "stateMutability": "payable", 410 | "type": "receive" 411 | } 412 | ] -------------------------------------------------------------------------------- /src/components/TokenDetails/Chats.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useMemo } from 'react'; 2 | import Image from 'next/image'; 3 | import { getChatMessages, addChatMessage } from '@/utils/api'; 4 | import { useAccount } from 'wagmi'; 5 | import { toast } from 'react-toastify'; 6 | import { formatTimestamp, getRandomAvatarImage, shortenAddress } from '@/utils/chatUtils'; 7 | import { motion, AnimatePresence } from 'framer-motion'; 8 | import { TokenWithTransactions } from '@/interface/types'; 9 | import SiweAuth from '@/components/auth/SiweAuth'; 10 | 11 | interface ChatMessage { 12 | id: number; 13 | user: string; 14 | token: string; 15 | message: string; 16 | reply_to: number | null; 17 | timestamp: string; 18 | } 19 | 20 | interface ChatsProps { 21 | tokenAddress: string; 22 | tokenInfo: TokenWithTransactions; 23 | } 24 | 25 | const Chats: React.FC = ({ tokenAddress, tokenInfo }) => { 26 | const [messages, setMessages] = useState([]); 27 | const [newMessage, setNewMessage] = useState(''); 28 | const [isPopupOpen, setIsPopupOpen] = useState(false); 29 | const [replyToId, setReplyToId] = useState(undefined); 30 | const { address } = useAccount(); 31 | const [isAuthenticated, setIsAuthenticated] = useState(false); 32 | 33 | const userAvatars = useMemo(() => { 34 | const avatars: { [key: string]: string } = {}; 35 | messages.forEach(msg => { 36 | if (!avatars[msg.user]) { 37 | avatars[msg.user] = getRandomAvatarImage(); 38 | } 39 | }); 40 | return avatars; 41 | }, [messages]); 42 | 43 | useEffect(() => { 44 | checkAuth(); 45 | fetchMessages(); 46 | const interval = setInterval(fetchMessages, 30000); 47 | return () => clearInterval(interval); 48 | }, [tokenAddress]); 49 | 50 | const checkAuth = async () => { 51 | try { 52 | const response = await fetch('/api/auth/user'); 53 | if (response.ok) { 54 | setIsAuthenticated(true); 55 | fetchMessages(); 56 | } else { 57 | setIsAuthenticated(false); 58 | } 59 | } catch (error) { 60 | setIsAuthenticated(false); 61 | } 62 | }; 63 | 64 | const handleAuthSuccess = () => { 65 | setIsAuthenticated(true); 66 | fetchMessages(); 67 | }; 68 | 69 | const fetchMessages = async () => { 70 | try { 71 | const fetchedMessages = await getChatMessages(tokenAddress); 72 | setMessages(fetchedMessages); 73 | } catch (error) { 74 | console.error('Error fetching messages:', error); 75 | toast.error('Failed to fetch messages'); 76 | } 77 | }; 78 | 79 | const handlePostMessage = async () => { 80 | if (!isAuthenticated) { 81 | toast.error('Please sign in with Ethereum to post a message'); 82 | return; 83 | } 84 | 85 | if (!newMessage.trim()) { 86 | toast.error('Please enter a message'); 87 | return; 88 | } 89 | 90 | try { 91 | await addChatMessage(address!, tokenAddress, newMessage, replyToId); 92 | setNewMessage(''); 93 | setIsPopupOpen(false); 94 | setReplyToId(undefined); 95 | fetchMessages(); 96 | toast.success('Message posted successfully'); 97 | } catch (error) { 98 | console.error('Error posting message:', error); 99 | toast.error('Failed to post message'); 100 | } 101 | }; 102 | 103 | const handleReply = (messageId: number) => { 104 | setReplyToId(messageId); 105 | setIsPopupOpen(true); 106 | }; 107 | 108 | const renderMessage = (msg: ChatMessage, depth: number = 0) => { 109 | const isReply = depth > 0; 110 | const isCreator = msg.user.toLowerCase() === tokenInfo.creatorAddress.toLowerCase(); 111 | return ( 112 | 120 |
121 | User Avatar 128 |
129 |
130 | 131 | {shortenAddress(msg.user)} 132 | {isCreator && (dev)} 133 | 134 | {formatTimestamp(msg.timestamp)} 135 |
136 |

{msg.message}

137 |
138 | 144 |
145 |
146 |
147 | {messages.filter(reply => reply.reply_to === msg.id).map(reply => renderMessage(reply, depth + 1))} 148 |
149 | ); 150 | }; 151 | 152 | return ( 153 |
154 |

Chats

155 | {!isAuthenticated ? ( 156 | 157 | ) : ( 158 | <> 159 | {messages.length === 0 ? ( 160 |

No messages yet. Be the first to chat!

161 | ) : ( 162 |
163 | 164 | {messages.filter(msg => msg.reply_to === null).map(msg => renderMessage(msg))} 165 | 166 |
167 | )} 168 | 174 | 175 | 176 | {isPopupOpen && ( 177 | 183 | 189 |

190 | {replyToId !== undefined ? 'Reply' : 'Post'} 191 |

192 |