├── src ├── utils │ ├── styleUtils.ts │ ├── useToast.ts │ ├── types │ │ ├── css.d.ts │ │ ├── react-split.d.ts │ │ ├── api.ts │ │ └── index.ts │ ├── brandConfig.ts │ ├── jitoService.ts │ ├── formatting.ts │ ├── wallets.ts │ ├── recentTokens.ts │ ├── hdWallet.ts │ ├── iframeManager.ts │ ├── rpcManager.ts │ ├── consolidate.ts │ └── trading.ts ├── logo.png ├── contexts │ ├── AppContextInstance.tsx │ ├── useAppContext.ts │ ├── IframeStateContext.tsx │ └── AppContext.tsx ├── components │ ├── ToastContext.tsx │ ├── Split.tsx │ ├── tools │ │ ├── automate │ │ │ ├── index.ts │ │ │ ├── ProfileCard.tsx │ │ │ ├── SniperFilterBuilder.tsx │ │ │ └── types.ts │ │ └── index.ts │ ├── Tooltip.tsx │ ├── BeRightBack.tsx │ ├── ErrorBoundary.tsx │ ├── Notifications.tsx │ ├── Header.tsx │ ├── modals │ │ ├── CalculatePNLModal.tsx │ │ └── ExportSeedPhraseModal.tsx │ ├── Styles.tsx │ ├── HorizontalHeader.tsx │ └── Config.tsx ├── vite-env.d.ts └── Mobile.tsx ├── postcss.config.js ├── manifest.json ├── docs ├── SECURITY.md ├── AUDIT.md └── WHITELABEL.md ├── .gitignore ├── brand.json ├── LICENSE ├── tailwind.config.js ├── scripts └── generate-html.js ├── package.json ├── index.template.html ├── .eslintrc.cjs ├── index.html ├── vite.config.js └── README.md /src/utils/styleUtils.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/razedotbot/solana-ui/HEAD/src/logo.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } -------------------------------------------------------------------------------- /src/contexts/AppContextInstance.tsx: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | import type { AppContextType } from './AppContext'; 3 | 4 | export const AppContext = createContext(undefined); 5 | 6 | -------------------------------------------------------------------------------- /src/components/ToastContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext } from "react" 2 | 3 | export const ToastContext = createContext<{ 4 | showToast: (message: string, type: 'success' | 'error') => void 5 | }>({ 6 | showToast: () => {}, 7 | }) 8 | -------------------------------------------------------------------------------- /src/utils/useToast.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "react" 2 | import { ToastContext } from "../components/ToastContext" 3 | 4 | export const useToast = (): { showToast: (message: string, type: 'success' | 'error') => void } => { 5 | return useContext(ToastContext) 6 | } 7 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Fury Bundler", 3 | "short_name": "Fury", 4 | "display": "standalone", 5 | "orientation": "portrait", 6 | "theme_color": "#041726", 7 | "background_color": "#041726", 8 | "start_url": "/", 9 | "icons": [] 10 | } -------------------------------------------------------------------------------- /src/components/Split.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import OriginalSplit from 'react-split'; 3 | import type { SplitProps } from 'react-split'; 4 | 5 | /** 6 | * Wrapper for react-split component. 7 | */ 8 | export const Split: React.FC = (props) => { 9 | return ; 10 | }; 11 | 12 | export default Split; 13 | -------------------------------------------------------------------------------- /src/contexts/useAppContext.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { AppContext } from './AppContextInstance'; 3 | import type { AppContextType } from './AppContext'; 4 | 5 | export const useAppContext = (): AppContextType => { 6 | const context = useContext(AppContext); 7 | if (!context) { 8 | throw new Error('useAppContext must be used within AppContextProvider'); 9 | } 10 | return context; 11 | }; 12 | 13 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare global { 4 | interface ImportMetaEnv { 5 | readonly MODE: string; 6 | readonly BASE_URL: string; 7 | readonly PROD: boolean; 8 | readonly DEV: boolean; 9 | readonly SSR: boolean; 10 | // Add more env variables as needed 11 | } 12 | 13 | interface ImportMeta { 14 | readonly env: ImportMetaEnv; 15 | } 16 | } 17 | 18 | export {} 19 | 20 | -------------------------------------------------------------------------------- /src/utils/types/css.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css' { 2 | const content: Record; 3 | export default content; 4 | } 5 | 6 | declare module '*.png' { 7 | const content: string; 8 | export default content; 9 | } 10 | 11 | declare module '*.jpg' { 12 | const content: string; 13 | export default content; 14 | } 15 | 16 | declare module '*.jpeg' { 17 | const content: string; 18 | export default content; 19 | } 20 | 21 | declare module '*.svg' { 22 | const content: string; 23 | export default content; 24 | } -------------------------------------------------------------------------------- /src/utils/brandConfig.ts: -------------------------------------------------------------------------------- 1 | import brandConfig from '../../brand.json'; 2 | 3 | export interface BrandConfig { 4 | brand: { 5 | name: string; 6 | displayName: string; 7 | altText: string; 8 | domain: string; 9 | appUrl: string; 10 | docsUrl: string; 11 | githubUrl: string; 12 | githubOrg: string; 13 | social: { 14 | twitter: string; 15 | github: string; 16 | }; 17 | theme: { 18 | name: string; 19 | }; 20 | }; 21 | } 22 | 23 | export const getBrandConfig = (): BrandConfig => { 24 | return brandConfig as BrandConfig; 25 | }; 26 | 27 | export const brand = getBrandConfig().brand; -------------------------------------------------------------------------------- /docs/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | We support only the latest stable release of this project. 6 | Security fixes for older versions are not guaranteed. 7 | 8 | ## Reporting a Vulnerability 9 | 10 | If you discover a security vulnerability, we encourage you to report it **privately** and responsibly. 11 | 12 | - Please **open a GitHub issue** and prefix the title with `[SECURITY]`. 13 | - Alternatively, you can contact us via our Support Center: 14 | 👉 [https://help.raze.bot/](https://help.raze.bot/) 15 | 16 | We will review and address your report as promptly as possible. 17 | 18 | Thank you for helping us keep Raze.bot and our users safe! 19 | -------------------------------------------------------------------------------- /src/components/tools/automate/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Unified Trading Tools - Component Exports 3 | */ 4 | 5 | // Main component 6 | export { default as TradingTools } from './TradingTools'; 7 | 8 | // Profile management 9 | export { default as ProfileBuilder } from './ProfileBuilder'; 10 | export { default as ProfileCard } from './ProfileCard'; 11 | 12 | // Sniper Bot 13 | export { default as SniperFilterBuilder } from './SniperFilterBuilder'; 14 | 15 | // Copy Trade & Automate 16 | export { default as UnifiedConditionBuilder } from './UnifiedConditionBuilder'; 17 | export { default as UnifiedActionBuilder } from './UnifiedActionBuilder'; 18 | 19 | // Shared 20 | export { default as UnifiedWalletManager } from './UnifiedWalletManager'; 21 | -------------------------------------------------------------------------------- /src/components/tools/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Unified Trading Tools Module 3 | * 4 | * Combines Sniper Bot, Copy Trade, and Automate into a single, cohesive interface. 5 | * 6 | * @example 7 | * ```tsx 8 | * import { TradingTools } from './unified-tools'; 9 | * 10 | * function App() { 11 | * return ( 12 | * { 15 | * console.log('Execute:', type, profileId, action); 16 | * }} 17 | * /> 18 | * ); 19 | * } 20 | * ``` 21 | */ 22 | 23 | // Export all types 24 | export * from './automate/types'; 25 | 26 | // Export storage utilities 27 | export * from './automate/storage'; 28 | 29 | // Export components 30 | export * from './automate'; 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node.js 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | .pnpm-store/ 7 | 8 | # Environment variables 9 | .env 10 | .env.local 11 | .env.development.local 12 | .env.test.local 13 | .env.production.local 14 | 15 | # Build directories 16 | dist/ 17 | build/ 18 | out/ 19 | .next/ 20 | .nuxt/ 21 | .cache/ 22 | 23 | # Dependency directories 24 | jspm_packages/ 25 | .webpack/ 26 | .serverless/ 27 | .fusebox/ 28 | 29 | # Debug logs 30 | *.log 31 | *.logs 32 | 33 | # Editor directories and files 34 | .vscode/ 35 | .idea/ 36 | *.suo 37 | *.ntvs* 38 | *.njsproj 39 | *.sln 40 | *.sw? 41 | 42 | # OS generated files 43 | .DS_Store 44 | .DS_Store? 45 | ._* 46 | .Spotlight-V100 47 | .Trashes 48 | ehthumbs.db 49 | Thumbs.db 50 | 51 | # Testing 52 | coverage/ 53 | .nyc_output/ 54 | 55 | # Package manager specific 56 | package-lock.json 57 | yarn.lock 58 | pnpm-lock.yaml 59 | shrinkwrap.yaml 60 | /.kiro 61 | /.vercel 62 | -------------------------------------------------------------------------------- /brand.json: -------------------------------------------------------------------------------- 1 | { 2 | "brand": { 3 | "name": "Raze.BOT", 4 | "displayName": "RAZE.BOT", 5 | "altText": "Raze Bundler", 6 | "domain": "raze.bot", 7 | "appUrl": "https://app.raze.bot", 8 | "docsUrl": "https://docs.raze.bot", 9 | "githubUrl": "https://github.com/razedotbot/solana-ui/", 10 | "githubOrg": "https://github.com/Razedotbot", 11 | "social": { 12 | "twitter": "@razedotbot", 13 | "github": "razedotbot" 14 | }, 15 | "seo": { 16 | "title": "Bundler - raze.bot", 17 | "ogTitle": "Dominate Every Trading Opportunity - raze.bot", 18 | "description": "raze Tools Empowers You with Professional Tools for Maximum Market Impact", 19 | "ogImage": "https://raze.bot/images/bannermeta.jpg", 20 | "twitterImage": "https://raze.bot/images/bannermeta.png" 21 | }, 22 | "favicon": { 23 | "baseUrl": "https://raze.bot/images/favicon", 24 | "themeColor": "#000000", 25 | "tileColor": "#000000" 26 | }, 27 | "theme": { 28 | "name": "green" 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Faultz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/utils/types/react-split.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-split' { 2 | import type { ReactNode, CSSProperties } from 'react'; 3 | 4 | export interface SplitProps { 5 | children: ReactNode; 6 | className?: string; 7 | style?: CSSProperties; 8 | sizes?: number[]; 9 | minSize?: number | number[]; 10 | maxSize?: number | number[]; 11 | expandToMin?: boolean; 12 | gutterSize?: number; 13 | gutterAlign?: 'center' | 'start' | 'end'; 14 | snapOffset?: number; 15 | dragInterval?: number; 16 | direction?: 'horizontal' | 'vertical'; 17 | cursor?: string; 18 | gutter?: (index: number, direction: 'horizontal' | 'vertical') => HTMLElement; 19 | elementStyle?: ( 20 | dimension: 'width' | 'height', 21 | size: number, 22 | gutterSize: number, 23 | ) => CSSProperties; 24 | gutterStyle?: ( 25 | dimension: 'width' | 'height', 26 | gutterSize: number, 27 | ) => CSSProperties; 28 | onDrag?: (sizes: number[]) => void; 29 | onDragStart?: (sizes: number[]) => void; 30 | onDragEnd?: (sizes: number[]) => void; 31 | collapsed?: number; 32 | } 33 | 34 | export default function Split(props: SplitProps): JSX.Element; 35 | } 36 | 37 | -------------------------------------------------------------------------------- /src/components/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | export const Tooltip = ({ 4 | children, 5 | content, 6 | position = 'top' 7 | }: { 8 | children: React.ReactNode; 9 | content: string; 10 | position?: 'top' | 'bottom' | 'left' | 'right'; 11 | }): React.ReactElement => { 12 | const [isVisible, setIsVisible] = useState(false); 13 | 14 | const positionClasses = { 15 | top: 'bottom-full left-1/2 -translate-x-1/2 mb-2', 16 | bottom: 'top-full left-1/2 -translate-x-1/2 mt-2', 17 | left: 'right-full top-1/2 -translate-y-1/2 mr-2', 18 | right: 'left-full top-1/2 -translate-y-1/2 ml-2' 19 | }; 20 | 21 | return ( 22 |
23 |
setIsVisible(true)} 25 | onMouseLeave={() => setIsVisible(false)} 26 | > 27 | {children} 28 |
29 | {isVisible && ( 30 |
31 |
33 | {content} 34 |
35 |
36 | )} 37 |
38 | ); 39 | }; 40 | 41 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: { 9 | colors: { 10 | furyblue: { 11 | 50: '#041726', 12 | 100: '#041726', 13 | 200: '#041726', 14 | 300: '#041726', 15 | 400: '#041726', 16 | 500: '#041726', 17 | 600: '#041726', 18 | 700: '#041726', 19 | 800: '#041726', 20 | 900: '#0F2538', 21 | }, 22 | }, 23 | animation: { 24 | fadeIn: 'fadeIn 0.2s ease-out', 25 | scaleIn: 'scaleIn 0.2s ease-out', 26 | click: 'click 0.3s ease-out', 27 | 'pulse-slow': 'pulse-slow 3s ease-in-out infinite', 28 | }, 29 | keyframes: { 30 | fadeIn: { 31 | '0%': { opacity: '0' }, 32 | '100%': { opacity: '1' }, 33 | }, 34 | scaleIn: { 35 | '0%': { transform: 'scale(0.95)', opacity: '0' }, 36 | '100%': { transform: 'scale(1)', opacity: '1' }, 37 | }, 38 | click: { 39 | '0%': { transform: 'scale(1)' }, 40 | '50%': { transform: 'scale(0.98)' }, 41 | '100%': { transform: 'scale(1)' }, 42 | }, 43 | 'pulse-slow': { 44 | '0%, 100%': { opacity: '1' }, 45 | '50%': { opacity: '0.7' }, 46 | }, 47 | }, 48 | }, 49 | }, 50 | plugins: [], 51 | } -------------------------------------------------------------------------------- /scripts/generate-html.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { fileURLToPath } from 'url'; 4 | 5 | // Get current directory in ES modules 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = path.dirname(__filename); 8 | 9 | // Read the brand configuration 10 | const brandConfigPath = path.join(__dirname, '../brand.json'); 11 | const templatePath = path.join(__dirname, '../index.template.html'); 12 | const outputPath = path.join(__dirname, '../index.html'); 13 | 14 | try { 15 | // Read brand configuration 16 | const brandConfig = JSON.parse(fs.readFileSync(brandConfigPath, 'utf8')); 17 | const brand = brandConfig.brand; 18 | 19 | // Read HTML template 20 | const template = fs.readFileSync(templatePath, 'utf8'); 21 | 22 | // Replace placeholders with brand values 23 | let html = template 24 | .replace(/{{TITLE}}/g, brand.seo.title) 25 | .replace(/{{OG_TITLE}}/g, brand.seo.ogTitle) 26 | .replace(/{{DESCRIPTION}}/g, brand.seo.description) 27 | .replace(/{{OG_IMAGE}}/g, brand.seo.ogImage) 28 | .replace(/{{TWITTER_IMAGE}}/g, brand.seo.twitterImage) 29 | .replace(/{{DOMAIN}}/g, brand.domain) 30 | .replace(/{{FAVICON_BASE_URL}}/g, brand.favicon.baseUrl) 31 | .replace(/{{THEME_COLOR}}/g, brand.favicon.themeColor) 32 | .replace(/{{TILE_COLOR}}/g, brand.favicon.tileColor); 33 | 34 | // Write the generated HTML 35 | fs.writeFileSync(outputPath, html, 'utf8'); 36 | 37 | console.log('✅ index.html generated successfully from brand configuration'); 38 | console.log(`📄 Output: ${outputPath}`); 39 | 40 | } catch (error) { 41 | console.error('❌ Error generating HTML:', error.message); 42 | process.exit(1); 43 | } -------------------------------------------------------------------------------- /src/utils/jitoService.ts: -------------------------------------------------------------------------------- 1 | import type { ApiResponse } from './types'; 2 | 3 | interface WindowWithConfig { 4 | tradingServerUrl?: string; 5 | } 6 | 7 | /** 8 | * Sends a signed transaction to the server's /v2/sol/send endpoint 9 | * which then forwards it to the Jito bundle service 10 | * @param serializedTransaction - bs58-encoded serialized transaction 11 | * @returns Result from the bundle service 12 | */ 13 | export const sendToJitoBundleService = async (serializedTransaction: string): Promise => { 14 | try { 15 | // Get the server base URL 16 | const baseUrl = (window as unknown as WindowWithConfig).tradingServerUrl?.replace(/\/+$/, '') || ""; 17 | const sendBundleEndpoint = `${baseUrl}/v2/sol/send`; 18 | 19 | // Create the request payload - this matches what the server endpoint expects 20 | const payload = { 21 | transactions: [serializedTransaction] // Server expects an array of transactions 22 | }; 23 | 24 | // Send request to our server endpoint (not directly to Jito) 25 | const response = await fetch(sendBundleEndpoint, { 26 | method: 'POST', 27 | headers: { 28 | 'Content-Type': 'application/json' 29 | }, 30 | body: JSON.stringify(payload) 31 | }); 32 | 33 | const result: ApiResponse = await response.json() as ApiResponse; 34 | 35 | if (!result.success) { 36 | // Handle error from our server endpoint 37 | const errorMessage = result.error || 'Unknown error sending bundle'; 38 | const errorDetails = result.details ? `: ${result.details}` : ''; 39 | throw new Error(`${errorMessage}${errorDetails}`); 40 | } 41 | 42 | return result.result; 43 | } catch (error) { 44 | console.error('Error sending transaction bundle:', error); 45 | throw error; 46 | } 47 | }; -------------------------------------------------------------------------------- /docs/AUDIT.md: -------------------------------------------------------------------------------- 1 | # Security Audit Report - WALLETDB Encryption Implementation 2 | 3 | ## Overview 4 | 5 | This audit report documents the application security standards. The encryption was implemented to protect sensitive wallet private keys stored locally. 6 | 7 | ### Key Findings 8 | - **Encryption Standard:** AES (Advanced Encryption Standard) via crypto-js library 9 | - **Storage Security:** Private keys are encrypted at rest in both localStorage and IndexedDB 10 | - **Error Handling:** Robust fallback mechanisms prevent data loss 11 | 12 | ## Technical Implementation Review 13 | 14 | ### 1. Encryption Algorithm 15 | - **Algorithm:** AES (Advanced Encryption Standard) 16 | - **Library:** crypto-js v4.2.0 17 | - **Key Management:** Static encryption key (see recommendations) 18 | 19 | ### 2. Storage Architecture 20 | ``` 21 | Wallet Data Flow: 22 | Plaintext Wallet Data → AES Encryption → Encrypted Storage (localStorage + IndexedDB) 23 | ``` 24 | 25 | ### 3. Key Functions Audited 26 | 27 | #### `encryptData(data: string): string` 28 | - ✅ Properly encrypts data using AES 29 | - ✅ Includes error handling 30 | - ✅ Returns encrypted string 31 | 32 | #### `decryptData(encryptedData: string): string` 33 | - ✅ Properly decrypts AES encrypted data 34 | - ✅ Validates decryption success 35 | - ✅ Throws errors for invalid data 36 | 37 | #### `saveWalletsToCookies(wallets: WalletType[]): void` 38 | - ✅ Encrypts wallet data before storage 39 | - ✅ Stores in both localStorage and IndexedDB 40 | - ✅ Removes old unencrypted data 41 | - ✅ Includes fallback mechanisms 42 | 43 | #### `loadWalletsFromCookies(): WalletType[]` 44 | - ✅ Attempts to load encrypted data first 45 | - ✅ Automatic migration from unencrypted data 46 | - ✅ Comprehensive error handling 47 | - ✅ Logging for debugging 48 | 49 | ## Security Strengths 50 | 51 | ### ✅ Encryption Implementation 52 | - Uses industry-standard AES encryption 53 | - Proper error handling prevents data corruption 54 | - Encrypted data stored in multiple locations for redundancy 55 | 56 | ### ✅ Storage Security 57 | - Private keys never stored in plaintext after implementation 58 | - Dual storage (localStorage + IndexedDB) for reliability 59 | - Proper key-value structure in IndexedDB 60 | 61 | ### ✅ Error Resilience 62 | - Multiple fallback mechanisms 63 | - Graceful degradation if encryption fails 64 | - Comprehensive logging for troubleshooting 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "raze-solana-ui", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite --host", 8 | "build": "npm run typecheck && node scripts/generate-html.js && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "typecheck": "tsc --noEmit", 11 | "preview": "vite preview", 12 | "analyze": "npx vite-bundle-analyzer", 13 | "generate-html": "node scripts/generate-html.js" 14 | }, 15 | "dependencies": { 16 | "@jup-ag/api": "^6.0.30", 17 | "@radix-ui/react-select": "^2.1.4", 18 | "@radix-ui/react-switch": "^1.1.2", 19 | "@solana/spl-token": "0.1.8", 20 | "@solana/spl-token-registry": "^0.2.4574", 21 | "@solana/web3.js": "^1.95.8", 22 | "@types/bip39": "^2.4.2", 23 | "@types/js-cookie": "^3.0.6", 24 | "bip39": "^3.1.0", 25 | "bs58": "^6.0.0", 26 | "buffer": "^6.0.3", 27 | "class-variance-authority": "^0.7.0", 28 | "clsx": "^2.0.0", 29 | "crypto-js": "^4.2.0", 30 | "d3": "^7.9.0", 31 | "ed25519-hd-key": "^1.3.0", 32 | "html2canvas": "^1.4.1", 33 | "js-cookie": "^3.0.5", 34 | "lucide-react": "^0.471.0", 35 | "react": "^18.2.0", 36 | "react-dom": "^18.2.0", 37 | "react-router-dom": "^6.26.0", 38 | "react-split": "^2.0.14", 39 | "react-window": "^1.8.11", 40 | "serve": "^14.2.5", 41 | "tailwind-merge": "^1.14.0", 42 | "zustand": "^5.0.2" 43 | }, 44 | "devDependencies": { 45 | "@types/crypto-js": "^4.2.2", 46 | "@types/node": "^20.4.5", 47 | "@types/react": "^18.2.15", 48 | "@types/react-dom": "^18.2.7", 49 | "@types/react-router-dom": "^5.3.3", 50 | "@typescript-eslint/eslint-plugin": "^8.46.4", 51 | "@typescript-eslint/parser": "^8.46.4", 52 | "@vitejs/plugin-react": "^4.0.3", 53 | "autoprefixer": "^10.4.14", 54 | "eslint": "^8.45.0", 55 | "eslint-plugin-react-hooks": "^4.6.0", 56 | "eslint-plugin-react-refresh": "^0.4.3", 57 | "postcss": "^8.4.27", 58 | "rollup-plugin-visualizer": "^6.0.3", 59 | "tailwindcss": "^3.3.3", 60 | "terser": "^5.43.1", 61 | "typescript": "^5.9.3", 62 | "vite": "^6.4.0", 63 | "vite-bundle-analyzer": "^0.7.0", 64 | "vite-plugin-checker": "^0.11.0", 65 | "vite-plugin-node-polyfills": "^0.24.0" 66 | }, 67 | "overrides": { 68 | "node-fetch": "^2.7.0" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/utils/formatting.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Format wallet address to shortened form 3 | */ 4 | export const formatAddress = (address: string): string => { 5 | if (address.length < 8) return address; 6 | return `${address.slice(0, 4)}...${address.slice(-4)}`; 7 | }; 8 | 9 | /** 10 | * Format SOL balance with 4 decimal places 11 | */ 12 | export const formatSolBalance = (balance: number): string => { 13 | return balance.toFixed(4); 14 | }; 15 | 16 | /** 17 | * Format token balance with appropriate suffix (K, M, B) 18 | */ 19 | export const formatTokenBalance = (balance: number | undefined): string => { 20 | if (balance === undefined) return '0.00'; 21 | if (balance < 1000) return balance.toFixed(2); 22 | 23 | if (balance < 1_000_000) { 24 | return `${(balance / 1000).toFixed(1)}K`; 25 | } 26 | if (balance < 1_000_000_000) { 27 | return `${(balance / 1_000_000).toFixed(1)}M`; 28 | } 29 | return `${(balance / 1_000_000_000).toFixed(1)}B`; 30 | }; 31 | 32 | /** 33 | * Format large numbers with k, M, B suffixes 34 | */ 35 | export const formatNumber = (num: number | string): string => { 36 | const number = parseFloat(num.toString()); 37 | if (isNaN(number) || number === 0) return "0"; 38 | 39 | const absNum = Math.abs(number); 40 | 41 | if (absNum >= 1000000000) { 42 | return (number / 1000000000).toFixed(2).replace(/\.?0+$/, '') + 'B'; 43 | } else if (absNum >= 1000000) { 44 | return (number / 1000000).toFixed(2).replace(/\.?0+$/, '') + 'M'; 45 | } else if (absNum >= 1000) { 46 | return (number / 1000).toFixed(2).replace(/\.?0+$/, '') + 'k'; 47 | } else if (absNum >= 1) { 48 | return number.toFixed(2).replace(/\.?0+$/, ''); 49 | } else { 50 | // For very small numbers, show more decimal places 51 | return number.toFixed(6).replace(/\.?0+$/, ''); 52 | } 53 | }; 54 | 55 | /** 56 | * Format price for display 57 | */ 58 | export const formatPrice = (price: number | null | undefined): string => { 59 | if (!price || price === 0 || typeof price !== 'number' || isNaN(price)) return '$--'; 60 | if (price < 0.000001) return `$${price.toExponential(2)}`; 61 | if (price < 0.01) return `$${price.toFixed(6)}`; 62 | return `$${price.toFixed(4)}`; 63 | }; 64 | 65 | /** 66 | * Format large numbers with $ prefix and suffix 67 | */ 68 | export const formatLargeNumber = (num: number | null | undefined): string => { 69 | if (!num || num === 0 || typeof num !== 'number' || isNaN(num)) return '--'; 70 | if (num >= 1000000000) return `$${(num / 1000000000).toFixed(2)}B`; 71 | if (num >= 1000000) return `$${(num / 1000000).toFixed(2)}M`; 72 | if (num >= 1000) return `$${(num / 1000).toFixed(2)}K`; 73 | return `$${num.toFixed(2)}`; 74 | }; 75 | 76 | /** 77 | * Format count with locale string 78 | */ 79 | export const formatCount = (count: number | null | undefined): string => { 80 | if (!count && count !== 0) return '--'; 81 | if (typeof count !== 'number' || isNaN(count)) return '--'; 82 | return count.toLocaleString(); 83 | }; 84 | 85 | -------------------------------------------------------------------------------- /index.template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | {{TITLE}} 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 |
45 | 46 | 47 | -------------------------------------------------------------------------------- /src/utils/wallets.ts: -------------------------------------------------------------------------------- 1 | import type { WalletType, ScriptType } from './types'; 2 | 3 | // Re-export types for backward compatibility 4 | export type { ScriptType }; 5 | 6 | /** 7 | * Counts the number of active wallets in the provided wallet array 8 | * @param wallets Array of wallet objects 9 | * @returns Number of active wallets (excludes archived wallets) 10 | */ 11 | export const countActiveWallets = (wallets: WalletType[]): number => { 12 | return wallets.filter(wallet => wallet.isActive && !wallet.isArchived).length; 13 | }; 14 | 15 | /** 16 | * Returns an array of only the active wallets 17 | * @param wallets Array of wallet objects 18 | * @returns Array of active wallets (excludes archived wallets) 19 | */ 20 | export const getActiveWallets = (wallets: WalletType[]): WalletType[] => { 21 | return wallets.filter(wallet => wallet.isActive && !wallet.isArchived); 22 | }; 23 | 24 | // New function to toggle all wallets regardless of balance 25 | export const toggleAllWallets = (wallets: WalletType[]): WalletType[] => { 26 | const nonArchivedWallets = wallets.filter(wallet => !wallet.isArchived); 27 | const allActive = nonArchivedWallets.every(wallet => wallet.isActive); 28 | return wallets.map(wallet => ({ 29 | ...wallet, 30 | isActive: wallet.isArchived ? wallet.isActive : !allActive 31 | })); 32 | }; 33 | 34 | // Updated to use separate SOL balance tracking 35 | export const toggleAllWalletsWithBalance = ( 36 | wallets: WalletType[], 37 | solBalances: Map 38 | ): WalletType[] => { 39 | // Check if all non-archived wallets with balance are already active 40 | const walletsWithBalance = wallets.filter(wallet => 41 | !wallet.isArchived && (solBalances.get(wallet.address) || 0) > 0 42 | ); 43 | const allWithBalanceActive = walletsWithBalance.every(wallet => wallet.isActive); 44 | 45 | // Toggle based on current state 46 | return wallets.map(wallet => ({ 47 | ...wallet, 48 | isActive: !wallet.isArchived && (solBalances.get(wallet.address) || 0) > 0 49 | ? !allWithBalanceActive 50 | : wallet.isActive 51 | })); 52 | }; 53 | 54 | export const toggleWalletsByBalance = ( 55 | wallets: WalletType[], 56 | showWithTokens: boolean, 57 | solBalances: Map, 58 | tokenBalances: Map 59 | ): WalletType[] => { 60 | return wallets.map(wallet => ({ 61 | ...wallet, 62 | isActive: wallet.isArchived ? false : ( 63 | showWithTokens 64 | ? (tokenBalances.get(wallet.address) || 0) > 0 // Select wallets with tokens 65 | : (solBalances.get(wallet.address) || 0) > 0 && (tokenBalances.get(wallet.address) || 0) === 0 // Select wallets with only SOL 66 | ) 67 | })); 68 | }; 69 | 70 | /** 71 | * Gets the appropriate script name based on selected DEX and mode 72 | * @param selectedDex Selected DEX name 73 | * @param isBuyMode Whether in buy mode 74 | * @returns The corresponding script name 75 | */ 76 | export const getScriptName = (selectedDex: string, isBuyMode: boolean): ScriptType => { 77 | switch(selectedDex) { 78 | default: 79 | return isBuyMode ? 'buy' : 'sell'; 80 | } 81 | }; -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('eslint').Linter.Config} */ 2 | module.exports = { 3 | root: true, 4 | env: { 5 | browser: true, 6 | es2023: true, 7 | node: true, 8 | }, 9 | parser: '@typescript-eslint/parser', 10 | parserOptions: { 11 | project: ['./tsconfig.json'], 12 | tsconfigRootDir: __dirname, 13 | ecmaVersion: 'latest', 14 | sourceType: 'module', 15 | }, 16 | plugins: ['@typescript-eslint', 'react-refresh', 'react-hooks'], 17 | extends: [ 18 | 'eslint:recommended', 19 | 'plugin:react-hooks/recommended', 20 | 'plugin:@typescript-eslint/recommended', 21 | 'plugin:@typescript-eslint/recommended-requiring-type-checking', 22 | ], 23 | settings: { 24 | react: { 25 | version: 'detect', 26 | }, 27 | }, 28 | overrides: [ 29 | { 30 | files: ['vite.config.js', 'scripts/**/*.js'], 31 | env: { node: true }, 32 | parserOptions: { 33 | project: null, 34 | sourceType: 'module', 35 | }, 36 | }, 37 | { 38 | files: ['*.cjs'], 39 | env: { node: true }, 40 | parserOptions: { 41 | project: null, 42 | sourceType: 'script', 43 | }, 44 | }, 45 | ], 46 | ignorePatterns: ['dist', 'node_modules', 'public'], 47 | rules: { 48 | 'react-refresh/only-export-components': 'warn', 49 | '@typescript-eslint/no-unused-vars': [ 50 | 'error', 51 | { 52 | args: 'all', 53 | argsIgnorePattern: '^_', 54 | varsIgnorePattern: '^_', 55 | caughtErrors: 'all', 56 | caughtErrorsIgnorePattern: '^ignore$', 57 | ignoreRestSiblings: false, 58 | }, 59 | ], 60 | '@typescript-eslint/consistent-type-imports': [ 61 | 'error', 62 | { 63 | prefer: 'type-imports', 64 | disallowTypeAnnotations: false, 65 | }, 66 | ], 67 | '@typescript-eslint/explicit-function-return-type': [ 68 | 'error', 69 | { 70 | allowExpressions: true, 71 | allowTypedFunctionExpressions: true, 72 | allowHigherOrderFunctions: true, 73 | }, 74 | ], 75 | '@typescript-eslint/explicit-module-boundary-types': 'error', 76 | '@typescript-eslint/no-explicit-any': 'error', 77 | '@typescript-eslint/no-unsafe-assignment': 'error', 78 | '@typescript-eslint/no-unsafe-return': 'error', 79 | '@typescript-eslint/no-unsafe-member-access': 'error', 80 | '@typescript-eslint/no-floating-promises': ['error', { ignoreVoid: true }], 81 | '@typescript-eslint/no-misused-promises': [ 82 | 'error', 83 | { 84 | checksVoidReturn: { attributes: false }, 85 | }, 86 | ], 87 | '@typescript-eslint/require-await': 'error', 88 | '@typescript-eslint/switch-exhaustiveness-check': 'error', 89 | '@typescript-eslint/prefer-readonly': [ 90 | 'error', 91 | { onlyInlineLambdas: true }, 92 | ], 93 | 'no-console': ['warn', { allow: ['error', 'warn', 'info'] }], 94 | 'no-restricted-syntax': [ 95 | 'error', 96 | { 97 | selector: 'TSEnumDeclaration', 98 | message: 'Avoid enums, prefer union types instead.', 99 | }, 100 | ], 101 | }, 102 | }; 103 | 104 | -------------------------------------------------------------------------------- /src/components/BeRightBack.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Wallet, Settings } from 'lucide-react'; 3 | 4 | interface BeRightBackProps { 5 | onOpenWallets: () => void; 6 | onOpenSettings: () => void; 7 | } 8 | 9 | const BeRightBack: React.FC = ({ onOpenWallets, onOpenSettings }) => { 10 | return ( 11 |
12 | {/* Animated grid background */} 13 |
14 | 15 | {/* Main content */} 16 |
17 | {/* Glowing title */} 18 |

