File size: 24,282 Bytes
42754c0
1
// File: src/App.tsx import React from 'react'; import {    ConnectionProvider,  // File: src/index.ts import express from 'express'; import cors from 'cors'; import { Connection, PublicKey, clusterApiUrl } from '@solana/web3.js'; import { TOKEN_PROGRAM_ID } from '@solana/spl-token'; import { scanWalletRoute } from './routes/scanWallet'; import { tokenInfoRoute } from './routes/tokenInfo'; import { stakingRoute } from './routes/staking'; import { config } from './config';  const app = express(); const PORT = process.env.PORT || 3001;  // Configure RPC connection const connection = new Connection(   process.env.SOLANA_RPC_URL || clusterApiUrl('mainnet-beta'),   'confirmed' );  // Middleware app.use(cors()); app.use(express.json());  // Pass the connection to routes app.locals.connection = connection;  // Routes app.use('/api/scan', scanWalletRoute); app.use('/api/tokens', tokenInfoRoute); app.use('/api/staking', stakingRoute);  // Health check endpoint app.get('/health', (req, res) => {   res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() }); });  // Start server app.listen(PORT, () => {   console.log(`BONK AI API server running on port ${PORT}`);   console.log(`Environment: ${process.env.NODE_ENV}`); });  // File: src/config.ts // Configuration settings for the BONK AI backend export const config = {   // Token addresses   tokens: {     BONK: 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263',     BONKAI: '', // To be filled when token is created     WSOL: 'So11111111111111111111111111111111111111112'   },      // Programs   programs: {     TOKEN_PROGRAM: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',     BONK_AI_REGISTRY: '', // To be filled when deployed     BONK_AI_STAKING: '',  // To be filled when deployed   },      // RPC endpoint fallbacks   rpc: {     mainnet: process.env.MAINNET_RPC_URL || 'https://api.mainnet-beta.solana.com',     devnet: process.env.DEVNET_RPC_URL || 'https://api.devnet.solana.com'   },      // Cache settings   cache: {     ttl: 60 * 1000, // 1 minute cache for basic data     walletTtl: 5 * 60 * 1000 // 5 minutes for wallet data   },      // Rate limiting   rateLimit: {     windowMs: 15 * 60 * 1000, // 15 minutes     max: 100 // limit each IP to 100 requests per windowMs   } };  // File: src/routes/scanWallet.ts import { Router, Request, Response } from 'express'; import { PublicKey } from '@solana/web3.js'; import { TOKEN_PROGRAM_ID } from '@solana/spl-token'; import { config } from '../config'; import { scanWalletForCloseableAccounts } from '../services/walletService';  const router = Router();  // Endpoint to scan wallet for closeable accounts router.get('/:walletAddress', async (req: Request, res: Response) => {   try {  WalletProvider  } from '@solana/wallet-adapter-react'; import {    WalletAdapterNetwork  } from '@solana/wallet-adapter-base'; import {   PhantomWalletAdapter,   SolflareWalletAdapter,   BackpackWalletAdapter } from '@solana/wallet-adapter-wallets'; import {   WalletModalProvider } from '@solana/wallet-adapter-react-ui'; import { clusterApiUrl } from '@solana/web3.js'; import { BonkDashboard } from './components/BonkDashboard'; import { TokenProvider } from './context/TokenContext'; import { BonkThemeProvider } from './context/ThemeContext';  // Import wallet adapter styles import '@solana/wallet-adapter-react-ui/styles.css'; import './styles/global.css';  function App() {   // Set network to 'mainnet-beta' for production   const network = WalletAdapterNetwork.MainnetBeta;      // You can also provide a custom RPC endpoint   const endpoint = process.env.REACT_APP_RPC_URL || clusterApiUrl(network);      // Initialize wallet adapters for common Solana wallets   const wallets = [     new PhantomWalletAdapter(),     new SolflareWalletAdapter(),     new BackpackWalletAdapter()   ];    return (     <BonkThemeProvider>       <ConnectionProvider endpoint={endpoint}>         <WalletProvider wallets={wallets} autoConnect>           <WalletModalProvider>             <TokenProvider>               <div className="app-container">                 <BonkDashboard />               </div>             </TokenProvider>           </WalletModalProvider>         </WalletProvider>       </ConnectionProvider>     </BonkThemeProvider>   ); }  export default App;  // File: src/components/BonkDashboard.tsx import React, { useState } from 'react'; import { useWallet } from '@solana/wallet-adapter-react'; import { WalletMultiButton } from '@solana/wallet-adapter-react-ui'; import { BonkButton } from './BonkButton'; import { WalletOverview } from './WalletOverview'; import { StakingSection } from './StakingSection'; import { Footer } from './Footer'; import { Header } from './Header';  export const BonkDashboard: React.FC = () => {   const { publicKey } = useWallet();   const [activeTab, setActiveTab] = useState('dashboard');      return (     <div className="dashboard-container">       <Header />              <div className="wallet-connect-section">         <WalletMultiButton />         {!publicKey && (           <div className="connect-prompt">             <h2>Connect your wallet to use the BONK Button</h2>             <p>Clean up your Solana wallet and recover SOL with just one click!</p>           </div>         )}       </div>              {publicKey && (         <>           <div className="tabs">             <button                className={`tab ${activeTab === 'dashboard' ? 'active' : ''}`}               onClick={() => setActiveTab('dashboard')}             >               Dashboard             </button>             <button                className={`tab ${activeTab === 'bonkbutton' ? 'active' : ''}`}               onClick={() => setActiveTab('bonkbutton')}             >               BONK Button             </button>             <button                className={`tab ${activeTab === 'staking' ? 'active' : ''}`}               onClick={() => setActiveTab('staking')}             >               Staking             </button>           </div>                      <div className="tab-content">             {activeTab === 'dashboard' && <WalletOverview />}             {activeTab === 'bonkbutton' && <BonkButton />}             {activeTab === 'staking' && <StakingSection />}           </div>         </>       )}              <Footer />     </div>   ); };  // File: src/components/BonkButton.tsx import React, { useState, useCallback, useEffect } from 'react'; import { useConnection, useWallet } from '@solana/wallet-adapter-react'; import {    Transaction,    PublicKey,    sendAndConfirmTransaction  } from '@solana/web3.js'; import {    TOKEN_PROGRAM_ID,    createCloseAccountInstruction,   getAccount,   getAssociatedTokenAddress  } from '@solana/spl-token'; import '../styles/BonkButton.css';  interface TokenAccount {   pubkey: PublicKey;   account: {     mint: PublicKey;     owner: PublicKey;     amount: bigint;     isNative: boolean;   }   isWrappedSol: boolean; }  interface CloseableAccount {   address: PublicKey;   mint: PublicKey;   isWrappedSol: boolean;   balance: string; }  const WSOL_MINT = new PublicKey('So11111111111111111111111111111111111111112');  export const BonkButton: React.FC = () => {   const { connection } = useConnection();   const { publicKey, sendTransaction } = useWallet();      const [isScanning, setIsScanning] = useState(false);   const [closeableAccounts, setCloseableAccounts] = useState<CloseableAccount[]>([]);   const [selectedAccounts, setSelectedAccounts] = useState<string[]>([]);   const [isCleaning, setIsCleaning] = useState(false);   const [cleaningComplete, setCleaningComplete] = useState(false);   const [recoveredSol, setRecoveredSol] = useState(0);   const [error, setError] = useState<string | null>(null);    // Scan for closeable accounts   const scanForCloseableAccounts = useCallback(async () => {     if (!publicKey) return;          setIsScanning(true);     setError(null);          try {       // Get all token accounts owned by this wallet       const tokenAccounts = await connection.getParsedTokenAccountsByOwner(         publicKey,         { programId: TOKEN_PROGRAM_ID }       );              const closeableAccts: CloseableAccount[] = [];              for (const { pubkey, account } of tokenAccounts.value) {         const parsedAccount = account.data.parsed.info;         const isWrappedSol = parsedAccount.mint === WSOL_MINT.toString();         const balance = parsedAccount.tokenAmount.uiAmount;                  // Account is closeable if it's wrapped SOL or has zero balance         if (isWrappedSol || balance === 0) {           closeableAccts.push({             address: pubkey,             mint: new PublicKey(parsedAccount.mint),             isWrappedSol,             balance: balance.toString()           });         }       }              setCloseableAccounts(closeableAccts);              // Auto-select all accounts by default       setSelectedAccounts(closeableAccts.map(acct => acct.address.toString()));            } catch (err) {       console.error('Error scanning accounts:', err);       setError('Failed to scan accounts. Please try again.');     } finally {       setIsScanning(false);     }   }, [publicKey, connection]);      // Handle account selection   const toggleAccountSelection = (accountAddress: string) => {     setSelectedAccounts(prev => {       if (prev.includes(accountAddress)) {         return prev.filter(addr => addr !== accountAddress);       } else {         return [...prev, accountAddress];       }     });   };      // Clean selected accounts (close them)   const cleanSelectedAccounts = useCallback(async () => {     if (!publicKey || selectedAccounts.length === 0) return;          setIsCleaning(true);     setError(null);          try {       const transaction = new Transaction();              // Add close instruction for each selected account       for (const accountAddr of selectedAccounts) {         const account = closeableAccounts.find(           acct => acct.address.toString() === accountAddr         );                  if (account) {           transaction.add(             createCloseAccountInstruction(               account.address,        // token account to close               publicKey,              // destination for remaining SOL               publicKey               // account owner             )           );         }       }              // Send transaction       const signature = await sendTransaction(transaction, connection);              // Wait for confirmation       await connection.confirmTransaction(signature, 'confirmed');              // Calculate approximate SOL recovered (this is an estimate)       // Each account closure recovers ~0.002 SOL in rent       const estimatedRecovery = selectedAccounts.length * 0.002;       setRecoveredSol(estimatedRecovery);       setCleaningComplete(true);              // Reset the closeable accounts list       setCloseableAccounts([]);       setSelectedAccounts([]);            } catch (err) {       console.error('Error closing accounts:', err);       setError('Failed to close accounts. Please try again.');     } finally {       setIsCleaning(false);     }   }, [publicKey, connection, sendTransaction, selectedAccounts, closeableAccounts]);      // Reset the cleanup state to scan again   const resetCleanup = () => {     setCleaningComplete(false);     setRecoveredSol(0);   };    return (     <div className="bonk-button-container">       <div className="bonk-button-header">         <h2>BONK Button</h2>         <p>Clean up your wallet and recover SOL with one click!</p>       </div>              {error && (         <div className="error-message">           {error}         </div>       )}              {!cleaningComplete ? (         <>           {closeableAccounts.length === 0 ? (             <button                className="scan-button"               onClick={scanForCloseableAccounts}               disabled={isScanning || !publicKey}             >               {isScanning ? 'Scanning...' : 'Scan My Wallet'}             </button>           ) : (             <div className="accounts-section">               <h3>Found {closeableAccounts.length} closeable accounts:</h3>                              <div className="accounts-list">                 {closeableAccounts.map((account) => (                   <div                      key={account.address.toString()}                      className="account-item"                   >                     <input                       type="checkbox"                       checked={selectedAccounts.includes(account.address.toString())}                       onChange={() => toggleAccountSelection(account.address.toString())}                     />                     <div className="account-info">                       <span className="account-address">                         {account.address.toString().slice(0, 4)}...                         {account.address.toString().slice(-4)}                       </span>                       <span className="account-type">                         {account.isWrappedSol ? 'Wrapped SOL' : 'Empty Token Account'}                       </span>                     </div>                   </div>                 ))}               </div>                              <button                 className="bonk-button"                 onClick={cleanSelectedAccounts}                 disabled={isCleaning || selectedAccounts.length === 0}               >                 {isCleaning ? 'BONKING...' : 'BONK IT!'}               </button>             </div>           )}         </>       ) : (         <div className="cleanup-complete">           <h3>BONK! Wallet Cleaned Successfully!</h3>           <p>You've recovered approximately {recoveredSol.toFixed(3)} SOL</p>           <p>Your wallet is now cleaner and more efficient!</p>           <button              className="scan-again-button"             onClick={resetCleanup}           >             Scan Again           </button>         </div>       )}     </div>   ); };  // File: src/components/WalletOverview.tsx import React, { useEffect, useState } from 'react'; import { useConnection, useWallet } from '@solana/wallet-adapter-react'; import { LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js'; import { TOKEN_PROGRAM_ID } from '@solana/spl-token'; import { useTokenBalances } from '../hooks/useTokenBalances'; import '../styles/WalletOverview.css';  // Known token addresses (mainnet) const BONK_TOKEN_ADDRESS = 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263'; const BONKAI_TOKEN_ADDRESS = ''; // To be filled when BONKAI is created  export const WalletOverview: React.FC = () => {   const { connection } = useConnection();   const { publicKey } = useWallet();   const [solBalance, setSolBalance] = useState(0);   const {      bonkBalance,      bonkaiBalance,      isLoading: isLoadingTokens    } = useTokenBalances();   const [accountCount, setAccountCount] = useState(0);    useEffect(() => {     if (!publicKey) return;      const fetchSolBalance = async () => {       try {         const balance = await connection.getBalance(publicKey);         setSolBalance(balance / LAMPORTS_PER_SOL);       } catch (error) {         console.error('Error fetching SOL balance:', error);       }     };      const fetchAccountCount = async () => {       try {         const tokenAccounts = await connection.getParsedTokenAccountsByOwner(           publicKey,           { programId: TOKEN_PROGRAM_ID }         );         setAccountCount(tokenAccounts.value.length);       } catch (error) {         console.error('Error fetching token accounts:', error);       }     };      fetchSolBalance();     fetchAccountCount();          // Set up interval to refresh balances     const intervalId = setInterval(() => {       fetchSolBalance();     }, 30000); // every 30 seconds          return () => clearInterval(intervalId);   }, [publicKey, connection]);    if (!publicKey) {     return <div>Please connect your wallet to view this section.</div>;   }    return (     <div className="wallet-overview">       <h2>Wallet Overview</h2>              <div className="wallet-address">         {publicKey.toString().slice(0, 6)}...{publicKey.toString().slice(-6)}       </div>              <div className="balances-section">         <div className="balance-card sol">           <div className="token-icon">β—Ž</div>           <div className="token-details">             <h3>SOL</h3>             <p className="balance">{solBalance.toFixed(4)} SOL</p>           </div>         </div>                  <div className="balance-card bonk">           <div className="token-icon">πŸ•</div>           <div className="token-details">             <h3>BONK</h3>             <p className="balance">               {isLoadingTokens ? 'Loading...' : `${bonkBalance.toLocaleString()} BONK`}             </p>           </div>         </div>                  <div className="balance-card bonkai">           <div className="token-icon">πŸ€–</div>           <div className="token-details">             <h3>BONKAI</h3>             <p className="balance">               {isLoadingTokens ? 'Loading...' : `${bonkaiBalance.toLocaleString()} BONKAI`}             </p>           </div>         </div>       </div>              <div className="account-stats">         <div className="stat">           <h4>Token Accounts</h4>           <p>{accountCount}</p>         </div>         <div className="stat">           <h4>Estimated Rent</h4>           <p>~{(accountCount * 0.002).toFixed(3)} SOL</p>         </div>       </div>              <div className="bonk-tip">         <p>πŸ’‘ Use the BONK Button to clean up unused token accounts and recover SOL!</p>       </div>     </div>   ); };  // File: src/hooks/useTokenBalances.ts import { useState, useEffect } from 'react'; import { useConnection, useWallet } from '@solana/wallet-adapter-react'; import { PublicKey } from '@solana/web3.js'; import {    TOKEN_PROGRAM_ID,   getAssociatedTokenAddress  } from '@solana/spl-token';  // Known token addresses const BONK_TOKEN_ADDRESS = new PublicKey('DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263'); const BONKAI_TOKEN_ADDRESS = ''; // To be filled when BONKAI is created  export function useTokenBalances() {   const { connection } = useConnection();   const { publicKey } = useWallet();      const [bonkBalance, setBonkBalance] = useState(0);   const [bonkaiBalance, setBonkaiBalance] = useState(0);   const [isLoading, setIsLoading] = useState(false);   const [error, setError] = useState<string | null>(null);    useEffect(() => {     if (!publicKey) return;          const fetchTokenBalances = async () => {       setIsLoading(true);       setError(null);              try {         // Fetch all token accounts         const tokenAccounts = await connection.getParsedTokenAccountsByOwner(           publicKey,           { programId: TOKEN_PROGRAM_ID }         );                  // Find BONK balance         const bonkAccount = tokenAccounts.value.find(           ({ account }) => account.data.parsed.info.mint === BONK_TOKEN_ADDRESS.toString()         );                  if (bonkAccount) {           const balance = bonkAccount.account.data.parsed.info.tokenAmount.uiAmount;           setBonkBalance(balance);         } else {           setBonkBalance(0);         }                  // Find BONKAI balance if token address is set         if (BONKAI_TOKEN_ADDRESS) {           const bonkaiAccount = tokenAccounts.value.find(             ({ account }) => account.data.parsed.info.mint === BONKAI_TOKEN_ADDRESS           );                      if (bonkaiAccount) {             const balance = bonkaiAccount.account.data.parsed.info.tokenAmount.uiAmount;             setBonkaiBalance(balance);           } else {             setBonkaiBalance(0);           }         }       } catch (err) {         console.error('Error fetching token balances:', err);         setError('Failed to load token balances');       } finally {         setIsLoading(false);       }     };          fetchTokenBalances();          // Set up interval to refresh balances     const intervalId = setInterval(fetchTokenBalances, 30000); // every 30 seconds          return () => clearInterval(intervalId);   }, [publicKey, connection]);    return { bonkBalance, bonkaiBalance, isLoading, error }; }  // File: src/styles/global.css body {   margin: 0;   font-family: 'Press Start 2P', 'Courier New', monospace;   -webkit-font-smoothing: antialiased;   -moz-osx-font-smoothing: grayscale;   background-color: #202225;   color: #ffffff; }  .app-container {   max-width: 1200px;   margin: 0 auto;   padding: 20px; }  .dashboard-container {   display: flex;   flex-direction: column;   gap: 24px; }  .wallet-connect-section {   display: flex;   flex-direction: column;   align-items: center;   margin: 20px 0;   gap: 16px; }  .connect-prompt {   text-align: center;   max-width: 600px;   margin: 24px auto; }  .connect-prompt h2 {   font-size: 1.5rem;   margin-bottom: 16px;   color: #FFD700; }  .connect-prompt p {   font-size: 1rem;   color: #bbbbbb; }  .tabs {   display: flex;   gap: 4px;   border-bottom: 2px solid #444;   margin-bottom: 24px; }  .tab {   padding: 12px 24px;   background: none;   border: none;   color: #bbbbbb;   font-family: 'Press Start 2P', 'Courier New', monospace;   font-size: 0.9rem;   cursor: pointer;   transition: all 0.2s; }  .tab:hover {   color: #FFD700; }  .tab.active {   color: #FFD700;   border-bottom: 4px solid #FFD700;   margin-bottom: -2px; }  .tab-content {   min-height: 400px; }  button {   font-family: 'Press Start 2P', 'Courier New', monospace;   cursor: pointer; }  .error-message {   background-color: rgba(255, 0, 0, 0.1);   border: 1px solid #ff6b6b;   color: #ff6b6b;   padding: 12px;   border-radius: 4px;   margin: 16px 0; }  /* File: src/styles/BonkButton.css */ .bonk-button-container {   display: flex;   flex-direction: column;   gap: 24px;   padding: 24px;   background: #2a2d31;   border-radius: 8px;   box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); }  .bonk-button-header {   text-align: center;   margin-bottom: 24px; }  .bonk-button-header h2 {   color: #FFD700;   font-size: 1.8rem;   margin-bottom: 8px; }  .bonk-button-header p {   color: #bbbbbb;   font-size: 0.9rem; }  .scan-button, .bonk-button, .scan-again-button {   padding: 16px 24px;   font-size: 1rem;   border: none;   border-radius: 4px;   transition: all 0.2s;   text-transform: uppercase;   font-weight: bold;   margin: 0 auto;   display: block;   min-width: 200px; }  .scan-button {   background-color: #5865F2;   color: white; }  .scan-button:hover:not(:disabled) {   background-color: #4752c4;   transform: translateY(-2px); }  .scan-button:disabled {   background-color: #4752c4;   opacity: 0.6;   cursor: not-allowed; }  .bonk-button {   background-color: #FFD700;   color: #000000;   font-size: 1.2rem;   padding: 20px 36px;   box-shadow: 0 4px 0 #b89b00; }  .bonk-button:hover:not(:disabled) {   transform: translateY(-2px);   box-shadow: 0 6px 0 #b89b00; }  .bonk-button:active:not(:disabled) {   transform: translateY(2px);   box-shadow: 0 2px 0 #b89b00; }  .bonk-button:disabled {   opacity: 0.6;   cursor: not-allowed; }  .accounts-section {   display: flex;   flex-direction: column;   gap: 16px; }  .accounts-section h3 {   font-size: 1rem;   color: #FFD700;   margin-bottom: 16px; }  .accounts-list {   display: flex;   flex-direction: column;   gap: 8px;   max-height: 300px;   overflow-y: auto;   padding: 16px;   background-color: #1e2023;   border-radius: 4px;   margin-bottom: 24px; }  .account-item {   display: flex;   align-items: center;   padding: 12px;   background-color: #2a2d31;   border-radius: 4px;   gap: 12px; }  .account-info {   display: flex;   flex-direction: column;   gap: 4px; }  .account-address {   font-size: 0.8rem;   font-family: monospace;   color: #bbbbbb; }  .account-type {   font-size: 0.7rem;   color: #FFD700;   font-weight: bold; }  .cleanup-complete {   text-align: center;   padding: 24px;   background-color: rgba(255, 215, 0, 0.1);   border-radius: 8px;   margin: 24px 0; }  .cleanup-complete h3 {   color: #FFD700;   font-size: 1.5rem;   margin-bottom: 16px; }  .cleanup-complete p {   margin: 8px 0;   color: #ffffff; }  .scan-again-button {   background-color: #5865F2;   color: white;   margin-top: 16px; }  .scan-again-button:hover {   background-color: #4752c4; }