24 | BE RIGHT BACK 25 |

26 | 27 | {/* Subtitle */} 28 |

29 | We're currently performing maintenance 30 |

31 | 32 | {/* Animated loading indicator */} 33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | 42 | {/* Decorative elements */} 43 |
44 | 45 | 46 | 47 |
48 | 49 | {/* Action buttons */} 50 |
51 | 58 | 65 |
66 | 67 | {/* Bottom message */} 68 |

69 | Please check back soon 70 |

71 |
72 | 73 | {/* Scanline effect overlay */} 74 |
75 |
76 | ); 77 | }; 78 | 79 | export default BeRightBack; 80 | 81 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | Bundler - raze.bot 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 |
45 | 46 | 47 | -------------------------------------------------------------------------------- /src/utils/types/api.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Shared type definitions for API responses and common structures 3 | * Used across the Solana trading application 4 | */ 5 | 6 | /** 7 | * Generic API response wrapper 8 | * Used for responses from the trading server endpoints 9 | */ 10 | export interface ApiResponse { 11 | success: boolean; 12 | data?: T; 13 | error?: string; 14 | details?: string; 15 | result?: T; // Some endpoints use 'result' instead of 'data' 16 | } 17 | 18 | /** 19 | * Bundle result from Jito block engine 20 | * Used when sending transaction bundles through the backend proxy 21 | */ 22 | export interface BundleResult { 23 | jsonrpc: string; 24 | id: number; 25 | result?: string; 26 | error?: { 27 | code: number; 28 | message: string; 29 | }; 30 | } 31 | 32 | /** 33 | * Simplified bundle result for success/failure tracking 34 | * Used in utility functions that process bundle responses 35 | */ 36 | export interface SimpleBundleResult { 37 | success: boolean; 38 | signature?: string; 39 | txid?: string; 40 | bundleId?: string; 41 | error?: string; 42 | } 43 | 44 | /** 45 | * Token creation result 46 | * Used when creating new tokens on various platforms (Pump, Moon, etc.) 47 | */ 48 | export interface TokenCreationResult { 49 | success: boolean; 50 | tokenMint?: string; 51 | signature?: string; 52 | error?: string; 53 | details?: string; 54 | } 55 | 56 | /** 57 | * Server configuration 58 | * Stored on window object for global access 59 | */ 60 | export interface ServerConfig { 61 | tradingServerUrl: string; 62 | rpcUrl?: string; 63 | serverRegion?: string; 64 | } 65 | 66 | /** 67 | * Trading server instance information 68 | * Used for regional trading server discovery and selection 69 | */ 70 | export interface ServerInfo { 71 | id: string; 72 | name: string; 73 | url: string; 74 | region: string; 75 | flag: string; 76 | ping?: number; 77 | } 78 | /** 79 | * Bundle response from transaction bundle operations 80 | * Used when processing bundle results from Jito or other bundlers 81 | */ 82 | export interface BundleResponse { 83 | bundles?: Array<{ 84 | transactions: string[]; 85 | uuid?: string; 86 | }>; 87 | transactions?: string[]; 88 | signature?: string; 89 | error?: string; 90 | } 91 | 92 | /** 93 | * Token creation response from platform APIs 94 | * Used when creating tokens on Pump.fun, Moon, Bags, etc. 95 | */ 96 | export interface TokenCreationResponse { 97 | tokenCreation?: { 98 | mint: string; 99 | signature?: string; 100 | metadataUri?: string; 101 | }; 102 | mint?: string; 103 | signature?: string; 104 | error?: string; 105 | success?: boolean; 106 | } 107 | 108 | /** 109 | * Extended Window interface with toast notification support 110 | * Used for global toast notifications across the application 111 | */ 112 | export interface WindowWithToast extends Window { 113 | showToast?: (message: string, type: 'success' | 'error') => void; 114 | } 115 | 116 | /** 117 | * Preset tab configuration 118 | * Used for managing preset trading configurations in the UI 119 | */ 120 | export interface PresetTab { 121 | id: string; 122 | name: string; 123 | config?: Record; 124 | isActive?: boolean; 125 | } 126 | 127 | // Note: TradingStats, IframeWallet, and IframeData types have been moved to iframe.ts 128 | // Import them from the central index.ts or directly from iframe.ts 129 | -------------------------------------------------------------------------------- /docs/WHITELABEL.md: -------------------------------------------------------------------------------- 1 | # Whitelabel Guide 2 | 3 | ## Overview 4 | 5 | This guide explains how to customize the Solana UI for your own brand. 6 | 7 | ## Brand Configuration 8 | 9 | All brand-related settings are centralized in `brand.json`: 10 | 11 | ```json 12 | { 13 | "brand": { 14 | "name": "Your Brand", 15 | "displayName": "YOUR BRAND", 16 | "altText": "Your Brand Description", 17 | "domain": "yourdomain.com", 18 | "appUrl": "https://app.yourdomain.com", 19 | "docsUrl": "https://docs.yourdomain.com", 20 | "githubUrl": "https://github.com/yourbrand/repo", 21 | "githubOrg": "https://github.com/yourbrand", 22 | "social": { 23 | "twitter": "@yourbrand", 24 | "github": "yourbrand" 25 | }, 26 | "seo": { 27 | "title": "Your Brand - Trading Platform", 28 | "ogTitle": "Your Brand - Solana Trading", 29 | "description": "Your brand description for SEO", 30 | "ogImage": "https://yourdomain.com/images/og.jpg", 31 | "twitterImage": "https://yourdomain.com/images/twitter.png" 32 | }, 33 | "favicon": { 34 | "baseUrl": "https://yourdomain.com/images/favicon", 35 | "themeColor": "#000000", 36 | "tileColor": "#000000" 37 | }, 38 | "theme": { 39 | "name": "green" 40 | } 41 | } 42 | } 43 | ``` 44 | 45 | ## Customization Steps 46 | 47 | ### 1. Update Brand Configuration 48 | 49 | Edit `brand.json` with your brand details: 50 | 51 | - **name**: Internal brand name 52 | - **displayName**: Display name shown in the UI (typically uppercase) 53 | - **altText**: Alt text for logo images 54 | - **domain**: Your domain without protocol 55 | - **appUrl**: Full URL to your app 56 | - **docsUrl**: Link to your documentation 57 | 58 | ### 2. Replace Logo 59 | 60 | Replace `src/logo.png` with your logo. The logo should: 61 | - Be a PNG with transparent background 62 | - Work well at small sizes (32-64px height) 63 | - Have good contrast against dark backgrounds 64 | 65 | ### 3. Customize Theme 66 | 67 | Create a new CSS file or edit `green.css`: 68 | 69 | ```css 70 | :root { 71 | --color-primary: #your-brand-color; 72 | --color-primary-light: #your-lighter-color; 73 | --color-primary-dark: #your-darker-color; 74 | /* See docs/CUSTOMIZATION.md for all variables */ 75 | } 76 | ``` 77 | 78 | Update the theme name in `brand.json`: 79 | 80 | ```json 81 | { 82 | "theme": { 83 | "name": "your-theme-name" 84 | } 85 | } 86 | ``` 87 | 88 | ### 4. Update Favicon 89 | 90 | Place your favicon files at the URL specified in `brand.json`: 91 | - favicon.ico 92 | - favicon-16x16.png 93 | - favicon-32x32.png 94 | - apple-touch-icon.png 95 | 96 | ### 5. Update SEO 97 | 98 | Configure the SEO section in `brand.json`: 99 | - **title**: Browser tab title 100 | - **ogTitle**: Open Graph title for social sharing 101 | - **description**: Meta description 102 | - **ogImage**: Image for social sharing (1200x630px recommended) 103 | - **twitterImage**: Twitter card image 104 | 105 | ### 6. Regenerate HTML 106 | 107 | After updating `brand.json`, regenerate the HTML template: 108 | 109 | ```bash 110 | npm run generate-html 111 | ``` 112 | 113 | This updates `index.html` with your brand configuration. 114 | 115 | ## Build and Deploy 116 | 117 | ```bash 118 | npm run build 119 | ``` 120 | 121 | Deploy the `dist` folder to your hosting provider. 122 | 123 | ## Additional Resources 124 | 125 | - [Theme Customization](CUSTOMIZATION.md) - Full CSS variable reference 126 | - [Iframe Integration](IFRAME.md) - Embed in external applications 127 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | import { resolve } from 'path'; 4 | import { visualizer } from 'rollup-plugin-visualizer'; 5 | import checker from 'vite-plugin-checker'; 6 | import { nodePolyfills } from 'vite-plugin-node-polyfills'; 7 | 8 | export default defineConfig({ 9 | base: '/', 10 | plugins: [ 11 | react(), 12 | nodePolyfills({ 13 | // Enable polyfills for specific globals and modules 14 | globals: { 15 | Buffer: true, 16 | global: true, 17 | process: true, 18 | }, 19 | // Enable polyfills for specific modules (e.g., crypto, stream, etc.) 20 | protocolImports: true, 21 | }), 22 | checker({ 23 | overlay: true, 24 | typescript: { 25 | tsconfigPath: 'tsconfig.json', 26 | }, 27 | eslint: { 28 | lintCommand: 'eslint "./src/**/*.{ts,tsx}"', 29 | }, 30 | }), 31 | visualizer({ 32 | filename: 'dist/stats.html', 33 | open: true, 34 | gzipSize: true, 35 | brotliSize: true, 36 | }) 37 | ], 38 | resolve: { 39 | alias: { 40 | '@': resolve(__dirname, './src'), 41 | }, 42 | }, 43 | 44 | // Pre-bundle dependencies that have CommonJS issues 45 | optimizeDeps: { 46 | include: [ 47 | '@solana/web3.js', 48 | '@solana/spl-token', 49 | '@solana/spl-token-registry', 50 | 'bs58', 51 | 'buffer', 52 | 'bip39', 53 | 'ed25519-hd-key', 54 | 'crypto-js', 55 | ], 56 | esbuildOptions: { 57 | target: 'es2020', 58 | // Define global for CommonJS modules 59 | define: { 60 | global: 'globalThis', 61 | }, 62 | }, 63 | }, 64 | build: { 65 | rollupOptions: { 66 | output: { 67 | // Simplified chunking - only split truly independent ESM libraries 68 | // Avoid splitting CommonJS modules which can break require() calls 69 | manualChunks: { 70 | // Only split pure ESM libraries that are safe to chunk 71 | 'vendor-react': ['react', 'react-dom'], 72 | 'vendor-router': ['react-router-dom'], 73 | }, 74 | 75 | // Optimize chunk size 76 | chunkFileNames: 'assets/[name]-[hash].js', 77 | entryFileNames: 'assets/[name]-[hash].js', 78 | assetFileNames: 'assets/[name]-[hash].[ext]' 79 | }, 80 | }, 81 | 82 | // Optimize build performance 83 | target: 'es2020', 84 | minify: 'esbuild', // Use esbuild instead of terser for better compatibility 85 | // Note: esbuild minification is faster and more reliable than terser 86 | 87 | // Set chunk size warning limit (increased to 600KB since we're splitting more) 88 | chunkSizeWarningLimit: 600, 89 | 90 | // Enable CSS code splitting 91 | cssCodeSplit: true, 92 | 93 | // Improve tree shaking 94 | reportCompressedSize: true, 95 | 96 | // Source maps for production debugging (optional, can be disabled for smaller builds) 97 | sourcemap: false, 98 | 99 | // CommonJS options for better compatibility 100 | commonjsOptions: { 101 | include: [/node_modules/], 102 | transformMixedEsModules: true, 103 | requireReturnsDefault: 'auto' 104 | } 105 | }, 106 | 107 | // Ensure proper module format 108 | modulePreload: { 109 | polyfill: true 110 | }, 111 | 112 | // Development server configuration 113 | server: { 114 | port: 3010, 115 | host: '0.0.0.0', 116 | 117 | // Allow all hosts (no restrictions) 118 | allowedHosts: true, 119 | 120 | // Add Permissions Policy headers for clipboard access 121 | headers: { 122 | 'Permissions-Policy': 'clipboard-read=*, clipboard-write=*' 123 | }, 124 | 125 | strictPort: false, 126 | 127 | // File system options 128 | fs: { 129 | strict: false 130 | } 131 | }, 132 | 133 | // Preview server configuration (for production builds) 134 | preview: { 135 | port: 3010, 136 | headers: { 137 | 'Permissions-Policy': 'clipboard-read=*, clipboard-write=*' 138 | } 139 | } 140 | }); -------------------------------------------------------------------------------- /src/components/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import { Component, type ReactNode, type ErrorInfo } from 'react'; 2 | import { AlertTriangle, Home, RefreshCw } from 'lucide-react'; 3 | 4 | interface ErrorBoundaryProps { 5 | children: ReactNode; 6 | fallback?: (error: Error, errorInfo: ErrorInfo, reset: () => void) => ReactNode; 7 | } 8 | 9 | interface ErrorBoundaryState { 10 | hasError: boolean; 11 | error: Error | null; 12 | errorInfo: ErrorInfo | null; 13 | } 14 | 15 | class ErrorBoundary extends Component { 16 | constructor(props: ErrorBoundaryProps) { 17 | super(props); 18 | this.state = { 19 | hasError: false, 20 | error: null, 21 | errorInfo: null 22 | }; 23 | } 24 | 25 | static getDerivedStateFromError(_error: Error): Partial { 26 | return { hasError: true }; 27 | } 28 | 29 | override componentDidCatch(error: Error, errorInfo: ErrorInfo): void { 30 | console.error('ErrorBoundary caught an error:', error, errorInfo); 31 | this.setState({ 32 | error, 33 | errorInfo 34 | }); 35 | } 36 | 37 | resetError = (): void => { 38 | this.setState({ 39 | hasError: false, 40 | error: null, 41 | errorInfo: null 42 | }); 43 | }; 44 | 45 | override render(): ReactNode { 46 | if (this.state.hasError) { 47 | if (this.props.fallback && this.state.error && this.state.errorInfo) { 48 | return this.props.fallback(this.state.error, this.state.errorInfo, this.resetError); 49 | } 50 | 51 | // Default error UI 52 | return ( 53 |
54 |
55 |
56 |
57 | 58 |
59 |
60 | 61 |

62 | Something went wrong 63 |

64 | 65 |

66 | An unexpected error occurred. Please try refreshing the page or return to the homepage. 67 |

68 | 69 | {this.state.error && import.meta.env.DEV && ( 70 |
71 |

72 | {this.state.error.toString()} 73 |

74 | {this.state.errorInfo && ( 75 |
 76 |                     {this.state.errorInfo.componentStack}
 77 |                   
78 | )} 79 |
80 | )} 81 | 82 |
83 | 90 | 91 | 98 |
99 |
100 |
101 | ); 102 | } 103 | 104 | return this.props.children; 105 | } 106 | } 107 | 108 | export default ErrorBoundary; 109 | 110 | -------------------------------------------------------------------------------- /src/utils/recentTokens.ts: -------------------------------------------------------------------------------- 1 | import type { RecentToken } from './types'; 2 | 3 | const STORAGE_KEY = 'raze_recent_tokens'; 4 | const MAX_RECENT_TOKENS = 10; 5 | 6 | /** 7 | * Add a token to recent history 8 | */ 9 | export const addRecentToken = (address: string): void => { 10 | try { 11 | const recent = getRecentTokens(); 12 | 13 | // Remove existing entry if present (deduplication) 14 | const filtered = recent.filter(t => t.address !== address); 15 | 16 | // Add new entry at the beginning 17 | const newToken: RecentToken = { 18 | address, 19 | lastViewed: Date.now() 20 | }; 21 | 22 | filtered.unshift(newToken); 23 | 24 | // Keep only MAX_RECENT_TOKENS 25 | const trimmed = filtered.slice(0, MAX_RECENT_TOKENS); 26 | 27 | // Save to localStorage 28 | localStorage.setItem(STORAGE_KEY, JSON.stringify(trimmed)); 29 | } catch (error) { 30 | if (error instanceof DOMException && error.name === 'QuotaExceededError') { 31 | // Storage quota exceeded - clear old data and retry 32 | console.warn('localStorage quota exceeded, clearing recent tokens'); 33 | clearRecentTokens(); 34 | try { 35 | const newToken: RecentToken = { 36 | address, 37 | lastViewed: Date.now() 38 | }; 39 | localStorage.setItem(STORAGE_KEY, JSON.stringify([newToken])); 40 | } catch (retryError) { 41 | console.error('Error adding recent token after clearing:', retryError); 42 | } 43 | } else { 44 | console.error('Error adding recent token:', error); 45 | } 46 | } 47 | }; 48 | 49 | /** 50 | * Get all recent tokens 51 | */ 52 | export const getRecentTokens = (): RecentToken[] => { 53 | try { 54 | const stored = localStorage.getItem(STORAGE_KEY); 55 | if (!stored) return []; 56 | 57 | const parsed: unknown = JSON.parse(stored); 58 | 59 | // Validate data structure 60 | if (!Array.isArray(parsed)) { 61 | console.warn('Invalid recent tokens data structure, clearing'); 62 | clearRecentTokens(); 63 | return []; 64 | } 65 | 66 | // Filter out invalid tokens with proper type guard 67 | const validated = parsed.filter((token: unknown): token is RecentToken => { 68 | if (typeof token !== 'object' || token === null) return false; 69 | const t = token as Record; 70 | const address = t['address']; 71 | const lastViewed = t['lastViewed']; 72 | return ( 73 | typeof address === 'string' && 74 | address.length > 0 && 75 | typeof lastViewed === 'number' && 76 | lastViewed > 0 77 | ); 78 | }); 79 | 80 | // If validation removed items, update storage 81 | if (validated.length !== parsed.length) { 82 | localStorage.setItem(STORAGE_KEY, JSON.stringify(validated)); 83 | } 84 | 85 | return validated; 86 | } catch (error) { 87 | console.error('Error loading recent tokens:', error); 88 | // Clear corrupted data 89 | clearRecentTokens(); 90 | return []; 91 | } 92 | }; 93 | 94 | /** 95 | * Clear all recent tokens 96 | */ 97 | export const clearRecentTokens = (): void => { 98 | try { 99 | localStorage.removeItem(STORAGE_KEY); 100 | } catch (error) { 101 | console.error('Error clearing recent tokens:', error); 102 | } 103 | }; 104 | 105 | /** 106 | * Remove a specific token from recent history 107 | */ 108 | export const removeRecentToken = (address: string): void => { 109 | try { 110 | const recent = getRecentTokens(); 111 | const filtered = recent.filter(t => t.address !== address); 112 | 113 | if (filtered.length === 0) { 114 | clearRecentTokens(); 115 | } else { 116 | localStorage.setItem(STORAGE_KEY, JSON.stringify(filtered)); 117 | } 118 | } catch (error) { 119 | console.error('Error removing recent token:', error); 120 | } 121 | }; 122 | 123 | /** 124 | * Format timestamp to relative time 125 | */ 126 | export const formatTimeAgo = (timestamp: number): string => { 127 | const now = Date.now(); 128 | const diff = now - timestamp; 129 | 130 | const seconds = Math.floor(diff / 1000); 131 | const minutes = Math.floor(seconds / 60); 132 | const hours = Math.floor(minutes / 60); 133 | const days = Math.floor(hours / 24); 134 | 135 | if (days > 0) return `${days}d ago`; 136 | if (hours > 0) return `${hours}h ago`; 137 | if (minutes > 0) return `${minutes}m ago`; 138 | return 'Just now'; 139 | }; 140 | -------------------------------------------------------------------------------- /src/Mobile.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Wallet, TrendingUp, Zap } from 'lucide-react'; 3 | 4 | interface MobileLayoutProps { 5 | currentPage: 'wallets' | 'chart' | 'actions'; 6 | setCurrentPage: (page: 'wallets' | 'chart' | 'actions') => void; 7 | children: { 8 | WalletsPage: React.ReactNode; 9 | Frame: React.ReactNode; 10 | ActionsPage: React.ReactNode; 11 | }; 12 | } 13 | 14 | const MobileLayout: React.FC = ({ 15 | currentPage, 16 | setCurrentPage, 17 | children 18 | }) => { 19 | const tabs = [ 20 | { id: 'wallets' as const, label: 'Wallets', icon: Wallet }, 21 | { id: 'chart' as const, label: 'Chart', icon: TrendingUp }, 22 | { id: 'actions' as const, label: 'Actions', icon: Zap }, 23 | ]; 24 | 25 | return ( 26 |
27 | {/* Content Area */} 28 |
29 | {/* Subtle grid background */} 30 |
40 | 41 | {/* All Pages - Keep mounted, control visibility */} 42 |
43 | {/* Wallets Page */} 44 |
49 | {children.WalletsPage} 50 |
51 | 52 | {/* Chart Page */} 53 |
58 | {children.Frame} 59 |
60 | 61 | {/* Actions Page */} 62 |
67 | {children.ActionsPage} 68 |
69 |
70 |
71 | 72 | {/* Bottom Tab Navigation */} 73 |
77 |
78 | {tabs.map((tab) => { 79 | const Icon = tab.icon; 80 | const isActive = currentPage === tab.id; 81 | 82 | return ( 83 | 121 | ); 122 | })} 123 |
124 |
125 |
126 | ); 127 | }; 128 | 129 | export default MobileLayout; 130 | -------------------------------------------------------------------------------- /src/utils/hdWallet.ts: -------------------------------------------------------------------------------- 1 | import * as bip39 from 'bip39'; 2 | import { derivePath } from 'ed25519-hd-key'; 3 | import { Keypair } from '@solana/web3.js'; 4 | import bs58 from 'bs58'; 5 | 6 | /** 7 | * Solana derivation path standard: m/44'/501'/account'/0' 8 | * 44' = BIP44 9 | * 501' = Solana coin type 10 | * account' = Account index (0, 1, 2, ...) 11 | * 0' = Change index (always 0 for Solana) 12 | */ 13 | export const SOLANA_DERIVATION_PATH = "m/44'/501'"; 14 | 15 | /** 16 | * Generate a new mnemonic phrase 17 | * @param wordCount Number of words (12 or 24) 18 | * @returns Mnemonic phrase as a string 19 | */ 20 | export const generateMnemonic = (wordCount: 12 | 24 = 12): string => { 21 | const strength = wordCount === 12 ? 128 : 256; 22 | return bip39.generateMnemonic(strength); 23 | }; 24 | 25 | /** 26 | * Validate a mnemonic phrase 27 | * @param mnemonic Mnemonic phrase to validate 28 | * @returns True if valid, false otherwise 29 | */ 30 | export const validateMnemonic = (mnemonic: string): boolean => { 31 | return bip39.validateMnemonic(mnemonic); 32 | }; 33 | 34 | /** 35 | * Get the word count from a mnemonic phrase 36 | * @param mnemonic Mnemonic phrase 37 | * @returns Number of words (12 or 24) or null if invalid 38 | */ 39 | export const getMnemonicWordCount = (mnemonic: string): 12 | 24 | null => { 40 | const words = mnemonic.trim().split(/\s+/); 41 | if (words.length === 12) { 42 | return 12; 43 | } 44 | if (words.length === 24) { 45 | return 24; 46 | } 47 | return null; 48 | }; 49 | 50 | /** 51 | * Derive a seed from a mnemonic phrase 52 | * @param mnemonic Mnemonic phrase 53 | * @param passphrase Optional passphrase for additional security 54 | * @returns Seed as Buffer 55 | */ 56 | export const deriveSeedFromMnemonic = ( 57 | mnemonic: string, 58 | passphrase: string = '' 59 | ): Buffer => { 60 | return bip39.mnemonicToSeedSync(mnemonic, passphrase); 61 | }; 62 | 63 | /** 64 | * Build full derivation path for a specific account index 65 | * @param accountIndex Account index (0, 1, 2, ...) 66 | * @returns Full derivation path string 67 | */ 68 | export const buildDerivationPath = (accountIndex: number): string => { 69 | return `${SOLANA_DERIVATION_PATH}/${accountIndex}'/0'`; 70 | }; 71 | 72 | /** 73 | * Derive a wallet from mnemonic at a specific account index 74 | * @param mnemonic Mnemonic phrase 75 | * @param accountIndex Account index to derive (default: 0) 76 | * @param passphrase Optional passphrase 77 | * @returns Object containing address and private key 78 | */ 79 | export const deriveWalletFromMnemonic = ( 80 | mnemonic: string, 81 | accountIndex: number = 0, 82 | passphrase: string = '' 83 | ): { address: string; privateKey: string; derivationPath: string } => { 84 | // Validate mnemonic 85 | if (!validateMnemonic(mnemonic)) { 86 | throw new Error('Invalid mnemonic phrase'); 87 | } 88 | 89 | // Derive seed from mnemonic 90 | const seed = deriveSeedFromMnemonic(mnemonic, passphrase); 91 | 92 | // Build derivation path 93 | const path = buildDerivationPath(accountIndex); 94 | 95 | // Derive key pair from seed using the path 96 | const derivedSeed = derivePath(path, seed.toString('hex')).key; 97 | 98 | // Create Solana keypair from derived seed 99 | const keypair = Keypair.fromSeed(derivedSeed); 100 | 101 | // Get address and private key 102 | const address = keypair.publicKey.toString(); 103 | const privateKey = bs58.encode(keypair.secretKey); 104 | 105 | return { 106 | address, 107 | privateKey, 108 | derivationPath: path, 109 | }; 110 | }; 111 | 112 | /** 113 | * Derive multiple wallets from a single mnemonic 114 | * @param mnemonic Mnemonic phrase 115 | * @param count Number of wallets to derive 116 | * @param startIndex Starting account index (default: 0) 117 | * @param passphrase Optional passphrase 118 | * @returns Array of derived wallets 119 | */ 120 | export const deriveMultipleWallets = ( 121 | mnemonic: string, 122 | count: number, 123 | startIndex: number = 0, 124 | passphrase: string = '' 125 | ): Array<{ address: string; privateKey: string; derivationPath: string; accountIndex: number }> => { 126 | const wallets = []; 127 | 128 | for (let i = 0; i < count; i++) { 129 | const accountIndex = startIndex + i; 130 | const wallet = deriveWalletFromMnemonic(mnemonic, accountIndex, passphrase); 131 | wallets.push({ 132 | ...wallet, 133 | accountIndex, 134 | }); 135 | } 136 | 137 | return wallets; 138 | }; 139 | 140 | /** 141 | * Get the next available account index for a master wallet 142 | * @param currentMaxIndex Current maximum account index used 143 | * @returns Next available account index 144 | */ 145 | export const getNextAccountIndex = (currentMaxIndex: number): number => { 146 | return currentMaxIndex + 1; 147 | }; 148 | 149 | /** 150 | * Format derivation path for display 151 | * @param accountIndex Account index 152 | * @returns Formatted derivation path 153 | */ 154 | export const formatDerivationPath = (accountIndex: number): string => { 155 | return buildDerivationPath(accountIndex); 156 | }; 157 | 158 | -------------------------------------------------------------------------------- /src/contexts/IframeStateContext.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Context for persisting iframe view state 3 | * Caches data per view to avoid re-fetching when switching views 4 | */ 5 | 6 | import React, { createContext, useContext, useState, useCallback, useMemo, type ReactNode } from 'react'; 7 | 8 | export type ViewType = 'holdings' | 'monitor' | 'token'; 9 | 10 | interface ViewState { 11 | tradingStats: { 12 | bought: number; 13 | sold: number; 14 | net: number; 15 | trades: number; 16 | timestamp: number; 17 | } | null; 18 | solPrice: number | null; 19 | currentWallets: Array<{ address: string; label?: string }>; 20 | recentTrades: Array<{ 21 | type: 'buy' | 'sell'; 22 | address: string; 23 | tokensAmount: number; 24 | avgPrice: number; 25 | solAmount: number; 26 | timestamp: number; 27 | signature: string; 28 | }>; 29 | tokenPrice: { 30 | tokenPrice: number; 31 | tokenMint: string; 32 | timestamp: number; 33 | tradeType: 'buy' | 'sell'; 34 | volume: number; 35 | } | null; 36 | marketCap: number | null; 37 | timestamp: number; 38 | } 39 | 40 | interface IframeStateContextType { 41 | getViewState: (view: ViewType, tokenMint?: string) => ViewState | null; 42 | setViewState: (view: ViewType, state: Partial, tokenMint?: string) => void; 43 | clearViewState: (view: ViewType, tokenMint?: string) => void; 44 | clearAllStates: () => void; 45 | } 46 | 47 | const IframeStateContext = createContext(null); 48 | 49 | const CACHE_TTL = 30000; // 30 seconds 50 | const HOLDINGS_CACHE_TTL = 15000; // 15 seconds for holdings (shorter to prevent memory bloat) 51 | const MAX_CACHE_ENTRIES = 20; // Limit total cached views 52 | 53 | const getViewKey = (view: ViewType, tokenMint?: string): string => { 54 | return tokenMint ? `${view}:${tokenMint}` : view; 55 | }; 56 | 57 | const getCacheTTL = (view: ViewType): number => { 58 | // Holdings data can be large, use shorter TTL 59 | return view === 'holdings' ? HOLDINGS_CACHE_TTL : CACHE_TTL; 60 | }; 61 | 62 | export const IframeStateProvider: React.FC<{ children: ReactNode }> = ({ children }) => { 63 | const [viewStates, setViewStates] = useState>(new Map()); 64 | 65 | const getViewState = useCallback((view: ViewType, tokenMint?: string): ViewState | null => { 66 | const key = getViewKey(view, tokenMint); 67 | const state = viewStates.get(key); 68 | 69 | if (!state) return null; 70 | 71 | // Check if cache is still valid (use view-specific TTL) 72 | const now = Date.now(); 73 | const ttl = getCacheTTL(view); 74 | if (now - state.timestamp > ttl) { 75 | // Cache expired, remove it 76 | setViewStates(prev => { 77 | const newMap = new Map(prev); 78 | newMap.delete(key); 79 | return newMap; 80 | }); 81 | return null; 82 | } 83 | 84 | return state; 85 | }, [viewStates]); 86 | 87 | const setViewState = useCallback(( 88 | view: ViewType, 89 | partialState: Partial, 90 | tokenMint?: string 91 | ) => { 92 | const key = getViewKey(view, tokenMint); 93 | setViewStates(prev => { 94 | const newMap = new Map(prev); 95 | const existing = newMap.get(key) || { 96 | tradingStats: null, 97 | solPrice: null, 98 | currentWallets: [], 99 | recentTrades: [], 100 | tokenPrice: null, 101 | marketCap: null, 102 | timestamp: Date.now(), 103 | }; 104 | 105 | newMap.set(key, { 106 | ...existing, 107 | ...partialState, 108 | timestamp: Date.now(), 109 | }); 110 | 111 | // Enforce cache size limit - remove oldest entries 112 | if (newMap.size > MAX_CACHE_ENTRIES) { 113 | const entries = Array.from(newMap.entries()) 114 | .sort((a, b) => a[1].timestamp - b[1].timestamp); 115 | 116 | const entriesToRemove = entries.slice(0, newMap.size - MAX_CACHE_ENTRIES); 117 | entriesToRemove.forEach(([entryKey]) => newMap.delete(entryKey)); 118 | } 119 | 120 | return newMap; 121 | }); 122 | }, []); 123 | 124 | const clearViewState = useCallback((view: ViewType, tokenMint?: string) => { 125 | const key = getViewKey(view, tokenMint); 126 | setViewStates(prev => { 127 | const newMap = new Map(prev); 128 | newMap.delete(key); 129 | return newMap; 130 | }); 131 | }, []); 132 | 133 | const clearAllStates = useCallback(() => { 134 | setViewStates(new Map()); 135 | }, []); 136 | 137 | const value = useMemo(() => ({ 138 | getViewState, 139 | setViewState, 140 | clearViewState, 141 | clearAllStates, 142 | }), [getViewState, setViewState, clearViewState, clearAllStates]); 143 | 144 | return ( 145 | 146 | {children} 147 | 148 | ); 149 | }; 150 | 151 | // eslint-disable-next-line react-refresh/only-export-components 152 | export const useIframeState = (): IframeStateContextType => { 153 | const context = useContext(IframeStateContext); 154 | if (!context) { 155 | throw new Error('useIframeState must be used within IframeStateProvider'); 156 | } 157 | return context; 158 | }; 159 | 160 | -------------------------------------------------------------------------------- /src/utils/iframeManager.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Centralized iframe communication manager 3 | * Handles message queuing, deduplication, and request coalescing 4 | */ 5 | 6 | import type { 7 | NavigateMessage, 8 | WalletMessage, 9 | IframeMessage, 10 | QueuedMessage, 11 | } from './types'; 12 | 13 | // Re-export types for backward compatibility 14 | export type { NavigateMessage, WalletMessage, IframeMessage }; 15 | 16 | class IframeManager { 17 | private iframeWindow: Window | null = null; 18 | private isReady: boolean = false; 19 | private messageQueue: QueuedMessage[] = []; 20 | private lastNavigateMessage: NavigateMessage | null = null; 21 | private lastWalletMessage: WalletMessage | null = null; 22 | private navigateDebounceTimer: ReturnType | null = null; 23 | private readonly NAVIGATE_DEBOUNCE_MS = 100; 24 | 25 | /** 26 | * Set the iframe window reference 27 | */ 28 | setIframeWindow(window: Window | null): void { 29 | this.iframeWindow = window; 30 | } 31 | 32 | /** 33 | * Mark iframe as ready and process queued messages 34 | */ 35 | setReady(ready: boolean): void { 36 | this.isReady = ready; 37 | if (ready) { 38 | this.processQueue(); 39 | } 40 | } 41 | 42 | /** 43 | * Check if iframe is ready 44 | */ 45 | getReady(): boolean { 46 | return this.isReady; 47 | } 48 | 49 | /** 50 | * Send a message to the iframe with deduplication 51 | */ 52 | sendMessage(message: IframeMessage): void { 53 | if (!this.isReady || !this.iframeWindow) { 54 | this.messageQueue.push({ message, timestamp: Date.now() }); 55 | return; 56 | } 57 | 58 | // Deduplicate navigation messages 59 | if (message.type === 'NAVIGATE') { 60 | this.sendNavigateMessage(message); 61 | return; 62 | } 63 | 64 | // Deduplicate wallet messages 65 | if (message.type === 'ADD_WALLETS' || message.type === 'CLEAR_WALLETS') { 66 | this.sendWalletMessage(message); 67 | return; 68 | } 69 | 70 | // Send other messages immediately 71 | this.iframeWindow.postMessage(message, '*'); 72 | } 73 | 74 | /** 75 | * Send navigation message with debouncing and deduplication 76 | */ 77 | private sendNavigateMessage(message: NavigateMessage): void { 78 | // Clear any pending navigation 79 | if (this.navigateDebounceTimer) { 80 | clearTimeout(this.navigateDebounceTimer); 81 | this.navigateDebounceTimer = null; 82 | } 83 | 84 | // Check if this is a duplicate of the last message 85 | if (this.isDuplicateNavigate(message)) { 86 | return; 87 | } 88 | 89 | // Debounce rapid navigation changes 90 | this.navigateDebounceTimer = setTimeout(() => { 91 | if (this.iframeWindow) { 92 | this.lastNavigateMessage = message; 93 | this.iframeWindow.postMessage(message, '*'); 94 | } 95 | this.navigateDebounceTimer = null; 96 | }, this.NAVIGATE_DEBOUNCE_MS); 97 | } 98 | 99 | /** 100 | * Send wallet message with deduplication 101 | */ 102 | private sendWalletMessage(message: WalletMessage): void { 103 | // Check if this is a duplicate 104 | if (this.isDuplicateWallet(message)) { 105 | return; 106 | } 107 | 108 | this.lastWalletMessage = message; 109 | if (this.iframeWindow) { 110 | this.iframeWindow.postMessage(message, '*'); 111 | } 112 | } 113 | 114 | /** 115 | * Check if navigation message is duplicate 116 | */ 117 | private isDuplicateNavigate(message: NavigateMessage): boolean { 118 | if (!this.lastNavigateMessage) return false; 119 | 120 | const last = this.lastNavigateMessage; 121 | return ( 122 | last.view === message.view && 123 | last.tokenMint === message.tokenMint && 124 | JSON.stringify(last.wallets?.sort()) === JSON.stringify(message.wallets?.sort()) 125 | ); 126 | } 127 | 128 | /** 129 | * Check if wallet message is duplicate 130 | */ 131 | private isDuplicateWallet(message: WalletMessage): boolean { 132 | if (!this.lastWalletMessage) return false; 133 | 134 | const last = this.lastWalletMessage; 135 | if (last.type !== message.type) return false; 136 | 137 | if (message.type === 'CLEAR_WALLETS') { 138 | return last.type === 'CLEAR_WALLETS'; 139 | } 140 | 141 | // Compare wallet arrays for ADD_WALLETS 142 | const normalizeWallets = (wallets?: Array): string[] => { 143 | if (!wallets) return []; 144 | return wallets.map(w => typeof w === 'string' ? w : w.address).sort(); 145 | }; 146 | 147 | const lastWallets = normalizeWallets(last.wallets); 148 | const currentWallets = normalizeWallets(message.wallets); 149 | return JSON.stringify(lastWallets) === JSON.stringify(currentWallets); 150 | } 151 | 152 | /** 153 | * Process queued messages 154 | */ 155 | private processQueue(): void { 156 | while (this.messageQueue.length > 0 && this.isReady && this.iframeWindow) { 157 | const queued = this.messageQueue.shift(); 158 | if (queued) { 159 | this.sendMessage(queued.message); 160 | } 161 | } 162 | this.messageQueue = []; 163 | } 164 | 165 | /** 166 | * Clear all queued messages 167 | */ 168 | clearQueue(): void { 169 | this.messageQueue = []; 170 | if (this.navigateDebounceTimer) { 171 | clearTimeout(this.navigateDebounceTimer); 172 | this.navigateDebounceTimer = null; 173 | } 174 | } 175 | 176 | /** 177 | * Reset state (useful for cleanup) 178 | */ 179 | reset(): void { 180 | this.clearQueue(); 181 | this.lastNavigateMessage = null; 182 | this.lastWalletMessage = null; 183 | this.isReady = false; 184 | this.iframeWindow = null; 185 | } 186 | } 187 | 188 | // Singleton instance 189 | export const iframeManager = new IframeManager(); 190 | 191 | -------------------------------------------------------------------------------- /src/components/Notifications.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useRef } from "react" 2 | import { AlertCircle, X, ZapIcon } from "lucide-react" 3 | import { ToastContext } from "./ToastContext" 4 | 5 | interface Toast { 6 | id: number 7 | message: string 8 | type: 'success' | 'error' 9 | } 10 | 11 | interface ToastProviderProps { 12 | children: React.ReactNode 13 | } 14 | 15 | // Custom styled toast animations 16 | const Animations = ` 17 | @keyframes slide-in { 18 | 0% { 19 | transform: translateX(100%); 20 | opacity: 0; 21 | } 22 | 10% { 23 | transform: translateX(-10px); 24 | opacity: 0.8; 25 | } 26 | 15% { 27 | transform: translateX(5px); 28 | } 29 | 20% { 30 | transform: translateX(0); 31 | opacity: 1; 32 | } 33 | 90% { 34 | transform: translateX(0); 35 | opacity: 1; 36 | } 37 | 100% { 38 | transform: translateX(100%); 39 | opacity: 0; 40 | } 41 | } 42 | 43 | @keyframes glow { 44 | 0% { 45 | box-shadow: 0 0 5px var(--color-primary-70); 46 | } 47 | 50% { 48 | box-shadow: 0 0 15px var(--color-primary-90), 0 0 30px var(--color-primary-50); 49 | } 50 | 100% { 51 | box-shadow: 0 0 5px var(--color-primary-70); 52 | } 53 | } 54 | 55 | @keyframes error-glow { 56 | 0% { 57 | box-shadow: 0 0 5px var(--color-error-70); 58 | } 59 | 50% { 60 | box-shadow: 0 0 15px var(--color-error-90), 0 0 30px var(--color-error-50); 61 | } 62 | 100% { 63 | box-shadow: 0 0 5px var(--color-error-70); 64 | } 65 | } 66 | 67 | @keyframes scanline { 68 | 0% { 69 | background-position: 0 0; 70 | } 71 | 100% { 72 | background-position: 0 100%; 73 | } 74 | } 75 | 76 | @keyframes text-glitch { 77 | 0% { 78 | text-shadow: 0 0 0 var(--color-text-secondary-90); 79 | } 80 | 5% { 81 | text-shadow: -2px 0 0 rgba(255, 0, 128, 0.8), 2px 0 0 rgba(0, 255, 255, 0.8); 82 | } 83 | 10% { 84 | text-shadow: 0 0 0 var(--color-text-secondary-90); 85 | } 86 | 15% { 87 | text-shadow: -2px 0 0 rgba(255, 0, 128, 0.8), 2px 0 0 rgba(0, 255, 255, 0.8); 88 | } 89 | 20% { 90 | text-shadow: 0 0 0 var(--color-text-secondary-90); 91 | } 92 | 100% { 93 | text-shadow: 0 0 0 var(--color-text-secondary-90); 94 | } 95 | } 96 | ` 97 | 98 | // CSS classes for styling 99 | const Classes = { 100 | successToast: "relative bg-app-primary border border-app-primary text-app-primary animate-[glow_2s_infinite]", 101 | errorToast: "relative bg-app-primary border border-error text-app-primary animate-[error-glow_2s_infinite]", 102 | scanline: "absolute inset-0 pointer-events-none bg-gradient-scanline-primary bg-[size:100%_4px] animate-[scanline_4s_linear_infinite] opacity-40", 103 | errorScanline: "absolute inset-0 pointer-events-none bg-gradient-scanline-error bg-[size:100%_4px] animate-[scanline_4s_linear_infinite] opacity-40", 104 | icon: "h-5 w-5 color-primary", 105 | errorIcon: "h-5 w-5 text-error", 106 | message: "font-mono tracking-wider animate-[text-glitch_3s_infinite]", 107 | closeButton: "ml-2 rounded-full p-1 hover:bg-primary-40 text-app-secondary transition-colors duration-300", 108 | errorCloseButton: "ml-2 rounded-full p-1 hover:bg-error-40 text-error-light transition-colors duration-300" 109 | } 110 | 111 | export const ToastProvider = ({ children }: ToastProviderProps): JSX.Element => { 112 | const [toasts, setToasts] = useState([]) 113 | const counterRef = useRef(0) 114 | 115 | const showToast = (message: string, type: 'success' | 'error'): void => { 116 | const id = Date.now() + counterRef.current 117 | counterRef.current += 1 118 | setToasts(prev => [...prev, { id, message, type }]) 119 | } 120 | 121 | const closeToast = (id: number): void => { 122 | setToasts(prev => prev.filter(toast => toast.id !== id)) 123 | } 124 | 125 | useEffect(() => { 126 | if (toasts.length > 0) { 127 | const timer = setTimeout(() => { 128 | setToasts(prev => prev.slice(1)) 129 | }, 2000) // Increased duration to 5 seconds to enjoy the effects 130 | return () => clearTimeout(timer) 131 | } 132 | return undefined 133 | }, [toasts]) 134 | return ( 135 | 136 | {children} 137 | {/* Add the custom animations to the DOM */} 138 | 139 | 140 |
141 | {toasts.map(toast => ( 142 |
149 | {/* Scanline effect */} 150 |
151 | 152 | {/* Corner accents for border effect */} 153 |
154 |
155 |
156 |
157 | 158 | {/* Icon and content */} 159 | {toast.type === 'success' ? ( 160 | 161 | ) : ( 162 | 163 | )} 164 |

{toast.message}

165 | 171 |
172 | ))} 173 |
174 |
175 | ) 176 | } 177 | 178 | export default ToastProvider -------------------------------------------------------------------------------- /src/utils/types/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Central Type Exports 3 | * 4 | * This module re-exports all types from the type system. 5 | * Import types from this file for convenience. 6 | * 7 | * @example 8 | * import type { WalletType, BuyConfig, TradingStrategy } from '@/utils/types'; 9 | * // or 10 | * import type { WalletType } from '../utils/types'; 11 | */ 12 | 13 | // ============================================================================ 14 | // API Types 15 | // ============================================================================ 16 | 17 | export type { 18 | ApiResponse, 19 | BundleResult, 20 | SimpleBundleResult, 21 | TokenCreationResult, 22 | ServerConfig, 23 | ServerInfo, 24 | BundleResponse, 25 | TokenCreationResponse, 26 | WindowWithToast, 27 | PresetTab as ApiPresetTab, 28 | } from './api'; 29 | 30 | // ============================================================================ 31 | // Wallet Types 32 | // ============================================================================ 33 | 34 | export type { 35 | WalletCategory, 36 | WalletSource, 37 | MasterWallet, 38 | CustomQuickTradeSettings, 39 | QuickBuyPreferences, 40 | WalletType, 41 | ConfigType, 42 | RecentToken, 43 | CategoryQuickTradeSettings, 44 | WalletBalance, 45 | WalletAmount, 46 | WalletImportResult, 47 | WalletExportData, 48 | WalletSelectionState, 49 | WalletSortDirection, 50 | WalletSortField, 51 | WalletFilterOptions, 52 | } from './wallet'; 53 | 54 | // ============================================================================ 55 | // Trading Types 56 | // ============================================================================ 57 | 58 | export type { 59 | BundleMode, 60 | ScriptType, 61 | WalletBuy, 62 | WalletSell, 63 | BuyConfig, 64 | SellConfig, 65 | ServerResponse, 66 | BuyBundle, 67 | SellBundle, 68 | BuyResult, 69 | SellResult, 70 | TradeHistoryEntry, 71 | AddTradeHistoryInput, 72 | ValidationResult, 73 | TradingState, 74 | TradingFormValues, 75 | TokenPrice, 76 | TokenMarketData, 77 | QuickTradeParams, 78 | QuickTradeResult, 79 | } from './trading'; 80 | 81 | // ============================================================================ 82 | // WebSocket Types 83 | // ============================================================================ 84 | 85 | export type { 86 | WebSocketWelcomeMessage, 87 | WebSocketConnectionMessage, 88 | WebSocketSubscriptionMessage, 89 | WebSocketTradeMessage, 90 | WebSocketErrorMessage, 91 | WebSocketMessage, 92 | TradeTransactionData, 93 | AutomateTrade, 94 | AutomateWebSocketConfig, 95 | MultiTokenWebSocketConfig, 96 | CopyTradeData as WebSocketCopyTradeData, 97 | CopyTradeWebSocketConfig, 98 | WebSocketConnectionState, 99 | WebSocketStatus, 100 | TokenSubscriptionRequest, 101 | SignerSubscriptionRequest, 102 | WebSocketSubscriptionRequest, 103 | WebSocketEventHandlers, 104 | WebSocketConstants, 105 | WebSocketPriceInfo, 106 | WebSocketAuthConfig, 107 | WebSocketAuthErrorCode, 108 | WebSocketAuthError, 109 | } from './websocket'; 110 | 111 | // ============================================================================ 112 | // Automation Types 113 | // ============================================================================ 114 | 115 | export type { 116 | TradingConditionType, 117 | ConditionOperator, 118 | TradingCondition, 119 | ActionAmountType, 120 | VolumeType, 121 | ActionPriority, 122 | TradingAction, 123 | ConditionLogic, 124 | CooldownUnit, 125 | TradingStrategy, 126 | WalletList, 127 | CopyTradeMode, 128 | TokenFilterMode, 129 | CopyTradeConditionType, 130 | CopyTradeCondition, 131 | CopyTradeAmountType, 132 | CopyTradeAction, 133 | SimpleModeCopyConfig, 134 | CopyTradeProfile, 135 | CopyTradeData, 136 | CopyTradeExecutionLog, 137 | StrategyExecutionLog, 138 | CopyTradeProfileStorage, 139 | TradingStrategyStorage, 140 | WhitelistEntry, 141 | WhitelistConfig, 142 | } from './automation'; 143 | 144 | // ============================================================================ 145 | // Component Types 146 | // ============================================================================ 147 | 148 | export type { 149 | ToastType, 150 | Toast, 151 | PresetTab, 152 | FundingMode, 153 | FundModalProps, 154 | TransferModalProps, 155 | MixerModalProps, 156 | ConsolidateModalProps, 157 | DistributeModalProps, 158 | DepositModalProps, 159 | CreateWalletModalProps, 160 | ImportWalletModalProps, 161 | CreateMasterWalletModalProps, 162 | ExportSeedPhraseModalProps, 163 | BurnModalProps, 164 | QuickTradeModalProps, 165 | WalletQuickTradeModalProps, 166 | CalculatePNLModalProps, 167 | MobilePage, 168 | MobileLayoutProps, 169 | WalletsPageProps, 170 | DeployPageProps, 171 | SettingsPageProps, 172 | PresetButtonProps, 173 | TabButtonProps, 174 | ServiceButtonProps, 175 | WalletAmount as ComponentWalletAmount, 176 | TransferStatus, 177 | TransferQueueItem, 178 | BalanceFilter, 179 | SortOption, 180 | SortDirection, 181 | WalletListFilterState, 182 | ModalStep, 183 | ModalState, 184 | MenuItem, 185 | HeaderProps, 186 | NotificationItem, 187 | NotificationsProps, 188 | } from './components'; 189 | 190 | // ============================================================================ 191 | // Iframe Types 192 | // ============================================================================ 193 | 194 | export type { 195 | ViewType, 196 | NavigateMessage, 197 | AddWalletsMessage, 198 | ClearWalletsMessage, 199 | GetWalletsMessage, 200 | WalletMessage, 201 | ToggleNonWhitelistedTradesMessage, 202 | SetQuickBuyConfigMessage, 203 | QuickBuyActivateMessage, 204 | QuickBuyDeactivateMessage, 205 | IframeMessage, 206 | IframeReadyResponse, 207 | WalletsAddedResponse, 208 | WalletsClearedResponse, 209 | CurrentWalletsResponse, 210 | WhitelistTradingStatsResponse, 211 | SolPriceUpdateResponse, 212 | WhitelistTradeResponse, 213 | TokenPriceUpdateResponse, 214 | TokenSelectedResponse, 215 | NonWhitelistedTradeResponse, 216 | HoldingsOpenedResponse, 217 | TokenClearedResponse, 218 | NavigationCompleteResponse, 219 | IframeResponse, 220 | TradingStats, 221 | IframeWallet, 222 | IframeRecentTrade, 223 | IframeTokenPrice, 224 | IframeData, 225 | ViewState, 226 | IframeStateContextType, 227 | QueuedMessage, 228 | IframeManagerState, 229 | FrameProps, 230 | } from './iframe'; 231 | -------------------------------------------------------------------------------- /src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { useNavigate, useLocation } from 'react-router-dom'; 3 | import { Settings, Bot, Blocks, Wallet, TrendingUp, BookOpen } from 'lucide-react'; 4 | import { brand } from '../utils/brandConfig'; 5 | import logo from '../logo.png'; 6 | 7 | interface HeaderProps { 8 | tokenAddress?: string; 9 | onNavigateHome?: () => void; 10 | showToast?: (message: string, type: 'success' | 'error') => void; 11 | } 12 | 13 | const VerticalSidebar: React.FC = () => { 14 | const navigate = useNavigate(); 15 | const location = useLocation(); 16 | 17 | const handleWalletsClick = (): void => { 18 | navigate('/wallets'); 19 | }; 20 | 21 | const handleMonitorClick = (): void => { 22 | navigate('/monitor'); 23 | }; 24 | 25 | const handleAutomateClick = (): void => { 26 | navigate('/automate'); 27 | }; 28 | 29 | 30 | const handleDeployClick = (): void => { 31 | navigate('/deploy'); 32 | }; 33 | 34 | const handleSettingsClick = (): void => { 35 | navigate('/settings'); 36 | }; 37 | 38 | const handleDocsClick = (): void => { 39 | window.open(brand.docsUrl, '_blank', 'noopener,noreferrer'); 40 | }; 41 | 42 | const isActive = (path: string): boolean => { 43 | return location.pathname === path; 44 | }; 45 | 46 | const getButtonClassName = (path: string): string => { 47 | const active = isActive(path); 48 | return `group flex flex-col items-center justify-center gap-2 px-3 py-4 rounded-lg transition-all duration-200 w-full 49 | ${active 50 | ? 'bg-primary-20 border border-app-primary-80 color-primary shadow-inner-black-80' 51 | : 'bg-app-primary-60 border border-app-primary-40 text-app-secondary-60 hover-border-primary-40 hover-text-app-secondary hover-bg-app-primary-80-alpha' 52 | }`; 53 | }; 54 | 55 | const getIconClassName = (path: string): string => { 56 | const active = isActive(path); 57 | return `transition-all duration-200 ${active ? 'color-primary' : 'text-app-secondary-60 group-hover:color-primary'}`; 58 | }; 59 | 60 | return ( 61 |
62 | {/* Wallets */} 63 | 72 | 73 | {/* Trade */} 74 | 83 | 84 | {/* Automate */} 85 | 94 | 95 | {/* Deploy */} 96 | 105 | 106 | {/* Documentation */} 107 | 117 | 118 | {/* Settings */} 119 | 128 |
129 | ); 130 | }; 131 | 132 | export const HomepageHeader: React.FC = ({ 133 | onNavigateHome, 134 | }) => { 135 | const navigate = useNavigate(); 136 | 137 | const handleLogoClick = useCallback(() => { 138 | onNavigateHome?.(); 139 | navigate('/'); 140 | }, [onNavigateHome, navigate]); 141 | 142 | return ( 143 | 161 | ); 162 | }; 163 | 164 | // Unified header component for all pages 165 | export const UnifiedHeader: React.FC = () => { 166 | const navigate = useNavigate(); 167 | 168 | const handleLogoClick = useCallback((): void => { 169 | navigate('/'); 170 | }, [navigate]); 171 | 172 | return ( 173 | 191 | ); 192 | }; 193 | 194 | export default HomepageHeader; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Built on Solana 3 | Open Source 4 |

5 | 6 | ## 🚀 One-Click Deployment 7 | 8 | You can deploy **Raze.bot** instantly using either **Vercel** or **Netlify** with the buttons below: 9 | 10 |
11 | 12 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/razedotbot/solana-ui) 13 | [![Deploy with Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/razedotbot/solana-ui) 14 | 15 |
16 | 17 | ![Raze.bot image](https://i.imgur.com/lpC1qju.png) 18 | 19 | **Raze.bot** is a comprehensive, open-source multi-wallet trading platform designed for the Solana blockchain. 20 | It provides users with a seamless interface to connect various Solana wallets and execute trades efficiently. 21 | 22 | > 🛠️ This project was developed by the team at [**Raze.bot**](https://raze.bot) using [**Raze APIs**](https://docs.raze.bot) to power its backend integrations and blockchain interactions. 23 | 24 | --- 25 | 26 | ## 📚 Documentation 27 | 28 | Find the full documentation here: 29 | 👉 [https://docs.raze.bot/solana-ui/introduction](https://docs.raze.bot/solana-ui/introduction) 30 | 31 | --- 32 | 33 | ## ✨ Features 34 | 35 | ### Wallet Management 36 | - 🔑 **Multi-Wallet Support** – Create, import, and manage multiple Solana wallets with HD wallet derivation 37 | - � **Import Options** – Private key import (Base58), seed phrase recovery, bulk import from file 38 | - 🏷️ **Wallet Organization** – Custom labels, categories (Soft, Medium, Hard), drag & drop reordering 39 | - 💰 **Wallet Operations** – Fund/Distribute, Consolidate, Transfer, Deposit, Burn tokens 40 | 41 | ### Trading Features 42 | - 📈 **Quick Trade** – One-click buy/sell with customizable preset buttons 43 | - � **Multi-Wallet Trading** – Execute trades across all wallets simultaneously 44 | - 📊 **Bundle Strategies** – Single Thread, Batch Mode, or All-In-One execution 45 | - 📋 **Limit Orders** – Market cap triggers, price targets, expiry dates 46 | 47 | ### Automation Tools 48 | - 🎯 **Sniper Bot** – Automatically snipe new token launches with configurable filters 49 | - 👥 **Copy Trading** – Mirror trades from successful wallets in real-time 50 | - ⚙️ **Custom Profiles** – Create automation profiles with conditions and actions 51 | 52 | ### Token Deployment 53 | - 🚀 **Multi-Platform** – Deploy to Pump.fun, Bonk.fun, Meteora, and more 54 | - 🎨 **Full Customization** – Token metadata, social links, image upload 55 | - 📦 **Multi-Wallet Bundling** – Deploy with up to 5-20 wallets depending on platform 56 | 57 | ### Additional Features 58 | - 📱 **Responsive Design** – Optimized for both desktop and mobile devices 59 | - 🎨 **Whitelabel Support** – Customizable themes, branding, and CSS variables 60 | - 🔒 **Security First** – AES encryption, local-first storage, fully auditable codebase 61 | - 🖼️ **Iframe Integration** – Embed Raze in your application via iframe 62 | 63 | --- 64 | 65 | ## 🚀 Demo 66 | 67 | Try the live version here: 68 | 👉 [https://sol.raze.bot/](https://sol.raze.bot/) 69 | 70 | --- 71 | 72 | ## 🧰 Getting Started 73 | 74 | ### Prerequisites 75 | 76 | - [Node.js](https://nodejs.org/) (v18 or later) 77 | - [npm](https://www.npmjs.com/) (comes with Node.js) 78 | 79 | ### Installation 80 | 81 | ```bash 82 | git clone https://github.com/razedotbot/solana-ui.git 83 | cd solana-ui 84 | npm install 85 | npm run dev 86 | ``` 87 | 88 | Visit: `http://localhost:5173` 89 | 90 | --- 91 | 92 | ## 🗂 Project Structure 93 | 94 | ``` 95 | solana-ui/ 96 | ├── src/ 97 | │ ├── components/ # Reusable UI components 98 | │ │ ├── modals/ # Modal dialogs 99 | │ │ └── tools/ # Trading tools & automation 100 | │ ├── contexts/ # React contexts 101 | │ ├── pages/ # Page components 102 | │ │ ├── HomePage.tsx 103 | │ │ ├── WalletsPage.tsx 104 | │ │ ├── AutomatePage.tsx 105 | │ │ ├── DeployPage.tsx 106 | │ │ └── SettingsPage.tsx 107 | │ └── utils/ # Utility functions 108 | ├── docs/ # Documentation 109 | ├── scripts/ # Build scripts 110 | ├── brand.json # Brand configuration 111 | ├── green.css # Default theme 112 | └── package.json 113 | ``` 114 | 115 | --- 116 | 117 | ## 📜 Available Scripts 118 | 119 | | Command | Description | 120 | |---------|-------------| 121 | | `npm run dev` | Start development server with hot reload | 122 | | `npm run build` | Build for production | 123 | | `npm run preview` | Preview production build locally | 124 | | `npm run lint` | Run ESLint for code quality | 125 | | `npm run typecheck` | Run TypeScript type checking | 126 | | `npm run analyze` | Analyze bundle size | 127 | | `npm run generate-html` | Regenerate HTML from template | 128 | 129 | --- 130 | 131 | ## 🧪 Technologies Used 132 | 133 | - [React 18](https://reactjs.org/) 134 | - [TypeScript](https://www.typescriptlang.org/) 135 | - [Vite](https://vitejs.dev/) 136 | - [Tailwind CSS](https://tailwindcss.com/) 137 | - [Solana Web3.js](https://solana-labs.github.io/solana-web3.js/) 138 | - [Jupiter API](https://jup.ag/) 139 | 140 | --- 141 | 142 | ## 🔧 Configuration 143 | 144 | ### Brand Configuration 145 | 146 | Customize branding by editing `brand.json`: 147 | 148 | ```json 149 | { 150 | "brand": { 151 | "name": "Your Brand", 152 | "displayName": "YOUR BRAND", 153 | "domain": "yourdomain.com", 154 | "appUrl": "https://app.yourdomain.com", 155 | "docsUrl": "https://docs.yourdomain.com", 156 | "theme": { 157 | "name": "green" 158 | } 159 | } 160 | } 161 | ``` 162 | 163 | After editing, regenerate HTML: 164 | 165 | ```bash 166 | npm run generate-html 167 | ``` 168 | 169 | ### Theme Customization 170 | 171 | Edit `green.css` or create a new theme file: 172 | 173 | ```css 174 | :root { 175 | /* Primary Colors */ 176 | --color-primary: #02b36d; 177 | --color-primary-light: #04d47c; 178 | --color-primary-dark: #01a35f; 179 | 180 | /* Background Colors */ 181 | --color-bg-primary: #050a0e; 182 | --color-bg-secondary: #0a1419; 183 | } 184 | ``` 185 | 186 | --- 187 | 188 | ## 🔒 Security 189 | 190 | Raze prioritizes security at every level: 191 | 192 | - **Encrypted Storage** – All wallet private keys are encrypted using AES encryption before storage 193 | - **Local-First** – Your keys never leave your device - all encryption happens client-side 194 | - **Dual Storage** – Redundant storage in localStorage and IndexedDB for reliability 195 | - **Open Source** – Fully auditable codebase - verify the security yourself 196 | 197 | --- 198 | 199 | ## 🌐 Community & Support 200 | 201 |

202 | GitHub 203 | Telegram 204 | Discord 205 |

206 | 207 | --- 208 | 209 | ## 🤝 Contributing 210 | 211 | Contributions are welcome! 212 | Fork the repo and open a pull request for new features, improvements, or bug fixes. 213 | 214 | --- 215 | 216 | ## 📄 License 217 | 218 | This project is licensed under the [MIT License](LICENSE). 219 | -------------------------------------------------------------------------------- /src/utils/rpcManager.ts: -------------------------------------------------------------------------------- 1 | import { Connection } from '@solana/web3.js'; 2 | 3 | export interface RPCEndpoint { 4 | id: string; 5 | url: string; 6 | name: string; 7 | isActive: boolean; 8 | priority: number; // Lower number = higher priority 9 | weight: number; // Weight for selection (0-100, should total 100 for all active endpoints) 10 | lastUsed?: number; 11 | failureCount: number; 12 | lastFailure?: number; 13 | } 14 | 15 | export class RPCManager { 16 | private endpoints: RPCEndpoint[]; 17 | private currentIndex: number = 0; 18 | private maxFailures: number = 3; 19 | private failureResetTime: number = 60000; // 1 minute 20 | 21 | constructor(endpoints: RPCEndpoint[]) { 22 | this.endpoints = this.sortEndpoints(endpoints.filter(e => e.isActive)); 23 | if (this.endpoints.length === 0) { 24 | throw new Error('At least one active RPC endpoint is required'); 25 | } 26 | } 27 | 28 | private sortEndpoints(endpoints: RPCEndpoint[]): RPCEndpoint[] { 29 | return [...endpoints].sort((a, b) => { 30 | // Sort by priority first, then by failure count 31 | if (a.priority !== b.priority) { 32 | return a.priority - b.priority; 33 | } 34 | return a.failureCount - b.failureCount; 35 | }); 36 | } 37 | 38 | private selectEndpointByWeight(availableEndpoints: RPCEndpoint[]): RPCEndpoint | null { 39 | if (availableEndpoints.length === 0) return null; 40 | if (availableEndpoints.length === 1) return availableEndpoints[0]; 41 | 42 | // Calculate total weight of available endpoints 43 | const totalWeight = availableEndpoints.reduce((sum, e) => sum + (e.weight || 0), 0); 44 | 45 | if (totalWeight === 0) { 46 | // If no weights set, use round-robin 47 | this.currentIndex = (this.currentIndex + 1) % availableEndpoints.length; 48 | return availableEndpoints[this.currentIndex]; 49 | } 50 | 51 | // Weighted random selection 52 | let random = Math.random() * totalWeight; 53 | for (const endpoint of availableEndpoints) { 54 | random -= (endpoint.weight || 0); 55 | if (random <= 0) { 56 | return endpoint; 57 | } 58 | } 59 | 60 | // Fallback to first endpoint 61 | return availableEndpoints[0]; 62 | } 63 | 64 | private resetFailureIfNeeded(endpoint: RPCEndpoint): void { 65 | if (endpoint.lastFailure && Date.now() - endpoint.lastFailure > this.failureResetTime) { 66 | endpoint.failureCount = 0; 67 | endpoint.lastFailure = undefined; 68 | } 69 | } 70 | 71 | private markFailure(endpoint: RPCEndpoint): void { 72 | endpoint.failureCount++; 73 | endpoint.lastFailure = Date.now(); 74 | } 75 | 76 | private markSuccess(endpoint: RPCEndpoint): void { 77 | endpoint.lastUsed = Date.now(); 78 | endpoint.failureCount = 0; 79 | endpoint.lastFailure = undefined; 80 | } 81 | 82 | private getNextEndpoint(): RPCEndpoint | null { 83 | // Reset failures for endpoints that have been idle long enough 84 | this.endpoints.forEach(e => this.resetFailureIfNeeded(e)); 85 | 86 | // Filter out endpoints that have exceeded max failures 87 | const availableEndpoints = this.endpoints.filter( 88 | e => e.failureCount < this.maxFailures 89 | ); 90 | 91 | if (availableEndpoints.length === 0) { 92 | // All endpoints have failed, reset all failure counts and try again 93 | this.endpoints.forEach(e => { 94 | e.failureCount = 0; 95 | e.lastFailure = undefined; 96 | }); 97 | return this.selectEndpointByWeight(this.endpoints) || this.endpoints[0] || null; 98 | } 99 | 100 | // Use weighted selection 101 | return this.selectEndpointByWeight(availableEndpoints); 102 | } 103 | 104 | public createConnection(): Promise { 105 | const errors: Error[] = []; 106 | 107 | // Try each endpoint until one succeeds 108 | for (let attempt = 0; attempt < this.endpoints.length; attempt++) { 109 | const endpoint = this.getNextEndpoint(); 110 | 111 | if (!endpoint) { 112 | return Promise.reject(new Error('No RPC endpoints available')); 113 | } 114 | 115 | try { 116 | const connection = new Connection(endpoint.url, 'confirmed'); 117 | 118 | // Return connection without testing (errors will be caught when actually used) 119 | this.markSuccess(endpoint); 120 | return Promise.resolve(connection); 121 | } catch (error) { 122 | this.markFailure(endpoint); 123 | errors.push(error instanceof Error ? error : new Error(String(error))); 124 | 125 | // Continue to next endpoint 126 | continue; 127 | } 128 | } 129 | 130 | // All endpoints failed 131 | return Promise.reject( 132 | new Error( 133 | `All RPC endpoints failed. Errors: ${errors.map(e => e.message).join(', ')}` 134 | ) 135 | ); 136 | } 137 | 138 | public getCurrentEndpoint(): RPCEndpoint | null { 139 | const availableEndpoints = this.endpoints.filter( 140 | e => e.failureCount < this.maxFailures 141 | ); 142 | return availableEndpoints[this.currentIndex] || this.endpoints[0] || null; 143 | } 144 | 145 | public getAllEndpoints(): RPCEndpoint[] { 146 | return [...this.endpoints]; 147 | } 148 | 149 | public updateEndpoints(endpoints: RPCEndpoint[]): void { 150 | // Normalize weights to ensure they total 100 for active endpoints 151 | const activeEndpoints = endpoints.filter(e => e.isActive); 152 | const totalWeight = activeEndpoints.reduce((sum, e) => sum + (e.weight || 0), 0); 153 | 154 | if (totalWeight > 0 && activeEndpoints.length > 0) { 155 | // Normalize weights proportionally 156 | endpoints.forEach(e => { 157 | if (e.isActive && e.weight !== undefined) { 158 | e.weight = Math.round((e.weight / totalWeight) * 100); 159 | } 160 | }); 161 | } else if (activeEndpoints.length > 0) { 162 | // If no weights set, distribute evenly 163 | const evenWeight = Math.round(100 / activeEndpoints.length); 164 | endpoints.forEach(e => { 165 | if (e.isActive) { 166 | e.weight = evenWeight; 167 | } 168 | }); 169 | } 170 | 171 | this.endpoints = this.sortEndpoints(endpoints.filter(e => e.isActive)); 172 | this.currentIndex = 0; 173 | } 174 | } 175 | 176 | export const createDefaultEndpoints = (): RPCEndpoint[] => { 177 | return [ 178 | { 179 | id: 'default-1', 180 | url: 'https://solana.drpc.org', 181 | name: 'dRPC', 182 | isActive: true, 183 | priority: 1, 184 | weight: 20, 185 | failureCount: 0, 186 | }, 187 | { 188 | id: 'default-2', 189 | url: 'https://solana-rpc.publicnode.com', 190 | name: 'PublicNode', 191 | isActive: true, 192 | priority: 2, 193 | weight: 60, 194 | failureCount: 0, 195 | }, 196 | { 197 | id: 'default-3', 198 | url: 'https://public.rpc.solanavibestation.com', 199 | name: 'Solana Vibe Station', 200 | isActive: true, 201 | priority: 4, 202 | weight: 20, 203 | failureCount: 0, 204 | } 205 | ]; 206 | }; 207 | 208 | /** 209 | * Create a connection using RPC manager from config 210 | * This is a helper for components that need to create connections on-demand 211 | */ 212 | export const createConnectionFromConfig = async ( 213 | rpcEndpoints?: string 214 | ): Promise => { 215 | try { 216 | if (rpcEndpoints) { 217 | const endpoints = JSON.parse(rpcEndpoints) as RPCEndpoint[]; 218 | const manager = new RPCManager(endpoints); 219 | return await manager.createConnection(); 220 | } 221 | } catch (error) { 222 | console.error('Error creating connection from config:', error); 223 | } 224 | 225 | // Fallback to default endpoints 226 | const defaultEndpoints = createDefaultEndpoints(); 227 | const manager = new RPCManager(defaultEndpoints); 228 | return await manager.createConnection(); 229 | }; 230 | -------------------------------------------------------------------------------- /src/contexts/AppContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useCallback, useMemo, type ReactNode } from 'react'; 2 | import type { Connection } from '@solana/web3.js'; 3 | import { 4 | loadWalletsFromCookies, 5 | loadConfigFromCookies, 6 | saveConfigToCookies, 7 | saveWalletsToCookies, 8 | fetchWalletBalances, 9 | type WalletType, 10 | type ConfigType 11 | } from '../Utils'; 12 | import { AppContext } from './AppContextInstance'; 13 | import { RPCManager, createDefaultEndpoints, type RPCEndpoint } from '../utils/rpcManager'; 14 | 15 | export interface AppContextType { 16 | // Wallet state 17 | wallets: WalletType[]; 18 | setWallets: (wallets: WalletType[] | ((prev: WalletType[]) => WalletType[])) => void; 19 | 20 | // Config state 21 | config: ConfigType; 22 | setConfig: (config: ConfigType) => void; 23 | updateConfig: (key: keyof ConfigType, value: string) => void; 24 | 25 | // Connection state 26 | connection: Connection | null; 27 | setConnection: (connection: Connection | null) => void; 28 | rpcManager: RPCManager | null; 29 | 30 | // Balance state 31 | solBalances: Map; 32 | setSolBalances: (balances: Map | ((prev: Map) => Map)) => void; 33 | tokenBalances: Map; 34 | setTokenBalances: (balances: Map) => void; 35 | 36 | // Refresh state 37 | isRefreshing: boolean; 38 | refreshBalances: (tokenAddress?: string) => Promise; 39 | 40 | // Toast 41 | showToast: (message: string, type: 'success' | 'error') => void; 42 | } 43 | 44 | const defaultConfig: ConfigType = { 45 | rpcEndpoints: JSON.stringify(createDefaultEndpoints()), 46 | transactionFee: '0.001', 47 | selectedDex: 'auto', 48 | isDropdownOpen: false, 49 | buyAmount: '', 50 | sellAmount: '', 51 | slippageBps: '9900', 52 | bundleMode: 'batch', 53 | singleDelay: '200', 54 | batchDelay: '1000', 55 | tradingServerEnabled: 'false', 56 | tradingServerUrl: 'https://localhost:4444', 57 | streamApiKey: '', 58 | }; 59 | 60 | interface AppContextProviderProps { 61 | children: ReactNode; 62 | showToast: (message: string, type: 'success' | 'error') => void; 63 | } 64 | 65 | export const AppContextProvider: React.FC = ({ children, showToast }) => { 66 | // State 67 | const [wallets, setWalletsState] = useState([]); 68 | const [config, setConfigState] = useState(defaultConfig); 69 | const [connection, setConnection] = useState(null); 70 | const [solBalances, setSolBalances] = useState>(new Map()); 71 | const [tokenBalances, setTokenBalances] = useState>(new Map()); 72 | const [isRefreshing, setIsRefreshing] = useState(false); 73 | const [rpcManager, setRpcManager] = useState(null); 74 | 75 | // Load initial data from cookies 76 | useEffect(() => { 77 | try { 78 | const savedWallets = loadWalletsFromCookies(); 79 | if (savedWallets && savedWallets.length > 0) { 80 | setWalletsState(savedWallets); 81 | } 82 | 83 | const savedConfig = loadConfigFromCookies(); 84 | if (savedConfig) { 85 | setConfigState(savedConfig); 86 | 87 | // Create RPC manager and connection from saved config 88 | try { 89 | const endpoints = savedConfig.rpcEndpoints 90 | ? JSON.parse(savedConfig.rpcEndpoints) as RPCEndpoint[] 91 | : createDefaultEndpoints(); 92 | 93 | const manager = new RPCManager(endpoints); 94 | setRpcManager(manager); 95 | 96 | // Create initial connection 97 | manager.createConnection().then(conn => { 98 | setConnection(conn); 99 | }).catch(error => { 100 | console.error('Error creating initial connection:', error); 101 | showToast('Failed to connect to RPC endpoints', 'error'); 102 | }); 103 | } catch (error) { 104 | console.error('Error creating RPC manager:', error); 105 | } 106 | } 107 | } catch (error) { 108 | console.error('Error loading initial data:', error); 109 | } 110 | }, [showToast]); 111 | 112 | // Update RPC manager and connection when endpoints change 113 | useEffect(() => { 114 | if (config.rpcEndpoints) { 115 | try { 116 | const endpoints = JSON.parse(config.rpcEndpoints) as RPCEndpoint[]; 117 | const manager = new RPCManager(endpoints); 118 | setRpcManager(manager); 119 | 120 | // Create new connection with updated endpoints 121 | manager.createConnection().then(conn => { 122 | setConnection(conn); 123 | }).catch(error => { 124 | console.error('Error creating connection:', error); 125 | showToast('Failed to connect to RPC endpoints', 'error'); 126 | }); 127 | } catch (error) { 128 | console.error('Error updating RPC manager:', error); 129 | } 130 | } 131 | }, [config.rpcEndpoints, showToast]); 132 | 133 | // Wallet setters with cookie persistence 134 | const setWallets = useCallback((newWallets: WalletType[] | ((prev: WalletType[]) => WalletType[])) => { 135 | setWalletsState(prev => { 136 | const updated = typeof newWallets === 'function' ? newWallets(prev) : newWallets; 137 | saveWalletsToCookies(updated); 138 | return updated; 139 | }); 140 | }, []); 141 | 142 | // Balance setters with functional update support 143 | const setSolBalancesWrapper = useCallback((newBalances: Map | ((prev: Map) => Map)) => { 144 | setSolBalances(prev => { 145 | return typeof newBalances === 'function' ? newBalances(prev) : newBalances; 146 | }); 147 | }, []); 148 | 149 | // Config setters with cookie persistence 150 | const setConfig = useCallback((newConfig: ConfigType) => { 151 | setConfigState(newConfig); 152 | saveConfigToCookies(newConfig); 153 | }, []); 154 | 155 | const updateConfig = useCallback((key: keyof ConfigType, value: string) => { 156 | setConfigState(prev => { 157 | const updated = { ...prev, [key]: value }; 158 | saveConfigToCookies(updated); 159 | return updated; 160 | }); 161 | }, []); 162 | 163 | // Refresh balances 164 | const refreshBalances = useCallback(async (tokenAddress?: string) => { 165 | if (!rpcManager || wallets.length === 0) return; 166 | 167 | setIsRefreshing(true); 168 | try { 169 | // Pass rpcManager directly to fetchWalletBalances so it can rotate endpoints for each wallet 170 | await fetchWalletBalances( 171 | rpcManager, 172 | wallets, 173 | tokenAddress || '', 174 | setSolBalancesWrapper, 175 | setTokenBalances, 176 | solBalances, 177 | tokenBalances 178 | ); 179 | } catch (error) { 180 | console.error('Error refreshing balances:', error); 181 | showToast('Failed to refresh balances', 'error'); 182 | } finally { 183 | setIsRefreshing(false); 184 | } 185 | }, [rpcManager, wallets, solBalances, tokenBalances, showToast, setSolBalancesWrapper]); 186 | 187 | // Memoize context value 188 | const value = useMemo(() => ({ 189 | wallets, 190 | setWallets, 191 | config, 192 | setConfig, 193 | updateConfig, 194 | connection, 195 | setConnection, 196 | rpcManager, 197 | solBalances, 198 | setSolBalances: setSolBalancesWrapper, 199 | tokenBalances, 200 | setTokenBalances, 201 | isRefreshing, 202 | refreshBalances, 203 | showToast 204 | }), [ 205 | wallets, 206 | setWallets, 207 | config, 208 | setConfig, 209 | updateConfig, 210 | connection, 211 | rpcManager, 212 | solBalances, 213 | tokenBalances, 214 | isRefreshing, 215 | refreshBalances, 216 | showToast, 217 | setSolBalancesWrapper 218 | ]); 219 | 220 | return {children}; 221 | }; 222 | 223 | -------------------------------------------------------------------------------- /src/components/modals/CalculatePNLModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import { createPortal } from 'react-dom'; 3 | import { BarChart2, X } from 'lucide-react'; 4 | import { getWallets } from '../../Utils'; 5 | import PnlCard from '../PnlCard'; 6 | import type { IframeData } from '../../utils/types'; 7 | 8 | interface BasePnlModalProps { 9 | isOpen: boolean; 10 | onClose: () => void; 11 | } 12 | 13 | interface PnlModalProps extends BasePnlModalProps { 14 | handleRefresh: () => void; 15 | tokenAddress: string; 16 | iframeData: IframeData | null; 17 | tokenBalances: Map; 18 | } 19 | 20 | export const PnlModal: React.FC = ({ 21 | isOpen, 22 | onClose, 23 | tokenAddress, 24 | iframeData, 25 | tokenBalances 26 | }) => { 27 | const wallets = getWallets(); 28 | 29 | // Calculate PNL using the same formula as DataBox 30 | const pnlData = useMemo(() => { 31 | if (!iframeData || !iframeData.tradingStats) { 32 | return { 33 | totalPnl: 0, 34 | bought: 0, 35 | sold: 0, 36 | holdingsValue: 0, 37 | totalWallets: wallets.length, 38 | trades: 0 39 | }; 40 | } 41 | 42 | const { tradingStats, tokenPrice } = iframeData; 43 | 44 | // Calculate holdings value (same as DataBox) 45 | const totalTokens = Array.from(tokenBalances.values()).reduce((sum, balance) => sum + balance, 0); 46 | const currentTokenPrice = tokenPrice?.tokenPrice || 0; 47 | const holdingsValue = totalTokens * currentTokenPrice; 48 | 49 | // Calculate total PNL (same as DataBox: tradingStats.net + holdingsValue) 50 | const totalPnl = tradingStats.net + holdingsValue; 51 | 52 | return { 53 | totalPnl, 54 | bought: tradingStats.bought, 55 | sold: tradingStats.sold, 56 | holdingsValue, 57 | totalWallets: wallets.length, 58 | trades: tradingStats.trades || 0 59 | }; 60 | }, [iframeData, tokenBalances, wallets.length]); 61 | 62 | // If modal is not open, don't render anything 63 | if (!isOpen) return null; 64 | 65 | // Animation keyframes for elements 66 | const modalStyleElement = document.createElement('style'); 67 | modalStyleElement.textContent = ` 68 | @keyframes modal-pulse { 69 | 0% { box-shadow: 0 0 5px var(--color-primary-50), 0 0 15px var(--color-primary-20); } 70 | 50% { box-shadow: 0 0 15px var(--color-primary-80), 0 0 25px var(--color-primary-40); } 71 | 100% { box-shadow: 0 0 5px var(--color-primary-50), 0 0 15px var(--color-primary-20); } 72 | } 73 | 74 | @keyframes modal-fade-in { 75 | 0% { opacity: 0; } 76 | 100% { opacity: 1; } 77 | } 78 | 79 | @keyframes modal-slide-up { 80 | 0% { transform: translateY(20px); opacity: 0; } 81 | 100% { transform: translateY(0); opacity: 1; } 82 | } 83 | 84 | @keyframes modal-scan-line { 85 | 0% { transform: translateY(-100%); opacity: 0.3; } 86 | 100% { transform: translateY(100%); opacity: 0; } 87 | } 88 | 89 | .modal-content { 90 | position: relative; 91 | } 92 | 93 | .modal-input-:focus { 94 | box-shadow: 0 0 0 1px var(--color-primary-70), 0 0 15px var(--color-primary-50); 95 | transition: all 0.3s ease; 96 | } 97 | 98 | .modal-btn- { 99 | position: relative; 100 | overflow: hidden; 101 | transition: all 0.3s ease; 102 | } 103 | 104 | .modal-btn-::after { 105 | content: ""; 106 | position: absolute; 107 | top: -50%; 108 | left: -50%; 109 | width: 200%; 110 | height: 200%; 111 | background: linear-gradient( 112 | to bottom right, 113 | var(--color-primary-05) 0%, 114 | var(--color-primary-30) 50%, 115 | var(--color-primary-05) 100% 116 | ); 117 | transform: rotate(45deg); 118 | transition: all 0.5s ease; 119 | opacity: 0; 120 | } 121 | 122 | .modal-btn-:hover::after { 123 | opacity: 1; 124 | transform: rotate(45deg) translate(50%, 50%); 125 | } 126 | 127 | .modal-btn-:active { 128 | transform: scale(0.95); 129 | } 130 | 131 | .progress-bar- { 132 | position: relative; 133 | overflow: hidden; 134 | } 135 | 136 | .progress-bar-::after { 137 | content: ""; 138 | position: absolute; 139 | top: 0; 140 | left: 0; 141 | right: 0; 142 | bottom: 0; 143 | background: linear-gradient( 144 | 90deg, 145 | transparent 0%, 146 | var(--color-primary-70) 50%, 147 | transparent 100% 148 | ); 149 | width: 100%; 150 | height: 100%; 151 | transform: translateX(-100%); 152 | animation: progress-shine 3s infinite; 153 | } 154 | 155 | @keyframes progress-shine { 156 | 0% { transform: translateX(-100%); } 157 | 20% { transform: translateX(100%); } 158 | 100% { transform: translateX(100%); } 159 | } 160 | 161 | .glitch-text:hover { 162 | text-shadow: 0 0 2px var(--color-primary), 0 0 4px var(--color-primary); 163 | animation: glitch 2s infinite; 164 | } 165 | 166 | @keyframes glitch { 167 | 2%, 8% { transform: translate(-2px, 0) skew(0.3deg); } 168 | 4%, 6% { transform: translate(2px, 0) skew(-0.3deg); } 169 | 62%, 68% { transform: translate(0, 0) skew(0.33deg); } 170 | 64%, 66% { transform: translate(0, 0) skew(-0.33deg); } 171 | } 172 | 173 | /* Responsive adjustments */ 174 | @media (max-width: 640px) { 175 | .grid-cols-responsive { 176 | grid-template-columns: 1fr; 177 | } 178 | 179 | .flex-responsive { 180 | flex-direction: column; 181 | } 182 | 183 | .w-responsive { 184 | width: 100%; 185 | } 186 | } 187 | `; 188 | document.head.appendChild(modalStyleElement); 189 | 190 | return createPortal( 191 |
192 |
193 | {/* Ambient grid background */} 194 |
195 |
196 | 197 | {/* Header */} 198 |
199 |
200 |
201 | 202 |
203 |

204 | / TOKEN PNL CALCULATOR / 205 |

206 |
207 | 213 |
214 | 215 | {/* Content */} 216 |
217 |
218 | 219 | {/* Token Information */} 220 |
221 |
222 | TOKEN ADDRESS: 223 | 224 | {tokenAddress.slice(0, 6)}...{tokenAddress.slice(-4)} 225 | 226 |
227 |
228 | 229 | {/* PNL Card */} 230 |
231 | 241 |
242 |
243 | 244 |
245 | 246 |
247 |
, 248 | document.body 249 | ); 250 | }; -------------------------------------------------------------------------------- /src/components/tools/automate/ProfileCard.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * ProfileCard - Unified profile display component 3 | * 4 | * Displays profile information for all tool types with app-consistent styling 5 | */ 6 | 7 | import React from 'react'; 8 | import { 9 | Play, 10 | Pause, 11 | Edit2, 12 | Copy, 13 | Trash2, 14 | Zap, 15 | Users, 16 | Bot, 17 | Clock, 18 | Hash, 19 | ChevronRight 20 | } from 'lucide-react'; 21 | import type { 22 | SniperProfile, 23 | CopyTradeProfile, 24 | TradingStrategy, 25 | ToolType 26 | } from './types'; 27 | 28 | interface ProfileCardProps { 29 | type: ToolType; 30 | profile: SniperProfile | CopyTradeProfile | TradingStrategy; 31 | isSelected?: boolean; 32 | onSelect?: () => void; 33 | onToggle: () => void; 34 | onEdit: () => void; 35 | onDuplicate: () => void; 36 | onDelete: () => void; 37 | } 38 | 39 | const formatTime = (ms: number): string => { 40 | const date = new Date(ms); 41 | return date.toLocaleDateString('en-US', { 42 | month: 'short', 43 | day: 'numeric', 44 | hour: '2-digit', 45 | minute: '2-digit' 46 | }); 47 | }; 48 | 49 | const ProfileCard: React.FC = ({ 50 | type, 51 | profile, 52 | isSelected, 53 | onSelect, 54 | onToggle, 55 | onEdit, 56 | onDuplicate, 57 | onDelete, 58 | }) => { 59 | const getTypeIcon = (): React.ReactNode => { 60 | switch (type) { 61 | case 'sniper': return ; 62 | case 'copytrade': return ; 63 | case 'automate': return ; 64 | } 65 | }; 66 | 67 | const getTypeColor = (): string => { 68 | switch (type) { 69 | case 'sniper': return 'color-primary'; 70 | case 'copytrade': return 'color-primary'; 71 | case 'automate': return 'color-primary'; 72 | } 73 | }; 74 | 75 | const getTypeBadge = (): string => { 76 | switch (type) { 77 | case 'sniper': return 'SNIPER'; 78 | case 'copytrade': return 'COPY'; 79 | case 'automate': return 'AUTO'; 80 | } 81 | }; 82 | 83 | const getProfileStats = (): React.ReactNode => { 84 | switch (type) { 85 | case 'sniper': { 86 | const p = profile as SniperProfile; 87 | return ( 88 | <> 89 | {p.eventType.toUpperCase()} 90 | 91 | {p.filters.length} filter{p.filters.length !== 1 ? 's' : ''} 92 | 93 | {p.buyAmount} SOL 94 | 95 | ); 96 | } 97 | case 'copytrade': { 98 | const p = profile as CopyTradeProfile; 99 | return ( 100 | <> 101 | {p.mode.toUpperCase()} 102 | 103 | {p.walletAddresses.length} wallet{p.walletAddresses.length !== 1 ? 's' : ''} 104 | 105 | {p.conditions.length} cond 106 | 107 | ); 108 | } 109 | case 'automate': { 110 | const p = profile as TradingStrategy; 111 | return ( 112 | <> 113 | {p.conditions.length} cond 114 | 115 | {p.actions.length} action{p.actions.length !== 1 ? 's' : ''} 116 | 117 | {p.walletAddresses.length} wallet{p.walletAddresses.length !== 1 ? 's' : ''} 118 | 119 | ); 120 | } 121 | } 122 | }; 123 | 124 | return ( 125 |
137 | {/* Active indicator bar */} 138 | {profile.isActive && ( 139 |
140 | )} 141 | 142 |
146 | {/* Header Row */} 147 |
148 |
149 | {/* Status Toggle */} 150 | 163 | 164 | {/* Profile Info */} 165 |
166 |
167 | 168 | {getTypeIcon()} 169 | 170 |

171 | {profile.name} 172 |

173 | 180 | {profile.isActive ? 'ACTIVE' : getTypeBadge()} 181 | 182 |
183 | {profile.description && ( 184 |

185 | {profile.description} 186 |

187 | )} 188 |
189 |
190 | 191 | {/* Action Buttons */} 192 |
193 | 200 | 207 | 214 |
215 |
216 | 217 | {/* Stats Row */} 218 |
219 | {getProfileStats()} 220 |
221 | 222 | {/* Footer */} 223 |
224 |
225 | 226 | 227 | {profile.executionCount} 228 | 229 | {profile.lastExecuted && ( 230 | 231 | 232 | {formatTime(profile.lastExecuted)} 233 | 234 | )} 235 |
236 | 237 |
238 |
239 |
240 | ); 241 | }; 242 | 243 | export default ProfileCard; 244 | -------------------------------------------------------------------------------- /src/components/Styles.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | // Tooltip Component with styling 4 | export const WalletTooltip: React.FC<{ 5 | children: React.ReactNode; 6 | content: string; 7 | position?: 'top' | 'bottom' | 'left' | 'right'; 8 | }> = ({ 9 | children, 10 | content, 11 | position = 'top' 12 | }) => { 13 | const [isVisible, setIsVisible] = useState(false); 14 | 15 | const positionClasses = { 16 | top: 'bottom-full left-1/2 -translate-x-1/2 mb-2', 17 | bottom: 'top-full left-1/2 -translate-x-1/2 mt-2', 18 | left: 'right-full top-1/2 -translate-y-1/2 mr-2', 19 | right: 'left-full top-1/2 -translate-y-1/2 ml-2' 20 | }; 21 | 22 | return ( 23 |
24 |
setIsVisible(true)} 26 | onMouseLeave={() => setIsVisible(false)} 27 | > 28 | {children} 29 |
30 | {isVisible && ( 31 |
32 |
33 | {content} 34 |
35 |
36 | )} 37 |
38 | ); 39 | }; 40 | 41 | // Define the application styles that will be injected 42 | // eslint-disable-next-line react-refresh/only-export-components 43 | export const initStyles = (): string => { 44 | return ` 45 | /* Background grid animation */ 46 | @keyframes grid-pulse { 47 | 0% { opacity: 0.1; } 48 | 50% { opacity: 0.15; } 49 | 100% { opacity: 0.1; } 50 | } 51 | 52 | .bg { 53 | background-color: var(--color-bg-primary); 54 | background-image: 55 | linear-gradient(var(--color-primary-05) 1px, transparent 1px), 56 | linear-gradient(90deg, var(--color-primary-05) 1px, transparent 1px); 57 | background-size: var(--grid-size) var(--grid-size); 58 | background-position: center center; 59 | position: relative; 60 | overflow: hidden; 61 | } 62 | 63 | .bg::before { 64 | content: ""; 65 | position: absolute; 66 | top: 0; 67 | left: 0; 68 | right: 0; 69 | bottom: 0; 70 | background-image: 71 | linear-gradient(var(--color-primary-05) 1px, transparent 1px), 72 | linear-gradient(90deg, var(--color-primary-05) 1px, transparent 1px); 73 | background-size: var(--grid-size) var(--grid-size); 74 | background-position: center center; 75 | animation: grid-pulse var(--grid-pulse-speed) infinite; 76 | z-index: 0; 77 | } 78 | 79 | /* Button hover animations */ 80 | @keyframes btn-glow { 81 | 0% { box-shadow: 0 0 5px var(--color-primary); } 82 | 50% { box-shadow: 0 0 15px var(--color-primary); } 83 | 100% { box-shadow: 0 0 5px var(--color-primary); } 84 | } 85 | 86 | .btn { 87 | transition: all var(--transition-speed) ease; 88 | position: relative; 89 | overflow: hidden; 90 | } 91 | 92 | .btn:hover { 93 | animation: btn-glow var(--glow-speed) infinite; 94 | } 95 | 96 | .btn::after { 97 | content: ""; 98 | position: absolute; 99 | top: -50%; 100 | left: -50%; 101 | width: 200%; 102 | height: 200%; 103 | background: linear-gradient( 104 | to bottom right, 105 | transparent 0%, 106 | var(--color-primary-30) 50%, 107 | transparent 100% 108 | ); 109 | transform: rotate(45deg); 110 | transition: all var(--hover-speed) ease; 111 | opacity: 0; 112 | } 113 | 114 | .btn:hover::after { 115 | opacity: 1; 116 | transform: rotate(45deg) translate(50%, 50%); 117 | } 118 | 119 | /* Glitch effect for text */ 120 | @keyframes glitch { 121 | 2%, 8% { transform: translate(-2px, 0) skew(0.3deg); } 122 | 4%, 6% { transform: translate(2px, 0) skew(-0.3deg); } 123 | 62%, 68% { transform: translate(0, 0) skew(0.33deg); } 124 | 64%, 66% { transform: translate(0, 0) skew(-0.33deg); } 125 | } 126 | 127 | .glitch { 128 | position: relative; 129 | } 130 | 131 | .glitch:hover { 132 | animation: glitch 2s infinite; 133 | } 134 | 135 | /* Input focus effect */ 136 | .input:focus { 137 | box-shadow: 0 0 0 1px var(--color-primary-70), 0 0 15px var(--color-primary-50); 138 | transition: all var(--transition-speed) ease; 139 | } 140 | 141 | /* Card hover effect */ 142 | .card { 143 | transition: all var(--transition-speed) ease; 144 | } 145 | 146 | .card:hover { 147 | transform: translateY(-3px); 148 | box-shadow: 0 7px 20px rgba(0, 0, 0, 0.3), 0 0 15px var(--color-primary-30); 149 | } 150 | 151 | /* Scan line effect */ 152 | @keyframes scanline { 153 | 0% { 154 | transform: translateY(-100%); 155 | opacity: 0.7; 156 | } 157 | 100% { 158 | transform: translateY(100%); 159 | opacity: 0; 160 | } 161 | } 162 | 163 | .scanline { 164 | position: relative; 165 | overflow: hidden; 166 | } 167 | 168 | .scanline::before { 169 | content: ""; 170 | position: absolute; 171 | width: 100%; 172 | height: 10px; 173 | background: linear-gradient(to bottom, 174 | transparent 0%, 175 | var(--color-primary-20) 50%, 176 | transparent 100%); 177 | z-index: 10; 178 | animation: scanline var(--scanline-speed) linear infinite; 179 | } 180 | 181 | /* Split gutter styling */ 182 | .split-custom .gutter { 183 | background: linear-gradient(90deg, 184 | transparent 0%, 185 | var(--color-primary-10) 50%, 186 | transparent 100% 187 | ); 188 | position: relative; 189 | transition: all 0.3s ease; 190 | display: flex; 191 | flex-direction: column; 192 | align-items: center; 193 | justify-content: center; 194 | gap: 8px; 195 | } 196 | 197 | .split-custom .gutter-horizontal { 198 | cursor: col-resize; 199 | } 200 | 201 | .split-custom .gutter-horizontal:hover { 202 | background: linear-gradient(90deg, 203 | transparent 0%, 204 | var(--color-primary-20) 50%, 205 | transparent 100% 206 | ); 207 | } 208 | 209 | .split-custom .gutter-horizontal:active { 210 | background: linear-gradient(90deg, 211 | transparent 0%, 212 | var(--color-primary-30) 50%, 213 | transparent 100% 214 | ); 215 | } 216 | 217 | /* Animated dots pattern */ 218 | .gutter-dot { 219 | width: 3px; 220 | height: 3px; 221 | background-color: var(--color-primary-60); 222 | border-radius: 50%; 223 | opacity: 0.4; 224 | transition: all 0.3s ease; 225 | } 226 | 227 | .split-custom .gutter-horizontal:hover .gutter-dot { 228 | background-color: var(--color-primary-70); 229 | opacity: 0.7; 230 | } 231 | 232 | .split-custom .gutter-horizontal:active .gutter-dot { 233 | background-color: var(--color-primary); 234 | opacity: 1; 235 | box-shadow: 0 0 12px var(--color-primary); 236 | animation: gutterPulseActive 0.4s ease-in-out infinite; 237 | } 238 | 239 | @keyframes gutterPulseActive { 240 | 0%, 100% { 241 | transform: scale(1); 242 | opacity: 1; 243 | } 244 | 50% { 245 | transform: scale(1.8); 246 | opacity: 0.8; 247 | } 248 | } 249 | 250 | /* Non-resizable divider styling (visual only) */ 251 | .gutter-divider { 252 | width: 12px; 253 | background: linear-gradient(90deg, 254 | transparent 0%, 255 | var(--color-primary-10) 50%, 256 | transparent 100% 257 | ); 258 | position: relative; 259 | flex-shrink: 0; 260 | } 261 | 262 | .gutter-divider-non-resizable { 263 | cursor: default; 264 | } 265 | 266 | /* Neo-futuristic table styling */ 267 | .table { 268 | border-collapse: separate; 269 | border-spacing: 0; 270 | } 271 | 272 | .table thead th { 273 | background-color: var(--color-primary-10); 274 | border-bottom: 2px solid var(--color-primary-50); 275 | } 276 | 277 | .table tbody tr { 278 | transition: all var(--hover-speed) ease; 279 | } 280 | 281 | .table tbody tr:hover { 282 | background-color: var(--color-primary-05); 283 | } 284 | 285 | /* Neon text effect */ 286 | .neon-text { 287 | color: var(--color-primary); 288 | text-shadow: 0 0 5px var(--color-primary-70); 289 | } 290 | 291 | /* Notification animation */ 292 | @keyframes notification-slide { 293 | 0% { transform: translateX(50px); opacity: 0; } 294 | 10% { transform: translateX(0); opacity: 1; } 295 | 90% { transform: translateX(0); opacity: 1; } 296 | 100% { transform: translateX(50px); opacity: 0; } 297 | } 298 | 299 | .notification-anim { 300 | animation: notification-slide 4s forwards; 301 | } 302 | 303 | /* Loading animation */ 304 | @keyframes loading-pulse { 305 | 0% { transform: scale(0.85); opacity: 0.7; } 306 | 50% { transform: scale(1); opacity: 1; } 307 | 100% { transform: scale(0.85); opacity: 0.7; } 308 | } 309 | 310 | .loading-anim { 311 | animation: loading-pulse 1.5s infinite; 312 | } 313 | 314 | /* Button click effect */ 315 | .btn:active { 316 | transform: scale(0.95); 317 | box-shadow: 0 0 15px var(--color-primary-70); 318 | } 319 | 320 | /* Menu active state */ 321 | .menu-item-active { 322 | border-left: 3px solid var(--color-primary); 323 | background-color: var(--color-primary-10); 324 | } 325 | 326 | /* Angle brackets for headings */ 327 | .heading-brackets { 328 | position: relative; 329 | display: inline-block; 330 | } 331 | 332 | .heading-brackets::before, 333 | .heading-brackets::after { 334 | position: absolute; 335 | top: 50%; 336 | transform: translateY(-50%); 337 | color: var(--color-primary); 338 | font-weight: bold; 339 | } 340 | 341 | .heading-brackets::before { 342 | content: ">"; 343 | left: -15px; 344 | } 345 | 346 | .heading-brackets::after { 347 | content: "<"; 348 | right: -15px; 349 | } 350 | 351 | /* Fade-in animation */ 352 | @keyframes fadeIn { 353 | 0% { opacity: 0; } 354 | 100% { opacity: 1; } 355 | } 356 | `; 357 | }; -------------------------------------------------------------------------------- /src/utils/consolidate.ts: -------------------------------------------------------------------------------- 1 | import { Keypair, VersionedTransaction } from '@solana/web3.js'; 2 | import bs58 from 'bs58'; 3 | 4 | // Constants 5 | const MAX_BUNDLES_PER_SECOND = 2; 6 | 7 | // Rate limiting state 8 | const rateLimitState = { 9 | count: 0, 10 | lastReset: Date.now(), 11 | maxBundlesPerSecond: MAX_BUNDLES_PER_SECOND 12 | }; 13 | 14 | interface WalletConsolidation { 15 | address: string; 16 | privateKey: string; 17 | } 18 | 19 | interface ConsolidationBundle { 20 | transactions: string[]; // Base58 encoded transaction data 21 | } 22 | 23 | // Define interface for bundle result 24 | interface BundleResult { 25 | jsonrpc: string; 26 | id: number; 27 | result?: string; 28 | error?: { 29 | code: number; 30 | message: string; 31 | }; 32 | } 33 | 34 | /** 35 | * Check rate limit and wait if necessary 36 | */ 37 | const checkRateLimit = async (): Promise => { 38 | const now = Date.now(); 39 | 40 | if (now - rateLimitState.lastReset >= 1000) { 41 | rateLimitState.count = 0; 42 | rateLimitState.lastReset = now; 43 | } 44 | 45 | if (rateLimitState.count >= rateLimitState.maxBundlesPerSecond) { 46 | const waitTime = 1000 - (now - rateLimitState.lastReset); 47 | await new Promise(resolve => setTimeout(resolve, waitTime)); 48 | rateLimitState.count = 0; 49 | rateLimitState.lastReset = Date.now(); 50 | } 51 | 52 | rateLimitState.count++; 53 | }; 54 | 55 | interface WindowWithConfig { 56 | tradingServerUrl?: string; 57 | } 58 | 59 | /** 60 | * Send bundle to Jito block engine through our backend proxy 61 | */ 62 | const sendBundle = async (encodedBundle: string[]): Promise => { 63 | try { 64 | const baseUrl = (window as WindowWithConfig).tradingServerUrl?.replace(/\/+$/, '') || ''; 65 | 66 | // Send to our backend proxy instead of directly to Jito 67 | const response = await fetch(`${baseUrl}/v2/sol/send`, { 68 | method: 'POST', 69 | headers: { 'Content-Type': 'application/json' }, 70 | body: JSON.stringify({ 71 | transactions: encodedBundle 72 | }), 73 | }); 74 | 75 | const data = await response.json() as { result: BundleResult }; 76 | 77 | return data.result; 78 | } catch (error) { 79 | console.error('Error sending bundle:', error); 80 | throw error; 81 | } 82 | }; 83 | 84 | /** 85 | * Get partially prepared consolidation transactions from backend 86 | * The backend will create transactions without signing them 87 | */ 88 | const getPartiallyPreparedTransactions = async ( 89 | sourceAddresses: string[], 90 | receiverAddress: string, 91 | percentage: number 92 | ): Promise => { 93 | try { 94 | const baseUrl = (window as WindowWithConfig).tradingServerUrl?.replace(/\/+$/, '') || ''; 95 | 96 | const response = await fetch(`${baseUrl}/v2/sol/consolidate`, { 97 | method: 'POST', 98 | headers: { 'Content-Type': 'application/json' }, 99 | body: JSON.stringify({ 100 | sourceAddresses, 101 | receiverAddress, 102 | percentage 103 | }), 104 | }); 105 | 106 | if (!response.ok) { 107 | throw new Error(`HTTP error! Status: ${response.status}`); 108 | } 109 | 110 | const data = await response.json() as { success: boolean; error?: string; transactions?: string[]; data?: { transactions?: string[] } }; 111 | 112 | if (!data.success) { 113 | throw new Error(data.error || 'Failed to get partially prepared transactions'); 114 | } 115 | 116 | // Handle different response formats 117 | const transactions = (data as unknown as { data?: { transactions?: string[] } }).data?.transactions || data.transactions; 118 | 119 | if (!transactions || !Array.isArray(transactions)) { 120 | throw new Error('No transactions returned from backend'); 121 | } 122 | 123 | return transactions; // Array of base58 encoded partially prepared transactions 124 | } catch (error) { 125 | console.error('Error getting partially prepared transactions:', error); 126 | throw error; 127 | } 128 | }; 129 | 130 | /** 131 | * Complete transaction signing with source wallets and recipient wallet 132 | */ 133 | const completeTransactionSigning = ( 134 | partiallyPreparedTransactionsBase58: string[], 135 | sourceKeypairs: Map, 136 | receiverKeypair: Keypair 137 | ): string[] => { 138 | try { 139 | return partiallyPreparedTransactionsBase58.map(txBase58 => { 140 | // Deserialize transaction 141 | const txBuffer = bs58.decode(txBase58); 142 | const transaction = VersionedTransaction.deserialize(txBuffer); 143 | 144 | // Extract transaction message to determine required signers 145 | const message = transaction.message; 146 | const signers: Keypair[] = []; 147 | 148 | // Always add receiver keypair as it's the fee payer 149 | signers.push(receiverKeypair); 150 | 151 | // Add source keypairs based on accounts in transaction 152 | for (const accountKey of message.staticAccountKeys) { 153 | const pubkeyStr = accountKey.toBase58(); 154 | if (sourceKeypairs.has(pubkeyStr)) { 155 | signers.push(sourceKeypairs.get(pubkeyStr)!); 156 | } 157 | } 158 | 159 | // Sign the transaction 160 | transaction.sign(signers); 161 | 162 | // Serialize and encode the fully signed transaction 163 | return bs58.encode(transaction.serialize()); 164 | }); 165 | } catch (error) { 166 | console.error('Error completing transaction signing:', error); 167 | throw error; 168 | } 169 | }; 170 | 171 | /** 172 | * Prepare consolidation bundles 173 | */ 174 | const prepareConsolidationBundles = (signedTransactions: string[]): ConsolidationBundle[] => { 175 | // For simplicity, we're putting transactions in bundles of MAX_TXS_PER_BUNDLE 176 | const MAX_TXS_PER_BUNDLE = 5; 177 | const bundles: ConsolidationBundle[] = []; 178 | 179 | for (let i = 0; i < signedTransactions.length; i += MAX_TXS_PER_BUNDLE) { 180 | const bundleTransactions = signedTransactions.slice(i, i + MAX_TXS_PER_BUNDLE); 181 | bundles.push({ 182 | transactions: bundleTransactions 183 | }); 184 | } 185 | 186 | return bundles; 187 | }; 188 | 189 | /** 190 | * Execute SOL consolidation 191 | */ 192 | export const consolidateSOL = async ( 193 | sourceWallets: WalletConsolidation[], 194 | receiverWallet: WalletConsolidation, 195 | percentage: number 196 | ): Promise<{ success: boolean; result?: unknown; error?: string }> => { 197 | try { 198 | // Extract source addresses 199 | const sourceAddresses = sourceWallets.map(wallet => wallet.address); 200 | 201 | // Step 1: Get partially prepared transactions from backend 202 | const partiallyPreparedTransactions = await getPartiallyPreparedTransactions( 203 | sourceAddresses, 204 | receiverWallet.address, 205 | percentage 206 | ); 207 | 208 | // Step 2: Create keypairs from private keys 209 | const receiverKeypair = Keypair.fromSecretKey(bs58.decode(receiverWallet.privateKey)); 210 | 211 | // Create a map of source public keys to keypairs for faster lookups 212 | const sourceKeypairsMap = new Map(); 213 | sourceWallets.forEach(wallet => { 214 | const keypair = Keypair.fromSecretKey(bs58.decode(wallet.privateKey)); 215 | sourceKeypairsMap.set(keypair.publicKey.toBase58(), keypair); 216 | }); 217 | 218 | // Step 3: Complete transaction signing with source and receiver keys 219 | const fullySignedTransactions = completeTransactionSigning( 220 | partiallyPreparedTransactions, 221 | sourceKeypairsMap, 222 | receiverKeypair 223 | ); 224 | 225 | // Step 4: Prepare consolidation bundles 226 | const consolidationBundles = prepareConsolidationBundles(fullySignedTransactions); 227 | 228 | // Step 5: Send bundles 229 | const results: BundleResult[] = []; 230 | for (let i = 0; i < consolidationBundles.length; i++) { 231 | const bundle = consolidationBundles[i]; 232 | 233 | await checkRateLimit(); 234 | const result = await sendBundle(bundle.transactions); 235 | results.push(result); 236 | 237 | // Add delay between bundles (except after the last one) 238 | if (i < consolidationBundles.length - 1) { 239 | await new Promise(resolve => setTimeout(resolve, 500)); // 500ms delay 240 | } 241 | } 242 | 243 | return { 244 | success: true, 245 | result: results 246 | }; 247 | } catch (error) { 248 | console.error('SOL consolidation error:', error); 249 | return { 250 | success: false, 251 | error: error instanceof Error ? error.message : String(error) 252 | }; 253 | } 254 | }; 255 | 256 | /** 257 | * Validate consolidation inputs 258 | */ 259 | export const validateConsolidationInputs = ( 260 | sourceWallets: WalletConsolidation[], 261 | receiverWallet: WalletConsolidation, 262 | percentage: number, 263 | sourceBalances: Map 264 | ): { valid: boolean; error?: string } => { 265 | // Check if receiver wallet is valid 266 | if (!receiverWallet.address || !receiverWallet.privateKey) { 267 | return { valid: false, error: 'Invalid receiver wallet' }; 268 | } 269 | 270 | // Check if source wallets are valid 271 | if (!sourceWallets.length) { 272 | return { valid: false, error: 'No source wallets' }; 273 | } 274 | 275 | for (const wallet of sourceWallets) { 276 | if (!wallet.address || !wallet.privateKey) { 277 | return { valid: false, error: 'Invalid source wallet data' }; 278 | } 279 | 280 | const balance = sourceBalances.get(wallet.address) || 0; 281 | if (balance <= 0) { 282 | return { valid: false, error: `Source wallet ${wallet.address.substring(0, 6)}... has no balance` }; 283 | } 284 | } 285 | 286 | // Check if percentage is valid 287 | if (isNaN(percentage) || percentage <= 0 || percentage > 100) { 288 | return { valid: false, error: 'Percentage must be between 1 and 100' }; 289 | } 290 | 291 | return { valid: true }; 292 | }; -------------------------------------------------------------------------------- /src/components/tools/automate/SniperFilterBuilder.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * SniperFilterBuilder - Filter configuration for Sniper Bot 3 | */ 4 | 5 | import React from 'react'; 6 | import { Trash2, ToggleLeft, ToggleRight } from 'lucide-react'; 7 | import type { SniperFilter, SniperEventType, FilterMatchType } from './types'; 8 | 9 | const PLATFORMS = [ 10 | { value: '', label: 'Any Platform' }, 11 | { value: 'Pumpfun', label: 'Pumpfun' }, 12 | { value: 'Bonk', label: 'Bonk' }, 13 | { value: 'Raydium', label: 'Raydium' }, 14 | { value: 'Moonshot', label: 'Moonshot' }, 15 | ]; 16 | 17 | const MATCH_TYPES = [ 18 | { value: 'contains', label: 'Contains' }, 19 | { value: 'exact', label: 'Exact' }, 20 | { value: 'regex', label: 'Regex' }, 21 | ]; 22 | 23 | interface SniperFilterBuilderProps { 24 | filter: SniperFilter; 25 | index: number; 26 | eventType: SniperEventType; 27 | onUpdate: (updates: Partial) => void; 28 | onRemove: () => void; 29 | onToggle: () => void; 30 | } 31 | 32 | const formatAddress = (address: string): string => { 33 | if (address.length <= 12) return address; 34 | return `${address.slice(0, 6)}...${address.slice(-4)}`; 35 | }; 36 | 37 | const SniperFilterBuilder: React.FC = ({ 38 | filter, 39 | index, 40 | eventType, 41 | onUpdate, 42 | onRemove, 43 | onToggle, 44 | }) => { 45 | const showCreatorFilter = eventType === 'deploy' || eventType === 'both'; 46 | 47 | return ( 48 |
55 | {/* Header */} 56 |
57 |
58 | 62 | {index + 1} 63 | 64 | 65 | Filter 66 | 67 | 80 |
81 | 88 |
89 | 90 | {/* Filter Grid */} 91 |
92 | {/* Platform Filter */} 93 |
94 | 97 | 107 |
108 | 109 | {/* Mint Address Filter */} 110 |
111 | 114 | onUpdate({ mint: e.target.value || undefined })} 118 | placeholder="Specific mint address..." 119 | className="w-full px-3 py-2 bg-app-accent border border-app-primary-40 rounded font-mono text-sm text-app-primary 120 | focus:outline-none focus:border-app-primary-color transition-colors placeholder:text-app-secondary-60" 121 | /> 122 | {filter.mint && ( 123 |

{formatAddress(filter.mint)}

124 | )} 125 |
126 | 127 | {/* Creator/Signer Filter */} 128 | {showCreatorFilter && ( 129 |
130 | 133 | onUpdate({ signer: e.target.value || undefined })} 137 | placeholder="Creator wallet address..." 138 | className="w-full px-3 py-2 bg-app-accent border border-app-primary-40 rounded font-mono text-sm text-app-primary 139 | focus:outline-none focus:border-warning/50 transition-colors placeholder:text-app-secondary-60" 140 | /> 141 | {filter.signer && ( 142 |

{formatAddress(filter.signer)}

143 | )} 144 |
145 | )} 146 | 147 | {/* Name Pattern Filter */} 148 |
149 | 152 |
153 | onUpdate({ namePattern: e.target.value || undefined })} 157 | placeholder="Name pattern..." 158 | className="flex-1 px-3 py-2 bg-app-accent border border-app-primary-40 rounded font-mono text-sm text-app-primary 159 | focus:outline-none focus:border-warning/50 transition-colors placeholder:text-app-secondary-60" 160 | /> 161 | 171 |
172 |
173 | 174 | {/* Symbol Pattern Filter */} 175 |
176 | 179 |
180 | onUpdate({ symbolPattern: e.target.value || undefined })} 184 | placeholder="Symbol pattern..." 185 | className="flex-1 px-3 py-2 bg-app-accent border border-app-primary-40 rounded font-mono text-sm text-app-primary 186 | focus:outline-none focus:border-warning/50 transition-colors placeholder:text-app-secondary-60" 187 | /> 188 | 198 |
199 |
200 |
201 | 202 | {/* Filter Summary */} 203 | {filter.enabled && ( 204 |
205 |
206 | {!filter.platform && !filter.mint && !filter.signer && !filter.namePattern && !filter.symbolPattern ? ( 207 | ⚠ No filters set - will match all {eventType} events 208 | ) : ( 209 | 210 | ✓ Filter active 211 | {filter.platform && ` • Platform: ${filter.platform}`} 212 | {filter.mint && ` • Mint: ${formatAddress(filter.mint)}`} 213 | {filter.signer && ` • Creator: ${formatAddress(filter.signer)}`} 214 | {filter.namePattern && ` • Name: "${filter.namePattern}"`} 215 | {filter.symbolPattern && ` • Symbol: "${filter.symbolPattern}"`} 216 | 217 | )} 218 |
219 |
220 | )} 221 |
222 | ); 223 | }; 224 | 225 | export default SniperFilterBuilder; 226 | -------------------------------------------------------------------------------- /src/components/HorizontalHeader.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from 'react'; 2 | import { useNavigate, useLocation } from 'react-router-dom'; 3 | import { Settings, Bot, Blocks, Wallet, TrendingUp, BookOpen, Menu, X } from 'lucide-react'; 4 | import { brand } from '../utils/brandConfig'; 5 | import logo from '../logo.png'; 6 | 7 | interface HeaderProps { 8 | tokenAddress?: string; 9 | onNavigateHome?: () => void; 10 | } 11 | 12 | export const HorizontalHeader: React.FC = ({ 13 | onNavigateHome, 14 | }) => { 15 | const navigate = useNavigate(); 16 | const location = useLocation(); 17 | const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); 18 | 19 | const handleLogoClick = useCallback(() => { 20 | onNavigateHome?.(); 21 | navigate('/'); 22 | }, [onNavigateHome, navigate]); 23 | 24 | const isActive = (path: string): boolean => { 25 | return location.pathname === path; 26 | }; 27 | 28 | const getLinkClassName = (path: string, isProminent: boolean = false): string => { 29 | const active = isActive(path); 30 | 31 | if (isProminent) { 32 | return `flex items-center gap-2 px-5 py-2.5 rounded-lg transition-all duration-300 font-mono text-sm font-bold tracking-wider uppercase 33 | ${active 34 | ? 'bg-gradient-to-r from-app-primary-color to-app-primary-dark text-app-primary border border-app-primary shadow-[0_0_20px_rgba(2,179,109,0.5)] scale-105' 35 | : 'bg-gradient-to-r from-primary-20 to-primary-30 text-app-primary border border-app-primary-60 shadow-[0_0_15px_rgba(2,179,109,0.3)] hover:shadow-[0_0_25px_rgba(2,179,109,0.5)] hover:scale-105 hover:border-app-primary' 36 | }`; 37 | } 38 | 39 | return `flex items-center gap-2 px-4 py-2 rounded-lg transition-all duration-200 font-mono text-sm font-medium tracking-wider uppercase 40 | ${active 41 | ? 'bg-primary-20 text-app-primary border border-app-primary-40 shadow-[0_0_10px_rgba(2,179,109,0.2)]' 42 | : 'text-app-secondary-60 hover:text-app-primary hover:bg-app-primary-80-alpha hover:border-app-primary-30 border border-transparent' 43 | }`; 44 | }; 45 | 46 | const leftNavItems = [ 47 | { path: '/wallets', label: 'Wallets', icon: Wallet }, 48 | { path: '/automate', label: 'Automate', icon: Bot }, 49 | ]; 50 | 51 | const rightNavItems = [ 52 | { path: '/deploy', label: 'Deploy', icon: Blocks }, 53 | ]; 54 | 55 | const allNavItems = [ 56 | { path: '/wallets', label: 'Wallets', icon: Wallet, prominent: false }, 57 | { path: '/monitor', label: 'Trade', icon: TrendingUp, prominent: true }, 58 | { path: '/automate', label: 'Automate', icon: Bot, prominent: false }, 59 | { path: '/deploy', label: 'Deploy', icon: Blocks, prominent: false }, 60 | ]; 61 | 62 | return ( 63 |
64 | {/* Left: Logo */} 65 |
66 | 78 |
79 | 80 | {/* Center: All Nav Items - absolutely centered */} 81 | 123 | 124 | {/* Right: Settings */} 125 |
126 | 133 |
134 | 135 | {/* Mobile: Logo */} 136 |
137 | 149 |
150 | 151 | {/* Mobile Menu Toggle */} 152 | 158 | 159 | {/* Mobile Menu Overlay */} 160 | {isMobileMenuOpen && ( 161 |
162 | {allNavItems.map((item) => ( 163 | 174 | ))} 175 |
176 | 186 | 196 |
197 | )} 198 |
199 | ); 200 | }; 201 | -------------------------------------------------------------------------------- /src/components/Config.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { createPortal } from 'react-dom'; 3 | import { X, Settings, CreditCard, BookOpen } from 'lucide-react'; 4 | import type { ConfigType } from '../Utils'; 5 | import { RPCEndpointManager } from './RPCEndpointManager'; 6 | import { createDefaultEndpoints, type RPCEndpoint } from '../utils/rpcManager'; 7 | 8 | interface ConfigProps { 9 | isOpen: boolean; 10 | onClose: () => void; 11 | config: ConfigType; 12 | onConfigChange: (key: keyof ConfigType, value: string) => void; 13 | onSave: () => void; 14 | onShowTutorial?: () => void; 15 | } 16 | 17 | const Config: React.FC = ({ 18 | isOpen, 19 | onClose, 20 | config, 21 | onConfigChange, 22 | onSave, 23 | onShowTutorial 24 | }) => { 25 | // Add styles when the modal is opened 26 | useEffect(() => { 27 | if (isOpen) { 28 | const styleElement = document.createElement('style'); 29 | styleElement.textContent = ` 30 | @keyframes config-pulse { 31 | 0% { box-shadow: 0 0 5px rgba(2, 179, 109, 0.5), 0 0 15px rgba(2, 179, 109, 0.2); } 32 | 50% { box-shadow: 0 0 15px rgba(2, 179, 109, 0.8), 0 0 25px rgba(2, 179, 109, 0.4); } 33 | 100% { box-shadow: 0 0 5px rgba(2, 179, 109, 0.5), 0 0 15px rgba(2, 179, 109, 0.2); } 34 | } 35 | 36 | @keyframes config-fade-in { 37 | 0% { opacity: 0; } 38 | 100% { opacity: 1; } 39 | } 40 | 41 | @keyframes config-slide-up { 42 | 0% { transform: translateY(20px); opacity: 0; } 43 | 100% { transform: translateY(0); opacity: 1; } 44 | } 45 | 46 | @keyframes config-scan-line { 47 | 0% { transform: translateY(-100%); opacity: 0.3; } 48 | 100% { transform: translateY(100%); opacity: 0; } 49 | } 50 | 51 | .config-container { 52 | animation: config-fade-in 0.3s ease; 53 | } 54 | 55 | .config-content { 56 | animation: config-slide-up 0.4s ease; 57 | position: relative; 58 | } 59 | 60 | .config-content::before { 61 | content: ""; 62 | position: absolute; 63 | width: 100%; 64 | height: 5px; 65 | background: linear-gradient(to bottom, 66 | transparent 0%, 67 | rgba(2, 179, 109, 0.2) 50%, 68 | transparent 100%); 69 | z-index: 10; 70 | animation: config-scan-line 8s linear infinite; 71 | pointer-events: none; 72 | } 73 | 74 | .config-glow { 75 | animation: config-pulse 4s infinite; 76 | } 77 | 78 | .config-input-:focus { 79 | box-shadow: 0 0 0 1px rgba(2, 179, 109, 0.7), 0 0 15px rgba(2, 179, 109, 0.5); 80 | transition: all 0.3s ease; 81 | } 82 | 83 | .config-btn- { 84 | position: relative; 85 | overflow: hidden; 86 | transition: all 0.3s ease; 87 | } 88 | 89 | .config-btn-::after { 90 | content: ""; 91 | position: absolute; 92 | top: -50%; 93 | left: -50%; 94 | width: 200%; 95 | height: 200%; 96 | background: linear-gradient( 97 | to bottom right, 98 | rgba(2, 179, 109, 0) 0%, 99 | rgba(2, 179, 109, 0.3) 50%, 100 | rgba(2, 179, 109, 0) 100% 101 | ); 102 | transform: rotate(45deg); 103 | transition: all 0.5s ease; 104 | opacity: 0; 105 | } 106 | 107 | .config-btn-:hover::after { 108 | opacity: 1; 109 | transform: rotate(45deg) translate(50%, 50%); 110 | } 111 | 112 | .config-btn-:active { 113 | transform: scale(0.95); 114 | } 115 | 116 | .glitch-text:hover { 117 | text-shadow: 0 0 2px #02b36d, 0 0 4px #02b36d; 118 | animation: glitch 2s infinite; 119 | } 120 | 121 | @keyframes glitch { 122 | 2%, 8% { transform: translate(-2px, 0) skew(0.3deg); } 123 | 4%, 6% { transform: translate(2px, 0) skew(-0.3deg); } 124 | 62%, 68% { transform: translate(0, 0) skew(0.33deg); } 125 | 64%, 66% { transform: translate(0, 0) skew(-0.33deg); } 126 | } 127 | `; 128 | document.head.appendChild(styleElement); 129 | 130 | // Add a class to the body to prevent scrolling when modal is open 131 | document.body.style.overflow = 'hidden'; 132 | 133 | return () => { 134 | // Safely remove style element if it's still a child 135 | if (styleElement.parentNode === document.head) { 136 | document.head.removeChild(styleElement); 137 | } 138 | // Restore scrolling when modal is closed 139 | document.body.style.overflow = ''; 140 | }; 141 | } 142 | return undefined; 143 | }, [isOpen]); 144 | 145 | if (!isOpen) return null; 146 | 147 | return createPortal( 148 |
155 |
159 | {/* Ambient grid background */} 160 |
168 |
169 | 170 | {/* Header */} 171 |
172 |
173 |
174 | 175 |
176 |

177 | / SYSTEM CONFIG / 178 |

179 |
180 | 186 |
187 | 188 | {/* Content */} 189 |
190 |
191 | { 198 | onConfigChange('rpcEndpoints', JSON.stringify(endpoints)); 199 | }} 200 | /> 201 |
202 | 203 |
204 |
205 | 208 | 209 |
210 |
211 | onConfigChange('transactionFee', e.target.value)} 215 | className="w-full px-4 py-2.5 bg-[#091217] border border-[#02b36d30] rounded-lg text-[#e4fbf2] shadow-inner focus:border-[#02b36d] focus:ring-1 focus:ring-[#02b36d50] focus:outline-none transition-all duration-200 config-input- font-mono tracking-wider" 216 | step="0.000001" 217 | min="0" 218 | placeholder="ENTER TRANSACTION FEE" 219 | /> 220 |
221 |
222 |
223 | 224 | {onShowTutorial && ( 225 |
226 | 236 |
237 | )} 238 | 239 |
240 | 246 |
247 |
248 | 249 | 250 |
251 |
, 252 | document.body 253 | ); 254 | }; 255 | 256 | export default Config; -------------------------------------------------------------------------------- /src/components/tools/automate/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Unified Trading Tools Type Definitions 3 | * 4 | * Consolidated types for Sniper Bot, Copy Trade, and Automate features 5 | */ 6 | 7 | // ============================================================================ 8 | // Tool Types 9 | // ============================================================================ 10 | 11 | export type ToolType = 'sniper' | 'copytrade' | 'automate'; 12 | 13 | // ============================================================================ 14 | // Common Types 15 | // ============================================================================ 16 | 17 | export type PriorityLevel = 'low' | 'medium' | 'high' | 'turbo'; 18 | export type CooldownUnit = 'milliseconds' | 'seconds' | 'minutes'; 19 | export type OperatorType = 'greater' | 'less' | 'equal' | 'greaterEqual' | 'lessEqual'; 20 | export type FilterMatchType = 'exact' | 'contains' | 'regex'; 21 | 22 | // ============================================================================ 23 | // Wallet Types 24 | // ============================================================================ 25 | 26 | export interface WalletList { 27 | id: string; 28 | name: string; 29 | addresses: string[]; 30 | createdAt: number; 31 | updatedAt: number; 32 | } 33 | 34 | export interface SelectedWallet { 35 | privateKey: string; 36 | address: string; 37 | displayName: string; 38 | } 39 | 40 | export interface WalletType { 41 | address: string; 42 | privateKey?: string; 43 | name?: string; 44 | balance?: number; 45 | } 46 | 47 | // ============================================================================ 48 | // Sniper Bot Types 49 | // ============================================================================ 50 | 51 | export type SniperEventType = 'deploy' | 'migration' | 'both'; 52 | export type BuyAmountType = 'fixed' | 'percentage'; 53 | 54 | export interface DeployEventData { 55 | platform: string; 56 | mint: string; 57 | signer: string; 58 | name: string; 59 | symbol: string; 60 | uri: string; 61 | slot: number; 62 | creator_buy_sol?: number; 63 | creator_buy_tokens?: number; 64 | creator_buy_price?: number; 65 | } 66 | 67 | export interface MigrationEventData { 68 | mint: string; 69 | platform: string; 70 | slot: number; 71 | } 72 | 73 | export interface DeployEvent { 74 | type: 'deploy'; 75 | timestamp: number; 76 | data: DeployEventData; 77 | } 78 | 79 | export interface MigrationEvent { 80 | type: 'migration'; 81 | timestamp: number; 82 | data: MigrationEventData; 83 | } 84 | 85 | export type SniperEvent = DeployEvent | MigrationEvent; 86 | 87 | export interface SniperFilter { 88 | id: string; 89 | enabled: boolean; 90 | platform?: string; 91 | mint?: string; 92 | signer?: string; 93 | namePattern?: string; 94 | nameMatchType?: FilterMatchType; 95 | symbolPattern?: string; 96 | symbolMatchType?: FilterMatchType; 97 | } 98 | 99 | export interface SniperProfile { 100 | id: string; 101 | name: string; 102 | description: string; 103 | isActive: boolean; 104 | eventType: SniperEventType; 105 | filters: SniperFilter[]; 106 | buyAmountType: BuyAmountType; 107 | buyAmount: number; 108 | slippage: number; 109 | priority: PriorityLevel; 110 | cooldown: number; 111 | cooldownUnit: CooldownUnit; 112 | maxExecutions?: number; 113 | executionCount: number; 114 | lastExecuted?: number; 115 | createdAt: number; 116 | updatedAt: number; 117 | } 118 | 119 | export interface SniperExecutionLog { 120 | id: string; 121 | profileId: string; 122 | profileName: string; 123 | triggerEvent: SniperEvent; 124 | executedAction: { 125 | mint: string; 126 | solAmount: number; 127 | walletAddress: string; 128 | txSignature?: string; 129 | }; 130 | success: boolean; 131 | error?: string; 132 | timestamp: number; 133 | } 134 | 135 | export interface SniperBotStorage { 136 | profiles: SniperProfile[]; 137 | executionLogs: SniperExecutionLog[]; 138 | } 139 | 140 | export interface SniperBotWebSocketConfig { 141 | apiKey?: string; 142 | onDeploy: (event: DeployEvent) => void; 143 | onMigration: (event: MigrationEvent) => void; 144 | onError?: (error: Error) => void; 145 | onConnect?: () => void; 146 | onDisconnect?: () => void; 147 | } 148 | 149 | // ============================================================================ 150 | // Copy Trade Types 151 | // ============================================================================ 152 | 153 | export type CopyTradeMode = 'simple' | 'advanced'; 154 | export type TokenFilterMode = 'all' | 'specific'; 155 | 156 | export interface CopyTradeCondition { 157 | id: string; 158 | type: 'tradeSize' | 'marketCap' | 'tokenAge' | 'tradeType' | 'signerBalance'; 159 | operator: OperatorType; 160 | value: number; 161 | tradeType?: 'buy' | 'sell'; 162 | } 163 | 164 | export interface CopyTradeAction { 165 | id: string; 166 | type: 'buy' | 'sell' | 'mirror'; 167 | amountType: 'multiplier' | 'fixed' | 'percentage'; 168 | amount: number; 169 | slippage: number; 170 | priority: PriorityLevel; 171 | } 172 | 173 | export interface SimpleModeCopyConfig { 174 | amountMultiplier: number; 175 | slippage: number; 176 | priority: PriorityLevel; 177 | mirrorTradeType: boolean; 178 | } 179 | 180 | export interface CopyTradeProfile { 181 | id: string; 182 | name: string; 183 | description: string; 184 | isActive: boolean; 185 | mode: CopyTradeMode; 186 | simpleConfig?: SimpleModeCopyConfig; 187 | conditions: CopyTradeCondition[]; 188 | conditionLogic: 'and' | 'or'; 189 | actions: CopyTradeAction[]; 190 | walletListId: string | null; 191 | walletAddresses: string[]; 192 | tokenFilterMode: TokenFilterMode; 193 | specificTokens: string[]; 194 | blacklistedTokens: string[]; 195 | cooldown: number; 196 | cooldownUnit: CooldownUnit; 197 | maxExecutions?: number; 198 | executionCount: number; 199 | lastExecuted?: number; 200 | createdAt: number; 201 | updatedAt: number; 202 | } 203 | 204 | export interface CopyTradeData { 205 | type: 'buy' | 'sell'; 206 | signerAddress: string; 207 | tokenMint: string; 208 | tokenAmount: number; 209 | solAmount: number; 210 | avgPrice: number; 211 | marketCap: number; 212 | timestamp: number; 213 | signature: string; 214 | } 215 | 216 | export interface CopyTradeExecutionLog { 217 | id: string; 218 | profileId: string; 219 | profileName: string; 220 | originalTrade: CopyTradeData; 221 | executedAction: { 222 | type: 'buy' | 'sell'; 223 | amount: number; 224 | walletAddress: string; 225 | }; 226 | success: boolean; 227 | error?: string; 228 | timestamp: number; 229 | } 230 | 231 | export interface CopyTradeProfileStorage { 232 | profiles: CopyTradeProfile[]; 233 | executionLogs: CopyTradeExecutionLog[]; 234 | } 235 | 236 | // ============================================================================ 237 | // Automate (Strategy) Types 238 | // ============================================================================ 239 | 240 | export interface TradingCondition { 241 | id: string; 242 | type: 'marketCap' | 'buyVolume' | 'sellVolume' | 'netVolume' | 'lastTradeAmount' | 'lastTradeType' | 'whitelistActivity'; 243 | operator: OperatorType; 244 | value: number; 245 | timeframe?: number; 246 | whitelistAddress?: string; 247 | whitelistActivityType?: 'buyVolume' | 'sellVolume' | 'netVolume' | 'lastTradeAmount' | 'lastTradeType'; 248 | } 249 | 250 | export interface TradingAction { 251 | id: string; 252 | type: 'buy' | 'sell'; 253 | amountType: 'percentage' | 'sol' | 'lastTrade' | 'volume' | 'whitelistVolume'; 254 | amount: number; 255 | volumeType?: 'buyVolume' | 'sellVolume' | 'netVolume'; 256 | volumeMultiplier?: number; 257 | whitelistAddress?: string; 258 | whitelistActivityType?: 'buyVolume' | 'sellVolume' | 'netVolume'; 259 | slippage: number; 260 | priority: PriorityLevel; 261 | } 262 | 263 | export interface TradingStrategy { 264 | id: string; 265 | name: string; 266 | description: string; 267 | conditions: TradingCondition[]; 268 | conditionLogic: 'and' | 'or'; 269 | actions: TradingAction[]; 270 | isActive: boolean; 271 | cooldown: number; 272 | cooldownUnit: CooldownUnit; 273 | maxExecutions?: number; 274 | executionCount: number; 275 | lastExecuted?: number; 276 | createdAt: number; 277 | updatedAt: number; 278 | whitelistedAddresses: string[]; 279 | tokenAddresses: string[]; 280 | walletAddresses: string[]; 281 | } 282 | 283 | export interface AutomateTrade { 284 | signature: string; 285 | type: 'buy' | 'sell'; 286 | address: string; 287 | tokenAmount: number; 288 | solAmount: number; 289 | timestamp: number; 290 | } 291 | 292 | export interface MarketData { 293 | marketCap: number; 294 | buyVolume: number; 295 | sellVolume: number; 296 | netVolume: number; 297 | lastTrade: AutomateTrade | null; 298 | tokenPrice: number; 299 | priceChange24h?: number; 300 | } 301 | 302 | export interface TokenMonitor { 303 | tokenAddress: string; 304 | marketData: MarketData; 305 | activeStrategyIds: string[]; 306 | trades: AutomateTrade[]; 307 | wsConnected: boolean; 308 | addedAt: number; 309 | } 310 | 311 | export interface ActiveStrategyInstance { 312 | strategyId: string; 313 | tokenAddress: string; 314 | executionCount: number; 315 | lastExecuted?: number; 316 | isActive: boolean; 317 | } 318 | 319 | // ============================================================================ 320 | // Unified Profile Type (for combined management) 321 | // ============================================================================ 322 | 323 | export type UnifiedProfile = 324 | | { type: 'sniper'; profile: SniperProfile } 325 | | { type: 'copytrade'; profile: CopyTradeProfile } 326 | | { type: 'automate'; profile: TradingStrategy }; 327 | 328 | // ============================================================================ 329 | // UI State Types 330 | // ============================================================================ 331 | 332 | export interface ToolsUIState { 333 | activeTab: ToolType; 334 | isCreating: boolean; 335 | isEditing: string | null; 336 | selectedProfileId: string | null; 337 | searchTerm: string; 338 | filterActive: boolean | null; 339 | } 340 | 341 | export interface RecentSniperEvent { 342 | id: string; 343 | event: SniperEvent; 344 | matchedProfiles: string[]; 345 | sniped: boolean; 346 | timestamp: number; 347 | } 348 | -------------------------------------------------------------------------------- /src/utils/trading.ts: -------------------------------------------------------------------------------- 1 | import type { WalletType } from '../Utils'; 2 | import { executeBuy, createBuyConfig } from './buy'; 3 | import type { BundleMode } from './buy'; 4 | import { executeSell, createSellConfig } from './sell'; 5 | 6 | export interface TradingConfig { 7 | tokenAddress: string; 8 | solAmount?: number; 9 | sellPercent?: number; 10 | tokensAmount?: number; 11 | bundleMode?: BundleMode; 12 | batchDelay?: number; 13 | singleDelay?: number; 14 | } 15 | 16 | export interface FormattedWallet { 17 | address: string; 18 | privateKey: string; 19 | } 20 | 21 | export interface TradingResult { 22 | success: boolean; 23 | error?: string; 24 | } 25 | 26 | // Unified buy function using the new buy.ts 27 | const executeUnifiedBuy = async ( 28 | wallets: FormattedWallet[], 29 | config: TradingConfig, 30 | slippageBps?: number, 31 | jitoTipLamports?: number, 32 | transactionsFeeLamports?: number 33 | ): Promise => { 34 | try { 35 | // Load config once for all settings 36 | const { loadConfigFromCookies } = await import('../Utils'); 37 | const appConfig = loadConfigFromCookies(); 38 | 39 | // Use provided slippage or fall back to config default 40 | let finalSlippageBps = slippageBps; 41 | if (finalSlippageBps === undefined && appConfig?.slippageBps) { 42 | finalSlippageBps = parseInt(appConfig.slippageBps); 43 | } 44 | 45 | // Use provided jito tip or fall back to config default 46 | let finalJitoTipLamports = jitoTipLamports; 47 | if (finalJitoTipLamports === undefined && appConfig?.transactionFee) { 48 | const feeInSol = appConfig.transactionFee; 49 | finalJitoTipLamports = Math.floor(parseFloat(feeInSol) * 1_000_000_000); 50 | } 51 | 52 | // Use provided transactions fee or calculate from config 53 | let finalTransactionsFeeLamports = transactionsFeeLamports; 54 | if (finalTransactionsFeeLamports === undefined && appConfig?.transactionFee) { 55 | const feeInSol = appConfig.transactionFee; 56 | finalTransactionsFeeLamports = Math.floor((parseFloat(feeInSol) / 3) * 1_000_000_000); 57 | } 58 | 59 | // Use provided bundle mode or fall back to config default 60 | let finalBundleMode = config.bundleMode; 61 | if (finalBundleMode === undefined && appConfig?.bundleMode) { 62 | finalBundleMode = appConfig.bundleMode as BundleMode; 63 | } 64 | 65 | // Use provided delays or fall back to config defaults 66 | let finalBatchDelay = config.batchDelay; 67 | if (finalBatchDelay === undefined && appConfig?.batchDelay) { 68 | finalBatchDelay = parseInt(appConfig.batchDelay); 69 | } 70 | 71 | let finalSingleDelay = config.singleDelay; 72 | if (finalSingleDelay === undefined && appConfig?.singleDelay) { 73 | finalSingleDelay = parseInt(appConfig.singleDelay); 74 | } 75 | 76 | const buyConfig = createBuyConfig({ 77 | tokenAddress: config.tokenAddress, 78 | solAmount: config.solAmount!, 79 | slippageBps: finalSlippageBps, 80 | jitoTipLamports: finalJitoTipLamports, 81 | transactionsFeeLamports: finalTransactionsFeeLamports, 82 | bundleMode: finalBundleMode, 83 | batchDelay: finalBatchDelay, 84 | singleDelay: finalSingleDelay 85 | }); 86 | 87 | return await executeBuy(wallets, buyConfig); 88 | } catch (error) { 89 | return { success: false, error: error instanceof Error ? error.message : String(error) }; 90 | } 91 | }; 92 | 93 | // Unified sell function using the new sell.ts 94 | const executeUnifiedSell = async ( 95 | wallets: FormattedWallet[], 96 | config: TradingConfig, 97 | slippageBps?: number, 98 | outputMint?: string, 99 | jitoTipLamports?: number, 100 | transactionsFeeLamports?: number 101 | ): Promise => { 102 | try { 103 | // Load config once for all settings 104 | const { loadConfigFromCookies } = await import('../Utils'); 105 | const appConfig = loadConfigFromCookies(); 106 | 107 | // Use provided slippage or fall back to config default 108 | let finalSlippageBps = slippageBps; 109 | if (finalSlippageBps === undefined && appConfig?.slippageBps) { 110 | finalSlippageBps = parseInt(appConfig.slippageBps); 111 | } 112 | 113 | // Use provided jito tip or fall back to config default 114 | let finalJitoTipLamports = jitoTipLamports; 115 | if (finalJitoTipLamports === undefined && appConfig?.transactionFee) { 116 | const feeInSol = appConfig.transactionFee; 117 | finalJitoTipLamports = Math.floor(parseFloat(feeInSol) * 1_000_000_000); 118 | } 119 | 120 | // Use provided transactions fee or calculate from config 121 | let finalTransactionsFeeLamports = transactionsFeeLamports; 122 | if (finalTransactionsFeeLamports === undefined && appConfig?.transactionFee) { 123 | const feeInSol = appConfig.transactionFee; 124 | finalTransactionsFeeLamports = Math.floor((parseFloat(feeInSol) / 3) * 1_000_000_000); 125 | } 126 | 127 | // Use provided bundle mode or fall back to config default 128 | let finalBundleMode = config.bundleMode; 129 | if (finalBundleMode === undefined && appConfig?.bundleMode) { 130 | finalBundleMode = appConfig.bundleMode as BundleMode; 131 | } 132 | 133 | // Use provided delays or fall back to config defaults 134 | let finalBatchDelay = config.batchDelay; 135 | if (finalBatchDelay === undefined && appConfig?.batchDelay) { 136 | finalBatchDelay = parseInt(appConfig.batchDelay); 137 | } 138 | 139 | let finalSingleDelay = config.singleDelay; 140 | if (finalSingleDelay === undefined && appConfig?.singleDelay) { 141 | finalSingleDelay = parseInt(appConfig.singleDelay); 142 | } 143 | 144 | const sellConfig = createSellConfig({ 145 | tokenAddress: config.tokenAddress, 146 | sellPercent: config.sellPercent, 147 | tokensAmount: config.tokensAmount, 148 | slippageBps: finalSlippageBps, 149 | outputMint, 150 | jitoTipLamports: finalJitoTipLamports, 151 | transactionsFeeLamports: finalTransactionsFeeLamports, 152 | bundleMode: finalBundleMode, 153 | batchDelay: finalBatchDelay, 154 | singleDelay: finalSingleDelay 155 | }); 156 | 157 | return await executeSell(wallets, sellConfig); 158 | } catch (error) { 159 | return { success: false, error: error instanceof Error ? error.message : String(error) }; 160 | } 161 | }; 162 | 163 | // Main trading executor 164 | export const executeTrade = async ( 165 | _dex: string, 166 | wallets: WalletType[], 167 | config: TradingConfig, 168 | isBuyMode: boolean, 169 | solBalances: Map 170 | ): Promise => { 171 | const activeWallets = wallets.filter(wallet => wallet.isActive); 172 | 173 | if (activeWallets.length === 0) { 174 | return { success: false, error: 'Please activate at least one wallet' }; 175 | } 176 | 177 | const formattedWallets = activeWallets.map(wallet => ({ 178 | address: wallet.address, 179 | privateKey: wallet.privateKey 180 | })); 181 | 182 | const walletBalances = new Map(); 183 | activeWallets.forEach(wallet => { 184 | const balance = solBalances.get(wallet.address) || 0; 185 | walletBalances.set(wallet.address, balance); 186 | }); 187 | try { 188 | if (isBuyMode) { 189 | return await executeUnifiedBuy(formattedWallets, config); 190 | } else { 191 | return await executeUnifiedSell(formattedWallets, config); 192 | } 193 | } catch (error) { 194 | return { success: false, error: error instanceof Error ? error.message : String(error) }; 195 | } 196 | 197 | }; 198 | 199 | /** 200 | * Trade history management utilities 201 | * Stores and retrieves trade history from localStorage 202 | */ 203 | 204 | export interface TradeHistoryEntry { 205 | id: string; 206 | type: 'buy' | 'sell'; 207 | tokenAddress: string; 208 | timestamp: number; 209 | walletsCount: number; 210 | amount: number; 211 | amountType: 'sol' | 'percentage'; 212 | success: boolean; 213 | error?: string; 214 | bundleMode?: 'single' | 'batch' | 'all-in-one'; 215 | } 216 | 217 | const STORAGE_KEY = 'raze_trade_history'; 218 | const MAX_HISTORY_ENTRIES = 50; // Keep last 50 trades 219 | 220 | /** 221 | * Get all trade history entries from localStorage 222 | */ 223 | export const getTradeHistory = (): TradeHistoryEntry[] => { 224 | try { 225 | const stored = localStorage.getItem(STORAGE_KEY); 226 | if (!stored) return []; 227 | 228 | const history = JSON.parse(stored) as TradeHistoryEntry[]; 229 | // Sort by timestamp descending (newest first) 230 | return history.sort((a, b) => b.timestamp - a.timestamp); 231 | } catch (error) { 232 | console.error('Error reading trade history:', error); 233 | return []; 234 | } 235 | }; 236 | 237 | /** 238 | * Add a new trade entry to history 239 | */ 240 | export const addTradeHistory = (entry: Omit): void => { 241 | try { 242 | const history = getTradeHistory(); 243 | 244 | const newEntry: TradeHistoryEntry = { 245 | ...entry, 246 | id: `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`, 247 | timestamp: Date.now() 248 | }; 249 | 250 | // Add to beginning of array (newest first) 251 | history.unshift(newEntry); 252 | 253 | // Keep only the last MAX_HISTORY_ENTRIES entries 254 | const trimmedHistory = history.slice(0, MAX_HISTORY_ENTRIES); 255 | 256 | localStorage.setItem(STORAGE_KEY, JSON.stringify(trimmedHistory)); 257 | 258 | // Dispatch custom event for real-time updates 259 | window.dispatchEvent(new CustomEvent('tradeHistoryUpdated', { detail: newEntry })); 260 | } catch (error) { 261 | console.error('Error saving trade history:', error); 262 | } 263 | }; 264 | 265 | /** 266 | * Clear all trade history 267 | */ 268 | export const clearTradeHistory = (): void => { 269 | try { 270 | localStorage.removeItem(STORAGE_KEY); 271 | window.dispatchEvent(new CustomEvent('tradeHistoryUpdated')); 272 | } catch (error) { 273 | console.error('Error clearing trade history:', error); 274 | } 275 | }; 276 | 277 | /** 278 | * Get latest trades (up to limit) 279 | */ 280 | export const getLatestTrades = (limit: number = 10): TradeHistoryEntry[] => { 281 | const history = getTradeHistory(); 282 | return history.slice(0, limit); 283 | }; 284 | 285 | /** 286 | * Get trades for a specific token 287 | */ 288 | export const getTradesForToken = (tokenAddress: string): TradeHistoryEntry[] => { 289 | const history = getTradeHistory(); 290 | return history.filter(trade => trade.tokenAddress === tokenAddress); 291 | }; 292 | -------------------------------------------------------------------------------- /src/components/modals/ExportSeedPhraseModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { createPortal } from 'react-dom'; 3 | import { X, Copy, Check, AlertTriangle, Eye, EyeOff } from 'lucide-react'; 4 | 5 | interface ExportSeedPhraseModalProps { 6 | isOpen: boolean; 7 | onClose: () => void; 8 | mnemonic: string; 9 | masterWalletName: string; 10 | } 11 | 12 | const ExportSeedPhraseModal: React.FC = ({ 13 | isOpen, 14 | onClose, 15 | mnemonic, 16 | masterWalletName, 17 | }) => { 18 | const [confirmed, setConfirmed] = useState(false); 19 | const [showMnemonic, setShowMnemonic] = useState(false); 20 | const [copied, setCopied] = useState(false); 21 | const [countdown, setCountdown] = useState(null); 22 | 23 | // Reset state when modal opens 24 | useEffect(() => { 25 | if (isOpen) { 26 | setConfirmed(false); 27 | setShowMnemonic(false); 28 | setCopied(false); 29 | setCountdown(null); 30 | } 31 | }, [isOpen]); 32 | 33 | // Start countdown when mnemonic is revealed 34 | useEffect(() => { 35 | if (showMnemonic && countdown === null) { 36 | setCountdown(30); 37 | } 38 | }, [showMnemonic, countdown]); 39 | 40 | // Countdown timer 41 | useEffect(() => { 42 | if (countdown === null || countdown === 0) return; 43 | 44 | const timer = setTimeout(() => { 45 | setCountdown(countdown - 1); 46 | if (countdown === 1) { 47 | setShowMnemonic(false); 48 | setCountdown(null); 49 | } 50 | }, 1000); 51 | 52 | return () => clearTimeout(timer); 53 | }, [countdown]); 54 | 55 | const handleCopyMnemonic = async (): Promise => { 56 | try { 57 | await navigator.clipboard.writeText(mnemonic); 58 | setCopied(true); 59 | setTimeout(() => setCopied(false), 2000); 60 | } catch (error) { 61 | console.error('Failed to copy mnemonic:', error); 62 | } 63 | }; 64 | 65 | const handleReveal = (): void => { 66 | if (confirmed) { 67 | setShowMnemonic(true); 68 | } 69 | }; 70 | 71 | if (!isOpen) return null; 72 | 73 | const mnemonicWords = mnemonic.split(' '); 74 | 75 | return createPortal( 76 |
80 |
e.stopPropagation()} 84 | > 85 | {/* Header */} 86 |
87 |
88 |

89 | Export Seed Phrase 90 |

91 |

92 | {masterWalletName} 93 |

94 |
95 | 101 |
102 | 103 | {/* Content */} 104 |
105 | {/* Security Warning */} 106 |
107 |
108 | 109 |
110 |

Critical Security Warning

111 |
    112 |
  • 113 | 114 | Anyone with this seed phrase can access ALL wallets derived from it 115 |
  • 116 |
  • 117 | 118 | This includes stealing all your funds permanently 119 |
  • 120 |
  • 121 | 122 | Never share this with anyone, not even support staff 123 |
  • 124 |
  • 125 | 126 | Make sure nobody is watching your screen 127 |
  • 128 |
  • 129 | 130 | The seed phrase will automatically hide after 30 seconds 131 |
  • 132 |
133 |
134 |
135 |
136 | 137 | {/* Confirmation Checkbox */} 138 |
139 | setConfirmed(e.target.checked)} 144 | className="w-5 h-5 rounded border-app-primary-40 text-app-primary-color 145 | focus:ring-app-primary-color cursor-pointer" 146 | /> 147 | 153 |
154 | 155 | {/* Seed Phrase Display */} 156 | {!showMnemonic ? ( 157 |
158 | 159 |

160 | Seed phrase is hidden for security 161 |

162 | 174 |
175 | ) : ( 176 |
177 |
178 |
179 |

180 | YOUR SEED PHRASE ({mnemonicWords.length} WORDS) 181 |

182 | {countdown !== null && ( 183 | 184 | Auto-hiding in {countdown}s 185 | 186 | )} 187 |
188 | 205 |
206 | 207 |
208 | {mnemonicWords.map((word, index) => ( 209 |
214 | {index + 1}. 215 | {word} 216 |
217 | ))} 218 |
219 | 220 |
221 | 231 |
232 |
233 | )} 234 |
235 | 236 | {/* Footer */} 237 |
238 | 245 |
246 |
247 |
, 248 | document.body 249 | ); 250 | }; 251 | 252 | export default ExportSeedPhraseModal; 253 | 254 | --------------------------------------------------------------------------------