├── .env ├── .gitattributes ├── README.md ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── pump_fun_idl.json ├── src ├── app │ ├── api │ │ └── pump-proxy │ │ │ └── route.ts │ ├── layout.tsx │ ├── metadata.ts │ ├── page.tsx │ ├── providers.tsx │ └── template.tsx ├── coinData.ts ├── components │ ├── BlacklistManager.tsx │ ├── Footer.tsx │ ├── Header.tsx │ ├── OrderStatus.tsx │ ├── PurchasedTokens.tsx │ ├── TradingSettings.tsx │ └── TwitterFeed.tsx ├── constants.ts ├── contexts │ ├── BlacklistContext.tsx │ ├── BuylistContext.tsx │ ├── TradingContext.tsx │ └── WalletContext.tsx ├── dexscreenerClient.ts ├── pumpFunClient.ts ├── services │ ├── heliusService.ts │ └── twitterService.ts ├── styles │ └── globals.css ├── types.ts └── types │ └── index.ts ├── tailwind.config.js └── tsconfig.json /.env: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_TWITTER_WS_URL=BACKEND URL LINK 2 | NEXT_PUBLIC_HELIUS_RPC_URL=YOUR HELIUS RPC LINK 3 | # Choose the closest region to you for better performance 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pump.fun-Twitter-Bot 2 | Automatically monitors and trades Pump.Fun tokens based on Twitter activity. The bot can be configured to automatically buy tokens when they are tweeted about, with customizable criteria such as minimum follower count for the tweet author. 3 | 4 | ## Features 5 | - Real-time Twitter monitoring for Pump.Fun contract addresses/links 6 | - Automatic token purchases based on configurable criteria 7 | - Follower count filtering to target high-impact tweets 8 | - Support for both Pump.fun and DexScreener links 9 | - Modern web interface for monitoring and configuration 10 | - Real-time order status tracking 11 | - Blacklist and buylist functionality 12 | 13 | ## Setup and Configuration 14 | 15 | 1. Clone the repository and install dependencies: 16 | ```bash 17 | npm install 18 | ``` 19 | 20 | 2. Create a `.env` file in the root directory with the following variables: 21 | ```env 22 | NEXT_PUBLIC_HELIUS_RPC_URL=YOUR_HELIUS_RPC_LINK 23 | NEXT_PUBLIC_TWITTER_WS_URL=YOUR_BACKEND_WS_URL 24 | ``` 25 | Replace: 26 | - `YOUR_HELIUS_RPC_LINK` with your Helius RPC endpoint 27 | - `YOUR_BACKEND_WS_URL` with the WebSocket backend URL for Twitter monitoring 28 | 29 | 3. Configure your trading settings in the web interface: 30 | - Set minimum follower count for auto-buying 31 | - Configure buy amount in SOL 32 | - Set slippage tolerance 33 | - Enable/disable auto-buying 34 | - Manage blacklisted addresses 35 | - Set token creation time filters 36 | 37 | ## Running the Application 38 | 39 | Development mode: 40 | ```bash 41 | npm run dev 42 | ``` 43 | 44 | Production build: 45 | ```bash 46 | npm run build 47 | npm start 48 | ``` 49 | 50 | ## Security Notes 51 | - Never share your private keys or environment variables 52 | - Keep your `.env` file secure and never commit it to version control 53 | - Regularly monitor your wallet activity 54 | - Start with small trade amounts until you're comfortable with the bot's operation 55 | 56 | ## Trading Settings 57 | The bot can be configured through the web interface with the following options: 58 | - Minimum follower count for auto-buying 59 | - Buy amount in SOL per trade 60 | - Slippage tolerance percentage 61 | - Token age restrictions 62 | - Blacklist for specific Twitter accounts 63 | - Buylist for trusted accounts 64 | 65 | ## Logs and Monitoring 66 | - All trading activity is logged in the web interface 67 | - Real-time order status updates 68 | - Transaction links to Solscan for verification 69 | - Error reporting and notifications 70 | 71 | Enjoy trading! Remember to always trade responsibly and never invest more than you can afford to lose. -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | experimental: { 5 | appDir: true 6 | }, 7 | distDir: '.next', 8 | webpack: (config) => { 9 | config.resolve.fallback = { 10 | ...config.resolve.fallback, 11 | fs: false, 12 | os: false, 13 | path: false, 14 | crypto: false, 15 | }; 16 | return config; 17 | }, 18 | } 19 | 20 | module.exports = nextConfig 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "socialsnipe-client", 3 | "version": "1.0.0", 4 | "description": "TypeScript client for socialsnipe.fun protocol", 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "test": "jest" 11 | }, 12 | "dependencies": { 13 | "@headlessui/react": "^2.2.0", 14 | "@heroicons/react": "^2.2.0", 15 | "@project-serum/anchor": "^0.26.0", 16 | "@solana/spl-token": "^0.3.8", 17 | "@solana/web3.js": "^1.87.1", 18 | "@vercel/analytics": "^1.4.1", 19 | "axios": "^1.7.7", 20 | "bs58": "^5.0.0", 21 | "buffer": "^6.0.3", 22 | "cross-fetch": "^4.0.0", 23 | "date-fns": "^4.1.0", 24 | "encoding": "^0.1.13", 25 | "next": "13.5.4", 26 | "qrcode": "^1.5.4", 27 | "qrcode.react": "^4.1.0", 28 | "react": "^18.2.0", 29 | "react-dom": "^18.2.0", 30 | "react-hot-toast": "^2.4.1", 31 | "react-tooltip": "^5.28.0", 32 | "supports-color": "^9.4.0" 33 | }, 34 | "devDependencies": { 35 | "@tailwindcss/forms": "^0.5.6", 36 | "@types/bn.js": "^5.1.6", 37 | "@types/node": "^20.8.2", 38 | "@types/qrcode": "^1.5.5", 39 | "@types/react": "^18.2.24", 40 | "@types/react-dom": "^18.2.8", 41 | "autoprefixer": "^10.4.16", 42 | "debug": "^4.3.7", 43 | "postcss": "^8.4.31", 44 | "tailwind-scrollbar": "^3.1.0", 45 | "tailwindcss": "^3.3.3", 46 | "typescript": "^5.2.2" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /pump_fun_idl.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "name": "pump", 4 | "instructions": [ 5 | { 6 | "name": "initialize", 7 | "docs": [ 8 | "Creates the global state." 9 | ], 10 | "accounts": [ 11 | { 12 | "name": "global", 13 | "isMut": true, 14 | "isSigner": false 15 | }, 16 | { 17 | "name": "user", 18 | "isMut": true, 19 | "isSigner": true 20 | }, 21 | { 22 | "name": "systemProgram", 23 | "isMut": false, 24 | "isSigner": false 25 | } 26 | ], 27 | "args": [] 28 | }, 29 | { 30 | "name": "setParams", 31 | "docs": [ 32 | "Sets the global state parameters." 33 | ], 34 | "accounts": [ 35 | { 36 | "name": "global", 37 | "isMut": true, 38 | "isSigner": false 39 | }, 40 | { 41 | "name": "user", 42 | "isMut": true, 43 | "isSigner": true 44 | }, 45 | { 46 | "name": "systemProgram", 47 | "isMut": false, 48 | "isSigner": false 49 | }, 50 | { 51 | "name": "eventAuthority", 52 | "isMut": false, 53 | "isSigner": false 54 | }, 55 | { 56 | "name": "program", 57 | "isMut": false, 58 | "isSigner": false 59 | } 60 | ], 61 | "args": [ 62 | { 63 | "name": "feeRecipient", 64 | "type": "publicKey" 65 | }, 66 | { 67 | "name": "initialVirtualTokenReserves", 68 | "type": "u64" 69 | }, 70 | { 71 | "name": "initialVirtualSolReserves", 72 | "type": "u64" 73 | }, 74 | { 75 | "name": "initialRealTokenReserves", 76 | "type": "u64" 77 | }, 78 | { 79 | "name": "tokenTotalSupply", 80 | "type": "u64" 81 | }, 82 | { 83 | "name": "feeBasisPoints", 84 | "type": "u64" 85 | } 86 | ] 87 | }, 88 | { 89 | "name": "create", 90 | "docs": [ 91 | "Creates a new coin and bonding curve." 92 | ], 93 | "accounts": [ 94 | { 95 | "name": "mint", 96 | "isMut": true, 97 | "isSigner": true 98 | }, 99 | { 100 | "name": "mintAuthority", 101 | "isMut": false, 102 | "isSigner": false 103 | }, 104 | { 105 | "name": "bondingCurve", 106 | "isMut": true, 107 | "isSigner": false 108 | }, 109 | { 110 | "name": "associatedBondingCurve", 111 | "isMut": true, 112 | "isSigner": false 113 | }, 114 | { 115 | "name": "global", 116 | "isMut": false, 117 | "isSigner": false 118 | }, 119 | { 120 | "name": "mplTokenMetadata", 121 | "isMut": false, 122 | "isSigner": false 123 | }, 124 | { 125 | "name": "metadata", 126 | "isMut": true, 127 | "isSigner": false 128 | }, 129 | { 130 | "name": "user", 131 | "isMut": true, 132 | "isSigner": true 133 | }, 134 | { 135 | "name": "systemProgram", 136 | "isMut": false, 137 | "isSigner": false 138 | }, 139 | { 140 | "name": "tokenProgram", 141 | "isMut": false, 142 | "isSigner": false 143 | }, 144 | { 145 | "name": "associatedTokenProgram", 146 | "isMut": false, 147 | "isSigner": false 148 | }, 149 | { 150 | "name": "rent", 151 | "isMut": false, 152 | "isSigner": false 153 | }, 154 | { 155 | "name": "eventAuthority", 156 | "isMut": false, 157 | "isSigner": false 158 | }, 159 | { 160 | "name": "program", 161 | "isMut": false, 162 | "isSigner": false 163 | } 164 | ], 165 | "args": [ 166 | { 167 | "name": "name", 168 | "type": "string" 169 | }, 170 | { 171 | "name": "symbol", 172 | "type": "string" 173 | }, 174 | { 175 | "name": "uri", 176 | "type": "string" 177 | } 178 | ] 179 | }, 180 | { 181 | "name": "buy", 182 | "docs": [ 183 | "Buys tokens from a bonding curve." 184 | ], 185 | "accounts": [ 186 | { 187 | "name": "global", 188 | "isMut": false, 189 | "isSigner": false 190 | }, 191 | { 192 | "name": "feeRecipient", 193 | "isMut": true, 194 | "isSigner": false 195 | }, 196 | { 197 | "name": "mint", 198 | "isMut": false, 199 | "isSigner": false 200 | }, 201 | { 202 | "name": "bondingCurve", 203 | "isMut": true, 204 | "isSigner": false 205 | }, 206 | { 207 | "name": "associatedBondingCurve", 208 | "isMut": true, 209 | "isSigner": false 210 | }, 211 | { 212 | "name": "associatedUser", 213 | "isMut": true, 214 | "isSigner": false 215 | }, 216 | { 217 | "name": "user", 218 | "isMut": true, 219 | "isSigner": true 220 | }, 221 | { 222 | "name": "systemProgram", 223 | "isMut": false, 224 | "isSigner": false 225 | }, 226 | { 227 | "name": "tokenProgram", 228 | "isMut": false, 229 | "isSigner": false 230 | }, 231 | { 232 | "name": "rent", 233 | "isMut": false, 234 | "isSigner": false 235 | }, 236 | { 237 | "name": "eventAuthority", 238 | "isMut": false, 239 | "isSigner": false 240 | }, 241 | { 242 | "name": "program", 243 | "isMut": false, 244 | "isSigner": false 245 | } 246 | ], 247 | "args": [ 248 | { 249 | "name": "amount", 250 | "type": "u64" 251 | }, 252 | { 253 | "name": "maxSolCost", 254 | "type": "u64" 255 | } 256 | ] 257 | }, 258 | { 259 | "name": "sell", 260 | "docs": [ 261 | "Sells tokens into a bonding curve." 262 | ], 263 | "accounts": [ 264 | { 265 | "name": "global", 266 | "isMut": false, 267 | "isSigner": false 268 | }, 269 | { 270 | "name": "feeRecipient", 271 | "isMut": true, 272 | "isSigner": false 273 | }, 274 | { 275 | "name": "mint", 276 | "isMut": false, 277 | "isSigner": false 278 | }, 279 | { 280 | "name": "bondingCurve", 281 | "isMut": true, 282 | "isSigner": false 283 | }, 284 | { 285 | "name": "associatedBondingCurve", 286 | "isMut": true, 287 | "isSigner": false 288 | }, 289 | { 290 | "name": "associatedUser", 291 | "isMut": true, 292 | "isSigner": false 293 | }, 294 | { 295 | "name": "user", 296 | "isMut": true, 297 | "isSigner": true 298 | }, 299 | { 300 | "name": "systemProgram", 301 | "isMut": false, 302 | "isSigner": false 303 | }, 304 | { 305 | "name": "associatedTokenProgram", 306 | "isMut": false, 307 | "isSigner": false 308 | }, 309 | { 310 | "name": "tokenProgram", 311 | "isMut": false, 312 | "isSigner": false 313 | }, 314 | { 315 | "name": "eventAuthority", 316 | "isMut": false, 317 | "isSigner": false 318 | }, 319 | { 320 | "name": "program", 321 | "isMut": false, 322 | "isSigner": false 323 | } 324 | ], 325 | "args": [ 326 | { 327 | "name": "amount", 328 | "type": "u64" 329 | }, 330 | { 331 | "name": "minSolOutput", 332 | "type": "u64" 333 | } 334 | ] 335 | }, 336 | { 337 | "name": "withdraw", 338 | "docs": [ 339 | "Allows the admin to withdraw liquidity for a migration once the bonding curve completes" 340 | ], 341 | "accounts": [ 342 | { 343 | "name": "global", 344 | "isMut": false, 345 | "isSigner": false 346 | }, 347 | { 348 | "name": "mint", 349 | "isMut": false, 350 | "isSigner": false 351 | }, 352 | { 353 | "name": "bondingCurve", 354 | "isMut": true, 355 | "isSigner": false 356 | }, 357 | { 358 | "name": "associatedBondingCurve", 359 | "isMut": true, 360 | "isSigner": false 361 | }, 362 | { 363 | "name": "associatedUser", 364 | "isMut": true, 365 | "isSigner": false 366 | }, 367 | { 368 | "name": "user", 369 | "isMut": true, 370 | "isSigner": true 371 | }, 372 | { 373 | "name": "systemProgram", 374 | "isMut": false, 375 | "isSigner": false 376 | }, 377 | { 378 | "name": "tokenProgram", 379 | "isMut": false, 380 | "isSigner": false 381 | }, 382 | { 383 | "name": "rent", 384 | "isMut": false, 385 | "isSigner": false 386 | }, 387 | { 388 | "name": "eventAuthority", 389 | "isMut": false, 390 | "isSigner": false 391 | }, 392 | { 393 | "name": "program", 394 | "isMut": false, 395 | "isSigner": false 396 | } 397 | ], 398 | "args": [] 399 | } 400 | ], 401 | "accounts": [ 402 | { 403 | "name": "Global", 404 | "type": { 405 | "kind": "struct", 406 | "fields": [ 407 | { 408 | "name": "initialized", 409 | "type": "bool" 410 | }, 411 | { 412 | "name": "authority", 413 | "type": "publicKey" 414 | }, 415 | { 416 | "name": "feeRecipient", 417 | "type": "publicKey" 418 | }, 419 | { 420 | "name": "initialVirtualTokenReserves", 421 | "type": "u64" 422 | }, 423 | { 424 | "name": "initialVirtualSolReserves", 425 | "type": "u64" 426 | }, 427 | { 428 | "name": "initialRealTokenReserves", 429 | "type": "u64" 430 | }, 431 | { 432 | "name": "tokenTotalSupply", 433 | "type": "u64" 434 | }, 435 | { 436 | "name": "feeBasisPoints", 437 | "type": "u64" 438 | } 439 | ] 440 | } 441 | }, 442 | { 443 | "name": "BondingCurve", 444 | "type": { 445 | "kind": "struct", 446 | "fields": [ 447 | { 448 | "name": "virtualTokenReserves", 449 | "type": "u64" 450 | }, 451 | { 452 | "name": "virtualSolReserves", 453 | "type": "u64" 454 | }, 455 | { 456 | "name": "realTokenReserves", 457 | "type": "u64" 458 | }, 459 | { 460 | "name": "realSolReserves", 461 | "type": "u64" 462 | }, 463 | { 464 | "name": "tokenTotalSupply", 465 | "type": "u64" 466 | }, 467 | { 468 | "name": "complete", 469 | "type": "bool" 470 | } 471 | ] 472 | } 473 | } 474 | ], 475 | "events": [ 476 | { 477 | "name": "CreateEvent", 478 | "fields": [ 479 | { 480 | "name": "name", 481 | "type": "string", 482 | "index": false 483 | }, 484 | { 485 | "name": "symbol", 486 | "type": "string", 487 | "index": false 488 | }, 489 | { 490 | "name": "uri", 491 | "type": "string", 492 | "index": false 493 | }, 494 | { 495 | "name": "mint", 496 | "type": "publicKey", 497 | "index": false 498 | }, 499 | { 500 | "name": "bondingCurve", 501 | "type": "publicKey", 502 | "index": false 503 | }, 504 | { 505 | "name": "user", 506 | "type": "publicKey", 507 | "index": false 508 | } 509 | ] 510 | }, 511 | { 512 | "name": "TradeEvent", 513 | "fields": [ 514 | { 515 | "name": "mint", 516 | "type": "publicKey", 517 | "index": false 518 | }, 519 | { 520 | "name": "solAmount", 521 | "type": "u64", 522 | "index": false 523 | }, 524 | { 525 | "name": "tokenAmount", 526 | "type": "u64", 527 | "index": false 528 | }, 529 | { 530 | "name": "isBuy", 531 | "type": "bool", 532 | "index": false 533 | }, 534 | { 535 | "name": "user", 536 | "type": "publicKey", 537 | "index": false 538 | }, 539 | { 540 | "name": "timestamp", 541 | "type": "i64", 542 | "index": false 543 | }, 544 | { 545 | "name": "virtualSolReserves", 546 | "type": "u64", 547 | "index": false 548 | }, 549 | { 550 | "name": "virtualTokenReserves", 551 | "type": "u64", 552 | "index": false 553 | } 554 | ] 555 | }, 556 | { 557 | "name": "CompleteEvent", 558 | "fields": [ 559 | { 560 | "name": "user", 561 | "type": "publicKey", 562 | "index": false 563 | }, 564 | { 565 | "name": "mint", 566 | "type": "publicKey", 567 | "index": false 568 | }, 569 | { 570 | "name": "bondingCurve", 571 | "type": "publicKey", 572 | "index": false 573 | }, 574 | { 575 | "name": "timestamp", 576 | "type": "i64", 577 | "index": false 578 | } 579 | ] 580 | }, 581 | { 582 | "name": "SetParamsEvent", 583 | "fields": [ 584 | { 585 | "name": "feeRecipient", 586 | "type": "publicKey", 587 | "index": false 588 | }, 589 | { 590 | "name": "initialVirtualTokenReserves", 591 | "type": "u64", 592 | "index": false 593 | }, 594 | { 595 | "name": "initialVirtualSolReserves", 596 | "type": "u64", 597 | "index": false 598 | }, 599 | { 600 | "name": "initialRealTokenReserves", 601 | "type": "u64", 602 | "index": false 603 | }, 604 | { 605 | "name": "tokenTotalSupply", 606 | "type": "u64", 607 | "index": false 608 | }, 609 | { 610 | "name": "feeBasisPoints", 611 | "type": "u64", 612 | "index": false 613 | } 614 | ] 615 | } 616 | ], 617 | "errors": [ 618 | { 619 | "code": 6000, 620 | "name": "NotAuthorized", 621 | "msg": "The given account is not authorized to execute this instruction." 622 | }, 623 | { 624 | "code": 6001, 625 | "name": "AlreadyInitialized", 626 | "msg": "The program is already initialized." 627 | }, 628 | { 629 | "code": 6002, 630 | "name": "TooMuchSolRequired", 631 | "msg": "slippage: Too much SOL required to buy the given amount of tokens." 632 | }, 633 | { 634 | "code": 6003, 635 | "name": "TooLittleSolReceived", 636 | "msg": "slippage: Too little SOL received to sell the given amount of tokens." 637 | }, 638 | { 639 | "code": 6004, 640 | "name": "MintDoesNotMatchBondingCurve", 641 | "msg": "The mint does not match the bonding curve." 642 | }, 643 | { 644 | "code": 6005, 645 | "name": "BondingCurveComplete", 646 | "msg": "The bonding curve has completed and liquidity migrated to raydium." 647 | }, 648 | { 649 | "code": 6006, 650 | "name": "BondingCurveNotComplete", 651 | "msg": "The bonding curve has not completed." 652 | }, 653 | { 654 | "code": 6007, 655 | "name": "NotInitialized", 656 | "msg": "The program is not initialized." 657 | } 658 | ], 659 | "metadata": { 660 | "address": "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" 661 | } 662 | } -------------------------------------------------------------------------------- /src/app/api/pump-proxy/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | 3 | export const dynamic = 'force-dynamic'; 4 | export const revalidate = 0; 5 | 6 | export async function GET(request: Request) { 7 | const { searchParams } = new URL(request.url); 8 | let mintAddress = searchParams.get('mintAddress'); 9 | 10 | if (!mintAddress) { 11 | return NextResponse.json( 12 | { error: 'Missing or invalid mintAddress' }, 13 | { status: 400 } 14 | ); 15 | } 16 | 17 | // Remove 'pump' suffix if it exists for initial processing 18 | const baseMintAddress = mintAddress.endsWith('pump') 19 | ? mintAddress.slice(0, -4) 20 | : mintAddress; 21 | 22 | console.log('Fetching data for mintAddress:', mintAddress); 23 | 24 | // Function to handle API call with retries and response validation 25 | async function fetchWithRetry(url: string, options: any, retries = 3, delay = 1000) { 26 | let lastError: Error | null = null; 27 | 28 | for (let i = 0; i < retries; i++) { 29 | try { 30 | const response = await fetch(url, options); 31 | const contentType = response.headers.get('content-type'); 32 | 33 | // Check if response is HTML (error page) instead of JSON 34 | if (contentType?.includes('text/html')) { 35 | console.log(`Received HTML response from ${url}`); 36 | throw new Error('Endpoint unavailable'); 37 | } 38 | 39 | // For 404s, return immediately 40 | if (response.status === 404) { 41 | throw new Error('Token not found'); 42 | } 43 | 44 | // For 500s, wait and retry 45 | if (response.status === 500) { 46 | throw new Error('Internal server error'); 47 | } 48 | 49 | if (response.ok) { 50 | const data = await response.json(); 51 | 52 | // Validate the response data 53 | if (!data || (typeof data === 'object' && Object.keys(data).length === 0)) { 54 | throw new Error('Empty or invalid response data'); 55 | } 56 | 57 | return data; 58 | } 59 | 60 | const errorText = await response.text(); 61 | lastError = new Error(`API responded with status: ${response.status} - ${errorText}`); 62 | 63 | // Log detailed error information 64 | console.error(`API attempt ${i + 1} failed:`, { 65 | url, 66 | status: response.status, 67 | statusText: response.statusText, 68 | contentType, 69 | body: errorText.substring(0, 200), // Limit error text length 70 | timestamp: new Date().toISOString() 71 | }); 72 | 73 | // If not a 500 error and last retry, throw immediately 74 | if (response.status !== 500 && i === retries - 1) { 75 | throw lastError; 76 | } 77 | 78 | // Exponential backoff 79 | await new Promise(resolve => setTimeout(resolve, delay * Math.pow(2, i))); 80 | } catch (error) { 81 | if (error instanceof Error) { 82 | lastError = error; 83 | 84 | // If it's a 404 or HTML response, don't retry 85 | if (error.message === 'Token not found' || error.message === 'Endpoint unavailable') { 86 | throw error; 87 | } 88 | } else { 89 | lastError = new Error('Unknown error occurred'); 90 | } 91 | 92 | if (i === retries - 1) throw lastError; 93 | await new Promise(resolve => setTimeout(resolve, delay * Math.pow(2, i))); 94 | } 95 | } 96 | throw lastError || new Error('All retry attempts failed'); 97 | } 98 | 99 | const headers = { 100 | 'Accept': 'application/json', 101 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36', 102 | 'Origin': 'https://pump.fun', 103 | 'Referer': 'https://pump.fun/', 104 | }; 105 | 106 | // Try both versions of the mint address (with and without 'pump' suffix) 107 | try { 108 | // First try with the original mint address 109 | try { 110 | const data = await fetchWithRetry(`https://frontend-api.pump.fun/coins/${mintAddress}`, { 111 | headers, 112 | cache: 'no-store' 113 | }); 114 | return NextResponse.json(data); 115 | } catch (error) { 116 | // Type guard for Error instance 117 | if (!(error instanceof Error)) { 118 | throw new Error('Unknown error occurred'); 119 | } 120 | 121 | // If the error is not "Token not found", throw it 122 | if (error.message !== 'Token not found') { 123 | throw error; 124 | } 125 | 126 | // If the original address didn't work and it's different from the base address, 127 | // try the alternate version 128 | if (mintAddress !== baseMintAddress) { 129 | console.log('Trying base mint address:', baseMintAddress); 130 | const data = await fetchWithRetry(`https://frontend-api.pump.fun/coins/${baseMintAddress}`, { 131 | headers, 132 | cache: 'no-store' 133 | }); 134 | return NextResponse.json(data); 135 | } 136 | 137 | // If we have the base address and adding 'pump' might help, try that 138 | if (mintAddress === baseMintAddress) { 139 | const pumpAddress = `${baseMintAddress}pump`; 140 | console.log('Trying with pump suffix:', pumpAddress); 141 | const data = await fetchWithRetry(`https://frontend-api.pump.fun/coins/${pumpAddress}`, { 142 | headers, 143 | cache: 'no-store' 144 | }); 145 | return NextResponse.json(data); 146 | } 147 | 148 | // If we get here, neither version worked 149 | throw error; 150 | } 151 | } catch (error) { 152 | console.error('Error in pump-proxy:', error); 153 | 154 | // Type guard for Error instance 155 | if (!(error instanceof Error)) { 156 | return NextResponse.json( 157 | { 158 | error: 'Failed to fetch token data', 159 | details: 'Unknown error occurred', 160 | mintAddress, 161 | timestamp: new Date().toISOString() 162 | }, 163 | { status: 500 } 164 | ); 165 | } 166 | 167 | // Handle specific error cases 168 | if (error.message === 'Token not found') { 169 | return NextResponse.json( 170 | { 171 | error: 'Token not found', 172 | mintAddress, 173 | triedAddresses: [mintAddress, mintAddress !== baseMintAddress ? baseMintAddress : `${baseMintAddress}pump`], 174 | timestamp: new Date().toISOString() 175 | }, 176 | { status: 404 } 177 | ); 178 | } 179 | 180 | if (error.message === 'Endpoint unavailable') { 181 | return NextResponse.json( 182 | { 183 | error: 'Service temporarily unavailable', 184 | details: 'API endpoint is currently unavailable', 185 | mintAddress, 186 | timestamp: new Date().toISOString() 187 | }, 188 | { status: 503 } 189 | ); 190 | } 191 | 192 | // Generic error response 193 | return NextResponse.json( 194 | { 195 | error: 'Failed to fetch token data', 196 | details: error.message, 197 | mintAddress, 198 | timestamp: new Date().toISOString() 199 | }, 200 | { status: 500 } 201 | ); 202 | } 203 | } 204 | 205 | export async function OPTIONS() { 206 | return new NextResponse(null, { 207 | status: 204, 208 | headers: { 209 | 'Access-Control-Allow-Origin': '*', 210 | 'Access-Control-Allow-Methods': 'GET, OPTIONS', 211 | 'Access-Control-Allow-Headers': 'Content-Type, Authorization', 212 | }, 213 | }); 214 | } 215 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import { Metadata } from 'next'; 3 | import '../styles/globals.css'; 4 | import { Inter } from 'next/font/google'; 5 | import Providers from './providers'; 6 | import { Analytics } from "@vercel/analytics/react" 7 | 8 | const inter = Inter({ subsets: ['latin'] }); 9 | 10 | export const metadata: Metadata = { 11 | title: 'MemeSniper.Fun', 12 | description: 'Solana Social Based Token Sniper', 13 | icons: { 14 | icon: [ 15 | { url: '/favicon.svg', type: 'image/svg+xml' }, 16 | ], 17 | shortcut: ['/favicon.svg'], 18 | }, 19 | }; 20 | 21 | export default function RootLayout({ 22 | children, 23 | }: { 24 | children: ReactNode; 25 | }) { 26 | return ( 27 | 28 | 29 | 30 | {children} 31 | 32 | 33 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/app/metadata.ts: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | 3 | export const metadata: Metadata = { 4 | title: 'memesniper.fun', 5 | description: 'Solana Token Trading Bot - Real-time token monitoring and auto-buy capabilities', 6 | icons: { 7 | icon: [{ url: '🔫', type: 'image/svg+xml' }], 8 | }, 9 | openGraph: { 10 | title: 'MemeSniper.fun', 11 | description: 'Solana Token Trading Bot - Real-time token monitoring and auto-buy capabilities', 12 | url: 'https://memesniper.fun', 13 | siteName: 'memesniper.fun', 14 | images: [ 15 | { 16 | url: 'https://memesniper.fun/social-share.svg', 17 | width: 1200, 18 | height: 630, 19 | alt: 'memesniper.fun - Solana Token Trading Bot', 20 | }, 21 | ], 22 | locale: 'en_US', 23 | type: 'website', 24 | }, 25 | twitter: { 26 | card: 'summary_large_image', 27 | title: 'memesniper.fun', 28 | description: 'Solana Token Trading Bot - Real-time token monitoring and auto-buy capabilities', 29 | creator: '@SocialSnipeSol', 30 | images: ['https://memesniper.fun/social-share.svg'], 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import dynamic from 'next/dynamic'; 4 | import { TradingProvider } from '../contexts/TradingContext'; 5 | import Header from '@/components/Header'; 6 | import Footer from '@/components/Footer'; 7 | import TradingSettings from '@/components/TradingSettings'; 8 | import TwitterFeed from '@/components/TwitterFeed'; 9 | import { useState, useEffect } from 'react'; 10 | import { Keypair, Connection } from '@solana/web3.js'; 11 | import { useWalletContext } from '../contexts/WalletContext'; 12 | import { useTradingContext } from '../contexts/TradingContext'; 13 | import { PumpFunClient } from '../pumpFunClient'; 14 | import bs58 from 'bs58'; 15 | 16 | export default function Home() { 17 | const [isMobile, setIsMobile] = useState(false); 18 | const [pumpFunClient, setPumpFunClient] = useState(null); 19 | const { privateKey } = useWalletContext(); 20 | const tradingSettings = useTradingContext(); 21 | 22 | // Handle window resize 23 | useEffect(() => { 24 | const handleResize = () => { 25 | setIsMobile(window.innerWidth < 1024); // 1024px is the lg breakpoint 26 | }; 27 | 28 | // Set initial value 29 | handleResize(); 30 | 31 | // Add event listener 32 | window.addEventListener('resize', handleResize); 33 | 34 | // Cleanup 35 | return () => window.removeEventListener('resize', handleResize); 36 | }, []); 37 | 38 | useEffect(() => { 39 | if (!privateKey) return; 40 | 41 | try { 42 | const keyPair = Keypair.fromSecretKey(bs58.decode(privateKey)); 43 | const connection = new Connection(process.env.NEXT_PUBLIC_HELIUS_RPC_URL, 'confirmed'); 44 | const client = new PumpFunClient(connection, keyPair, process.env.NEXT_PUBLIC_HELIUS_RPC_URL, tradingSettings); 45 | setPumpFunClient(client); 46 | } catch (error) { 47 | console.error('Error initializing PumpFunClient:', error); 48 | } 49 | }, [privateKey, tradingSettings]); 50 | 51 | return ( 52 | 53 |
54 |
55 | 56 |
57 | {/* Mobile View */} 58 |
59 |
60 | 61 |
62 |
63 | 64 |
65 |
66 | 67 | {/* Desktop View - Preserved exactly as is */} 68 |
69 | {/* Twitter Feed - Larger emphasis */} 70 |
71 |
72 | 73 |
74 |
75 | 76 | {/* Trading Settings - Compact version */} 77 |
78 |
79 | 80 |
81 |
82 |
83 |
84 | 85 |
86 |
87 |
88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /src/app/providers.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ReactNode, useEffect, useState } from 'react'; 4 | import { TradingProvider } from '../contexts/TradingContext'; 5 | import { BlacklistProvider } from '../contexts/BlacklistContext'; 6 | import { BuylistProvider } from '../contexts/BuylistContext'; 7 | import { WalletProvider } from '../contexts/WalletContext'; 8 | import { Toaster } from 'react-hot-toast'; 9 | 10 | export default function Providers({ 11 | children, 12 | }: { 13 | children: ReactNode; 14 | }) { 15 | const [mounted, setMounted] = useState(false); 16 | 17 | useEffect(() => { 18 | const hasVisited = localStorage.getItem('hasVisited'); 19 | if (!hasVisited && typeof window !== 'undefined') { 20 | localStorage.setItem('hasVisited', 'true'); 21 | window.location.reload(); 22 | return; 23 | } 24 | 25 | setMounted(true); 26 | return () => { 27 | setMounted(false); 28 | }; 29 | }, []); 30 | 31 | if (!mounted) { 32 | return ( 33 |
34 |
Loading...
35 |
36 | ); 37 | } 38 | 39 | return ( 40 | 41 | 42 | 43 | 44 | {children} 45 | 46 | 47 | 48 | 49 | 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /src/app/template.tsx: -------------------------------------------------------------------------------- 1 | export const metadata = { 2 | title: 'memesniper.fun', 3 | description: 'Automated Solana token trading platform', 4 | }; 5 | 6 | export default function Template({ children }: { children: React.ReactNode }) { 7 | return children; 8 | } 9 | -------------------------------------------------------------------------------- /src/coinData.ts: -------------------------------------------------------------------------------- 1 | import { Connection, PublicKey } from '@solana/web3.js'; 2 | import { getAssociatedTokenAddress } from '@solana/spl-token'; 3 | import { Buffer } from 'buffer'; 4 | import { PUMP_FUN_PROGRAM } from './constants'; 5 | import { VirtualReserves, CoinData } from './types'; 6 | 7 | /** 8 | * Safely converts a bigint to a number with validation 9 | * @param value The bigint value to convert 10 | * @param fieldName The name of the field for error reporting 11 | * @returns The converted number value 12 | * @throws Error if the value exceeds safe integer limits 13 | */ 14 | function safeConvertBigIntToNumber(value: bigint, fieldName: string): number { 15 | if (value > BigInt(Number.MAX_SAFE_INTEGER)) { 16 | throw new Error(`${fieldName} exceeds maximum safe integer value`); 17 | } 18 | return Number(value); 19 | } 20 | 21 | /** 22 | * Retrieves and parses the virtual reserves data for a bonding curve account 23 | * @param connection The Solana connection instance 24 | * @param bondingCurve The public key of the bonding curve account 25 | * @returns The parsed virtual reserves data or null if not found 26 | */ 27 | export async function getVirtualReserves( 28 | connection: Connection, 29 | bondingCurve: PublicKey 30 | ): Promise { 31 | try { 32 | const accountInfo = await connection.getAccountInfo(bondingCurve); 33 | if (!accountInfo?.data || accountInfo.data.length < 41) { // 8 (discriminator) + 32 (reserves) + 1 (complete flag) 34 | console.error('Invalid account data length for bonding curve'); 35 | return null; 36 | } 37 | 38 | // Skip first 8 bytes (discriminator) 39 | const dataBuffer = accountInfo.data.slice(8); 40 | 41 | // Parse the data using DataView for consistent byte reading 42 | const view = new DataView(dataBuffer.buffer, dataBuffer.byteOffset, dataBuffer.byteLength); 43 | 44 | // Validate buffer has enough bytes 45 | if (view.byteLength < 41) { 46 | throw new Error('Insufficient data in bonding curve account'); 47 | } 48 | 49 | const virtualReserves: VirtualReserves = { 50 | virtualTokenReserves: view.getBigUint64(0, true), // true for little-endian 51 | virtualSolReserves: view.getBigUint64(8, true), 52 | realTokenReserves: view.getBigUint64(16, true), 53 | realSolReserves: view.getBigUint64(24, true), 54 | tokenTotalSupply: view.getBigUint64(32, true), 55 | complete: Boolean(view.getUint8(40)) // Flag is 1 byte 56 | }; 57 | 58 | // Validate reserves are non-negative 59 | if (virtualReserves.virtualTokenReserves < BigInt(0) || 60 | virtualReserves.virtualSolReserves < BigInt(0) || 61 | virtualReserves.realTokenReserves < BigInt(0) || 62 | virtualReserves.realSolReserves < BigInt(0) || 63 | virtualReserves.tokenTotalSupply < BigInt(0)) { 64 | throw new Error('Invalid negative reserve values detected'); 65 | } 66 | 67 | return virtualReserves; 68 | } catch (error) { 69 | console.error('Error getting virtual reserves:', error); 70 | if (error instanceof Error) { 71 | console.error('Error details:', error.message); 72 | } 73 | return null; 74 | } 75 | } 76 | 77 | /** 78 | * Derives the bonding curve and associated token accounts for a given mint 79 | * @param mint The mint address as a string 80 | * @returns A tuple of [bondingCurve, associatedBondingCurve] public keys or [null, null] if derivation fails 81 | */ 82 | export async function deriveBondingCurveAccounts( 83 | mint: string 84 | ): Promise<[PublicKey, PublicKey] | [null, null]> { 85 | try { 86 | if (!PublicKey.isOnCurve(new PublicKey(mint))) { 87 | throw new Error('Invalid mint address provided'); 88 | } 89 | 90 | const mintPubkey = new PublicKey(mint); 91 | const seeds = [ 92 | Buffer.from('bonding-curve'), 93 | mintPubkey.toBuffer() 94 | ]; 95 | 96 | const [bondingCurve] = PublicKey.findProgramAddressSync( 97 | seeds, 98 | PUMP_FUN_PROGRAM 99 | ); 100 | 101 | const associatedBondingCurve = await getAssociatedTokenAddress( 102 | mintPubkey, 103 | bondingCurve, 104 | true // allowOwnerOffCurve set to true 105 | ); 106 | 107 | return [bondingCurve, associatedBondingCurve]; 108 | } catch (error) { 109 | console.error('Error deriving bonding curve accounts:', error); 110 | if (error instanceof Error) { 111 | console.error('Error details:', error.message); 112 | } 113 | return [null, null]; 114 | } 115 | } 116 | 117 | // Cache structure 118 | interface CoinDataCache { 119 | data: CoinData; 120 | timestamp: number; 121 | } 122 | 123 | const coinDataCache = new Map(); 124 | const CACHE_DURATION = 10000; // 10 seconds cache 125 | 126 | /** 127 | * Retrieves comprehensive coin data for a given mint address 128 | * @param connection The Solana connection instance 129 | * @param mintStr The mint address as a string 130 | * @returns The coin data or null if retrieval fails 131 | */ 132 | export async function getCoinData( 133 | connection: Connection, 134 | mintStr: string 135 | ): Promise { 136 | try { 137 | // Check cache first 138 | const now = Date.now(); 139 | const cached = coinDataCache.get(mintStr); 140 | if (cached && now - cached.timestamp < CACHE_DURATION) { 141 | return cached.data; 142 | } 143 | 144 | const [bondingCurve, associatedBondingCurve] = await deriveBondingCurveAccounts(mintStr); 145 | if (!bondingCurve || !associatedBondingCurve) { 146 | throw new Error('Failed to derive bonding curve accounts'); 147 | } 148 | 149 | const virtualReserves = await getVirtualReserves(connection, bondingCurve); 150 | if (!virtualReserves) { 151 | throw new Error('Failed to fetch virtual reserves'); 152 | } 153 | 154 | const coinData = { 155 | mint: mintStr, 156 | bondingCurve: bondingCurve.toString(), 157 | associatedBondingCurve: associatedBondingCurve.toString(), 158 | virtualTokenReserves: safeConvertBigIntToNumber(virtualReserves.virtualTokenReserves, 'virtualTokenReserves'), 159 | virtualSolReserves: safeConvertBigIntToNumber(virtualReserves.virtualSolReserves, 'virtualSolReserves'), 160 | tokenTotalSupply: safeConvertBigIntToNumber(virtualReserves.tokenTotalSupply, 'tokenTotalSupply'), 161 | complete: virtualReserves.complete 162 | }; 163 | 164 | // Update cache 165 | coinDataCache.set(mintStr, { 166 | data: coinData, 167 | timestamp: now 168 | }); 169 | 170 | return coinData; 171 | } catch (error) { 172 | console.error('Error processing coin data:', error); 173 | if (error instanceof Error) { 174 | console.error('Error details:', error.message); 175 | } 176 | return null; 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/components/BlacklistManager.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useBlacklistContext } from '../contexts/BlacklistContext'; 4 | import { useState } from 'react'; 5 | 6 | export default function BlacklistManager() { 7 | const { blacklistedUsers, removeFromBlacklist } = useBlacklistContext(); 8 | const [filter, setFilter] = useState(''); 9 | 10 | const filteredUsers = blacklistedUsers.filter(user => 11 | user.toLowerCase().includes(filter.toLowerCase()) 12 | ); 13 | 14 | return ( 15 |
16 |
17 | setFilter(e.target.value)} 22 | className="flex-1 px-3 py-2 bg-gray-800 rounded-lg border border-gray-700 focus:ring-2 focus:ring-yellow-500/50 focus:border-yellow-500 text-white text-sm" 23 | /> 24 |
25 | 26 |
27 | {filteredUsers.length === 0 ? ( 28 |
29 | {filter ? 'No matching users found' : 'No blacklisted users'} 30 |
31 | ) : ( 32 | filteredUsers.map(username => ( 33 |
37 | @{username} 38 | 44 |
45 | )) 46 | )} 47 |
48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function Footer() { 4 | return ( 5 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Image from 'next/image'; 3 | 4 | export default function Header() { 5 | return ( 6 |
7 |
8 | {/* Network Status Bar */} 9 |
10 |
11 |
12 |
13 | Solana 14 |
15 |
16 |
17 | Twitter API 18 |
19 |
20 |
21 | 22 | {/* Main Header */} 23 |
24 |
25 |
26 |
27 | memesniper.fun 28 | 29 | Beta 30 | 31 |
32 |
33 |
34 |
35 |
36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/components/OrderStatus.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | import { useTradingContext } from '../contexts/TradingContext'; 5 | import { formatDistanceToNow } from 'date-fns'; 6 | 7 | const StatusIcon = ({ status }: { status: string }) => { 8 | if (status === 'pending') { 9 | return ( 10 | 11 | 12 | 13 | 14 | ); 15 | } 16 | if (status === 'success') { 17 | return ( 18 | 19 | 20 | 21 | ); 22 | } 23 | return ( 24 | 25 | 26 | 27 | ); 28 | }; 29 | 30 | export default function OrderStatus() { 31 | const { orders } = useTradingContext(); 32 | 33 | // Filter out removed orders 34 | const activeOrders = orders.filter(order => order.status !== 'removed'); 35 | 36 | if (activeOrders.length === 0) { 37 | return ( 38 |
39 |
40 |
41 | 42 | 43 | 44 |
45 |

No Recent Orders

46 |

Your trading activity will appear here

47 |
48 |
49 | ); 50 | } 51 | 52 | return ( 53 |
54 |
55 |
56 |

Recent Orders

57 | 58 | {activeOrders.length} {activeOrders.length === 1 ? 'Order' : 'Orders'} 59 | 60 |
61 |
62 | 63 |
64 | {activeOrders.map((order) => ( 65 |
73 |
74 |
75 |
76 | 77 | 78 | {order.tokenSymbol} - {order.tokenName} 79 | 80 |
81 |
82 | 85 | {order.type.toUpperCase()} 86 | 87 | {order.amount} SOL 88 |
89 |
90 | 91 |
92 |
97 | {order.status.charAt(0).toUpperCase() + order.status.slice(1)} 98 |
99 |
100 | {formatDistanceToNow(order.timestamp, { addSuffix: true })} 101 |
102 |
103 |
104 | 105 | {(order.signature || order.error) && ( 106 |
107 | {order.signature && ( 108 | 114 | 115 | 116 | 117 | View on Solscan 118 | 119 | )} 120 | {order.error && ( 121 |
122 | 123 | 124 | 125 | {order.error} 126 |
127 | )} 128 |
129 | )} 130 |
131 | ))} 132 |
133 |
134 | ); 135 | } 136 | -------------------------------------------------------------------------------- /src/components/PurchasedTokens.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useEffect, useState, useRef, useCallback } from 'react'; 4 | import { useTradingContext } from '../contexts/TradingContext'; 5 | import bs58 from 'bs58'; 6 | import { Connection, PublicKey, LAMPORTS_PER_SOL, Keypair } from '@solana/web3.js'; 7 | import { PumpFunClient } from '../pumpFunClient'; 8 | 9 | // Extend Window interface for triggerTokenUpdate 10 | declare global { 11 | interface Window { 12 | triggerTokenUpdate?: () => void; 13 | } 14 | } 15 | 16 | interface TokenHolding { 17 | mint: string; 18 | name: string; 19 | symbol: string; 20 | amount: number; 21 | decimals: number; 22 | pricePerToken?: number; 23 | totalValue?: number; 24 | isLoading?: boolean; 25 | sellAmount: number; // Changed to non-optional 26 | error?: string; 27 | } 28 | 29 | export default function PurchasedTokens() { 30 | const { privateKey, slippage } = useTradingContext(); 31 | const [holdings, setHoldings] = useState([]); 32 | const [solBalance, setSolBalance] = useState(0); 33 | const [loading, setLoading] = useState(false); 34 | const [error, setError] = useState(null); 35 | const [lastPurchaseTime, setLastPurchaseTime] = useState(null); 36 | const [pumpFunClient, setPumpFunClient] = useState(null); 37 | 38 | // Add refs for background updates 39 | const isUpdating = useRef(false); 40 | const needsUpdate = useRef(false); 41 | const mountedRef = useRef(true); 42 | const lastUpdateTime = useRef(0); 43 | 44 | const shouldUpdate = useCallback(() => { 45 | const now = Date.now(); 46 | const timeSinceLastUpdate = now - lastUpdateTime.current; 47 | return timeSinceLastUpdate >= 60000; // 1 minute in milliseconds 48 | }, []); 49 | 50 | const fetchSolBalance = useCallback(async (publicKey: string) => { 51 | try { 52 | const connection = new Connection(process.env.NEXT_PUBLIC_HELIUS_RPC_URL); 53 | const balance = await connection.getBalance(new PublicKey(publicKey)); 54 | if (mountedRef.current) { 55 | setSolBalance(balance / LAMPORTS_PER_SOL); 56 | } 57 | } catch (err) { 58 | console.error('Error fetching SOL balance:', err); 59 | if (mountedRef.current) { 60 | setError('Failed to fetch SOL balance'); 61 | } 62 | } 63 | }, []); 64 | 65 | const fetchTokenHoldings = useCallback(async (showLoading = false, force = false) => { 66 | if (!privateKey || isUpdating.current) { 67 | needsUpdate.current = true; 68 | return; 69 | } 70 | 71 | if (!force && !shouldUpdate()) { 72 | return; 73 | } 74 | 75 | try { 76 | isUpdating.current = true; 77 | if (showLoading) setLoading(true); 78 | if (mountedRef.current) setError(null); 79 | 80 | const decodedKey = bs58.decode(privateKey); 81 | const publicKey = bs58.encode(decodedKey.slice(32)); 82 | 83 | // Fetch SOL balance 84 | await fetchSolBalance(publicKey); 85 | 86 | const response = await fetch(process.env.NEXT_PUBLIC_HELIUS_RPC_URL, { 87 | method: 'POST', 88 | headers: { 89 | 'Content-Type': 'application/json', 90 | }, 91 | body: JSON.stringify({ 92 | jsonrpc: '2.0', 93 | id: 'helius-test', 94 | method: 'searchAssets', 95 | params: { 96 | ownerAddress: publicKey, 97 | tokenType: "fungible" 98 | } 99 | }), 100 | }); 101 | 102 | if (!response.ok) { 103 | throw new Error(`HTTP error! status: ${response.status}`); 104 | } 105 | 106 | const data = await response.json(); 107 | if (data.error) { 108 | throw new Error(data.error.message); 109 | } 110 | 111 | const pumpTokens = data.result.items 112 | .filter((asset: any) => asset.id.toLowerCase().endsWith('pump')) 113 | .map((asset: any) => ({ 114 | mint: asset.id, 115 | name: asset.content?.metadata?.name || 'Unknown Token', 116 | symbol: asset.content?.metadata?.symbol || '???', 117 | amount: asset.token_info?.balance / Math.pow(10, asset.token_info?.decimals || 0), 118 | decimals: asset.token_info?.decimals || 0, 119 | pricePerToken: asset.token_info?.price_info?.price_per_token, 120 | totalValue: asset.token_info?.price_info?.total_price, 121 | sellAmount: (holdings.find(h => h.mint === asset.id)?.sellAmount || 0) 122 | })); 123 | 124 | if (mountedRef.current) { 125 | setHoldings(pumpTokens); 126 | lastUpdateTime.current = Date.now(); 127 | } 128 | } catch (err) { 129 | console.error('Error fetching token holdings:', err); 130 | if (mountedRef.current) { 131 | setError(err instanceof Error ? err.message : 'Failed to fetch token holdings'); 132 | } 133 | } finally { 134 | isUpdating.current = false; 135 | if (showLoading && mountedRef.current) setLoading(false); 136 | } 137 | }, [privateKey, fetchSolBalance]); 138 | 139 | const fetchTokenPrices = useCallback(async () => { 140 | if (!holdings.length) return; 141 | 142 | try { 143 | const response = await fetch(process.env.NEXT_PUBLIC_HELIUS_RPC_URL, { 144 | method: 'POST', 145 | headers: { 146 | 'Content-Type': 'application/json', 147 | }, 148 | body: JSON.stringify({ 149 | jsonrpc: '2.0', 150 | id: 'helius-prices', 151 | method: 'searchAssets', 152 | params: { 153 | ownerAddress: null, 154 | tokenType: "fungible", 155 | grouping: ["mint"], 156 | compressed: true, 157 | page: 1, 158 | limit: 1000, 159 | displayOptions: { 160 | showCollectionMetadata: true, 161 | showUnverifiedCollections: true, 162 | showZeroBalance: true, 163 | showNativeBalance: true, 164 | showInscription: true 165 | } 166 | } 167 | }), 168 | }); 169 | 170 | if (!response.ok) { 171 | throw new Error(`HTTP error! status: ${response.status}`); 172 | } 173 | 174 | const data = await response.json(); 175 | if (data.error) { 176 | throw new Error(data.error.message); 177 | } 178 | 179 | const updatedHoldings = holdings.map(token => { 180 | const assetInfo = data.result.items.find((item: any) => 181 | item.id === token.mint 182 | ); 183 | 184 | return { 185 | ...token, 186 | pricePerToken: assetInfo?.token_info?.price_info?.price_per_token || token.pricePerToken, 187 | totalValue: assetInfo?.token_info?.price_info?.total_price || token.totalValue 188 | }; 189 | }); 190 | 191 | if (mountedRef.current) { 192 | setHoldings(updatedHoldings); 193 | } 194 | } catch (err) { 195 | console.error('Error fetching token prices:', err); 196 | } 197 | }, [holdings]); 198 | 199 | useEffect(() => { 200 | if (!privateKey) return; 201 | 202 | mountedRef.current = true; 203 | 204 | // Initial fetch with loading indicator 205 | fetchTokenHoldings(true, true); 206 | 207 | // Set up polling every minute 208 | const intervalId = setInterval(() => { 209 | fetchTokenHoldings(false, true); 210 | }, 60000); 211 | 212 | return () => { 213 | mountedRef.current = false; 214 | clearInterval(intervalId); 215 | }; 216 | }, [privateKey, fetchTokenHoldings]); 217 | 218 | useEffect(() => { 219 | const timeoutId = setTimeout(() => { 220 | if (holdings.length > 0) { 221 | fetchTokenPrices(); 222 | } 223 | }, 1000); // Delay price fetch by 1 second 224 | 225 | return () => clearTimeout(timeoutId); 226 | }, [holdings.length]); // Only depend on holdings.length, not the entire holdings array 227 | 228 | useEffect(() => { 229 | if (typeof window !== 'undefined') { 230 | window.triggerTokenUpdate = () => { 231 | fetchTokenHoldings(false, true); 232 | }; 233 | } 234 | return () => { 235 | if (typeof window !== 'undefined') { 236 | window.triggerTokenUpdate = undefined; 237 | } 238 | }; 239 | }, [fetchTokenHoldings]); 240 | 241 | const onTokenPurchase = useCallback(() => { 242 | setLastPurchaseTime(Date.now()); 243 | needsUpdate.current = true; 244 | fetchTokenHoldings(false, true); // Force update on purchase 245 | }, [fetchTokenHoldings]); 246 | 247 | const handleSellAmountChange = (mint: string, amount: number) => { 248 | setHoldings(prev => prev.map(token => { 249 | if (token.mint !== mint) return token; 250 | 251 | // Validate the amount 252 | if (amount < 0) amount = 0; 253 | if (amount > token.amount) amount = token.amount; 254 | 255 | return { 256 | ...token, 257 | sellAmount: amount, 258 | error: undefined // Clear any previous error 259 | }; 260 | })); 261 | }; 262 | 263 | const handleSellToken = async (mint: string) => { 264 | if (!privateKey || !pumpFunClient) return; 265 | 266 | const token = holdings.find(t => t.mint === mint); 267 | if (!token) return; 268 | 269 | // Get the sell amount 270 | const sellAmount = token.sellAmount; 271 | if (sellAmount <= 0 || sellAmount > token.amount) { 272 | setHoldings(prev => prev.map(t => 273 | t.mint === mint ? { ...t, error: `Invalid sell amount. Must be between 0 and ${token.amount}` } : t 274 | )); 275 | return; 276 | } 277 | 278 | // Calculate percentage of total balance 279 | const percentage = (sellAmount / token.amount) * 100; 280 | 281 | setHoldings(prev => prev.map(t => 282 | t.mint === mint ? { ...t, isLoading: true, error: undefined } : t 283 | )); 284 | 285 | try { 286 | const signature = await pumpFunClient.sell(mint, percentage, slippage); 287 | if (signature) { 288 | console.log('Sell successful:', signature); 289 | // Reset sell amount after successful sale 290 | handleSellAmountChange(mint, 0); 291 | // Refresh holdings after successful sale 292 | await fetchTokenHoldings(); 293 | } 294 | } catch (err) { 295 | console.error('Error selling token:', err); 296 | const errorMessage = err instanceof Error ? err.message : 'Failed to sell token'; 297 | setHoldings(prev => prev.map(t => 298 | t.mint === mint ? { ...t, error: errorMessage } : t 299 | )); 300 | } finally { 301 | setHoldings(prev => prev.map(t => 302 | t.mint === mint ? { ...t, isLoading: false } : t 303 | )); 304 | } 305 | }; 306 | 307 | const handleRefreshClick = useCallback((e: React.MouseEvent) => { 308 | e.preventDefault(); 309 | if (shouldUpdate()) { 310 | fetchTokenHoldings(true, true); // Force update on manual refresh 311 | } else { 312 | const timeLeft = Math.ceil((60000 - (Date.now() - lastUpdateTime.current)) / 1000); 313 | setError(`Please wait ${timeLeft} seconds before refreshing again`); 314 | } 315 | }, [fetchTokenHoldings, shouldUpdate]); 316 | 317 | const initPumpFunClient = useCallback(async () => { 318 | try { 319 | if (!privateKey || !process.env.NEXT_PUBLIC_HELIUS_RPC_URL) return; 320 | 321 | const connection = new Connection(process.env.NEXT_PUBLIC_HELIUS_RPC_URL); 322 | const decodedKey = bs58.decode(privateKey); 323 | const keypair = Keypair.fromSecretKey(decodedKey); 324 | const client = new PumpFunClient(connection, keypair); 325 | setPumpFunClient(client); 326 | } catch (err) { 327 | console.error('Error initializing PumpFunClient:', err); 328 | setError('Failed to initialize trading client'); 329 | } 330 | }, [privateKey]); 331 | 332 | useEffect(() => { 333 | if (privateKey) { 334 | initPumpFunClient(); 335 | } 336 | }, [privateKey, initPumpFunClient]); 337 | 338 | if (!privateKey) { 339 | return ( 340 |
341 |

Connect your wallet to view holdings

342 |
343 | ); 344 | } 345 | 346 | return ( 347 |
348 | {error && ( 349 |
350 | {error} 351 |
352 | )} 353 | 354 | {/* SOL Balance Card - Fixed at top */} 355 |
356 |
357 |
358 |
359 | SOL 360 |
361 |
362 |

Solana

363 |

SOL

364 |
365 |
366 |
367 |

368 | {solBalance !== null ? solBalance.toFixed(4) : '---'} 369 |

370 |

371 | ≈ ${solBalance !== null ? (solBalance * 20).toFixed(2) : '---'} 372 |

373 |
374 |
375 |
376 | 377 | {/* Holdings Section - Scrollable */} 378 |
379 |
380 |

Token Holdings

381 | 390 |
391 | 392 |
393 | {loading ? ( 394 |
395 |
396 |

Loading holdings...

397 |
398 | ) : holdings.length > 0 ? ( 399 |
400 | {holdings.map((token) => ( 401 |
402 |
403 |
404 |
405 | {token.symbol.slice(0, 2)} 406 |
407 |
408 |

{token.name}

409 |
410 | {token.symbol} 411 | 412 | {token.amount.toFixed(2)} 413 |
414 |
415 |
416 |
417 |

418 | ${token.pricePerToken ? (token.amount * token.pricePerToken).toFixed(2) : '---'} 419 |

420 |

421 | ${token.pricePerToken?.toFixed(6) || '---'} 422 |

423 |
424 |
425 | 426 | {/* Sell Controls - Collapsible */} 427 |
428 |
429 |
430 | {[25, 50, 75, 100].map((percent) => ( 431 | 438 | ))} 439 |
440 |
441 | handleSellAmountChange(token.mint, parseFloat(e.target.value))} 445 | className="w-full bg-gray-900/50 border border-gray-700 rounded px-2 py-0.5 text-xs text-gray-200 placeholder-gray-600" 446 | placeholder="Amount" 447 | /> 448 |
449 | 464 |
465 | {token.error && ( 466 |

{token.error}

467 | )} 468 | {token.sellAmount > 0 && ( 469 |

470 | Selling {((token.sellAmount / token.amount) * 100).toFixed(1)}% 471 | {token.pricePerToken && ( 472 | 473 | (≈ ${(token.sellAmount * token.pricePerToken).toFixed(2)}) 474 | 475 | )} 476 |

477 | )} 478 |
479 |
480 | ))} 481 |
482 | ) : ( 483 |
484 |
485 | 486 | 487 | 488 |
489 |

No tokens found in your wallet

490 |

Tokens will appear here after purchase

491 |
492 | )} 493 |
494 |
495 |
496 | ); 497 | } 498 | -------------------------------------------------------------------------------- /src/components/TradingSettings.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useState, useEffect, useCallback } from 'react'; 4 | import { Tab } from '@headlessui/react'; 5 | import { useTradingContext } from '../contexts/TradingContext'; 6 | import { useBlacklistContext } from '../contexts/BlacklistContext'; 7 | import { useBuylistContext } from '../contexts/BuylistContext'; 8 | import { toast } from 'react-hot-toast'; 9 | import QRCode from 'qrcode'; 10 | import { Keypair, Connection, PublicKey, LAMPORTS_PER_SOL } from '@solana/web3.js'; 11 | import bs58 from 'bs58'; 12 | import { EyeIcon, EyeSlashIcon, CogIcon, UserGroupIcon, WalletIcon, CurrencyDollarIcon } from '@heroicons/react/24/outline'; 13 | import { RPC_ENDPOINT } from '../constants'; 14 | import OrderStatus from './OrderStatus'; 15 | import PurchasedTokens from './PurchasedTokens'; 16 | 17 | interface TradingSettingsProps { 18 | isMobile: boolean; 19 | } 20 | 21 | function classNames(...classes: string[]) { 22 | return classes.filter(Boolean).join(' '); 23 | } 24 | 25 | const PRESET_AMOUNTS = [0.1, 0.25, 0.5, 1]; 26 | const PRESET_SLIPPAGES = [2.5, 5, 10, 25]; 27 | 28 | const TradingSettings: React.FC = ({ isMobile }) => { 29 | const { 30 | privateKey, 31 | setPrivateKey, 32 | minFollowers, 33 | setMinFollowers, 34 | autoBuyEnabled, 35 | setAutoBuyEnabled, 36 | buyAmount, 37 | setBuyAmount, 38 | slippage, 39 | setSlippage, 40 | followerCheckEnabled, 41 | setFollowerCheckEnabled, 42 | creationTimeEnabled, 43 | setCreationTimeEnabled, 44 | maxCreationTime, 45 | setMaxCreationTime, 46 | } = useTradingContext(); 47 | 48 | const { blacklistedUsers, addToBlacklist, removeFromBlacklist } = useBlacklistContext(); 49 | const { buylistedTokens, addToBuylist, removeFromBuylist } = useBuylistContext(); 50 | 51 | const [mounted, setMounted] = useState(false); 52 | const [showKey, setShowKey] = useState(false); 53 | const [error, setError] = useState(null); 54 | const [solBalance, setSolBalance] = useState(null); 55 | const [publicKey, setPublicKey] = useState(null); 56 | const [isImporting, setIsImporting] = useState(false); 57 | const [newBlacklistedUser, setNewBlacklistedUser] = useState(''); 58 | const [newBuylistUser, setNewBuylistUser] = useState(''); 59 | const [qrCodeUrl, setQrCodeUrl] = useState(null); 60 | const [selectedTab, setSelectedTab] = useState(0); 61 | const [showConfirmation, setShowConfirmation] = useState(false); 62 | const [pendingAutoBuy, setPendingAutoBuy] = useState(false); 63 | 64 | // Initialize Solana connection 65 | const connection = new Connection(RPC_ENDPOINT, 'confirmed'); 66 | 67 | const updateBalance = useCallback(async (address: string) => { 68 | try { 69 | const pubKey = new PublicKey(address); 70 | const balance = await connection.getBalance(pubKey); 71 | setSolBalance(balance / LAMPORTS_PER_SOL); 72 | } catch (err) { 73 | console.error('Error fetching balance:', err); 74 | setSolBalance(null); 75 | } 76 | }, [connection]); 77 | 78 | useEffect(() => { 79 | setMounted(true); 80 | }, []); 81 | 82 | useEffect(() => { 83 | const checkPrivateKey = async () => { 84 | if (privateKey) { 85 | try { 86 | const decodedKey = bs58.decode(privateKey); 87 | const keypair = Keypair.fromSecretKey(decodedKey); 88 | const pubKeyStr = keypair.publicKey.toString(); 89 | setPublicKey(pubKeyStr); 90 | await updateBalance(pubKeyStr); 91 | 92 | const qrUrl = await QRCode.toDataURL(pubKeyStr); 93 | setQrCodeUrl(qrUrl); 94 | } catch (err) { 95 | console.error('Error deriving public key:', err); 96 | setPublicKey(null); 97 | setQrCodeUrl(null); 98 | } 99 | } else { 100 | setPublicKey(null); 101 | setSolBalance(null); 102 | setQrCodeUrl(null); 103 | } 104 | }; 105 | 106 | checkPrivateKey(); 107 | }, [privateKey, updateBalance]); 108 | 109 | if (!mounted) { 110 | return null; 111 | } 112 | 113 | const handlePrivateKeyChange = (value: string) => { 114 | try { 115 | bs58.decode(value); 116 | setPrivateKey(value); 117 | setError(null); 118 | } catch (err) { 119 | setError('Invalid private key format'); 120 | } 121 | }; 122 | 123 | const handleGenerateWallet = () => { 124 | try { 125 | const randomBytes = new Uint8Array(32); 126 | crypto.getRandomValues(randomBytes); 127 | const newKeypair = Keypair.fromSeed(randomBytes); 128 | const newPrivateKey = bs58.encode(newKeypair.secretKey); 129 | const newPublicKey = newKeypair.publicKey.toString(); 130 | 131 | const content = `Private Key: ${newPrivateKey}\nPublic Key: ${newPublicKey}\n\nIMPORTANT: Keep this file secure and never share your private key with anyone!`; 132 | const blob = new Blob([content], { type: 'text/plain' }); 133 | const url = window.URL.createObjectURL(blob); 134 | const link = document.createElement('a'); 135 | link.href = url; 136 | link.download = `solana-wallet-${newPublicKey.slice(0, 8)}.txt`; 137 | document.body.appendChild(link); 138 | link.click(); 139 | document.body.removeChild(link); 140 | window.URL.revokeObjectURL(url); 141 | 142 | setPrivateKey(newPrivateKey); 143 | setError(null); 144 | toast.success('New wallet generated and private key downloaded'); 145 | } catch (err) { 146 | console.error('Failed to generate wallet:', err); 147 | setError('Failed to generate new wallet'); 148 | toast.error('Failed to generate new wallet'); 149 | } 150 | }; 151 | 152 | const handleImportClick = () => { 153 | if (!isImporting) { 154 | setPrivateKey(''); 155 | setPublicKey(''); 156 | setSolBalance(null); 157 | setIsImporting(true); 158 | } else { 159 | if (privateKey) { 160 | try { 161 | handlePrivateKeyChange(privateKey); 162 | toast.success('Wallet imported successfully'); 163 | setIsImporting(false); 164 | } catch (err) { 165 | toast.error('Invalid private key'); 166 | } 167 | } else { 168 | setError('Please enter a private key'); 169 | } 170 | } 171 | }; 172 | 173 | const handleAutoBuyToggle = () => { 174 | if (!autoBuyEnabled) { 175 | // If turning on auto-buy, show confirmation 176 | setShowConfirmation(true); 177 | setPendingAutoBuy(true); 178 | } else { 179 | // If turning off auto-buy, do it immediately 180 | setAutoBuyEnabled(false); 181 | } 182 | }; 183 | 184 | const confirmAutoBuy = () => { 185 | setAutoBuyEnabled(true); 186 | setShowConfirmation(false); 187 | setPendingAutoBuy(false); 188 | }; 189 | 190 | const cancelAutoBuy = () => { 191 | setShowConfirmation(false); 192 | setPendingAutoBuy(false); 193 | }; 194 | 195 | const tabs = [ 196 | { name: 'Trading', icon: CogIcon }, 197 | { name: 'Lists', icon: UserGroupIcon }, 198 | { name: 'Holdings', icon: CurrencyDollarIcon }, 199 | { name: 'Wallet', icon: WalletIcon }, 200 | ]; 201 | 202 | return ( 203 |
204 | 205 | 206 | {tabs.map((tab) => ( 207 | 210 | classNames( 211 | 'flex items-center space-x-2 px-4 py-3 text-sm font-medium focus:outline-none flex-1 transition-all duration-200', 212 | selected 213 | ? 'text-yellow-500 border-b-2 border-yellow-500 bg-gray-800/50' 214 | : 'text-gray-400 hover:text-gray-200 hover:bg-gray-800/30' 215 | ) 216 | } 217 | > 218 | 219 | {tab.name} 220 | 221 | ))} 222 | 223 | 224 | 225 | {/* Trading Panel */} 226 | 227 |
228 | {/* Auto-buy Settings */} 229 |
230 | {/* Main Auto-buy Toggle */} 231 |
235 |
236 | Auto-buy 237 |

238 | {autoBuyEnabled 239 | ? "Actively buying tokens from new tweets" 240 | : "Configure settings below, then enable to start buying"} 241 |

242 |
243 |
244 | e.stopPropagation()} 248 | className="sr-only peer" 249 | /> 250 |
251 |
252 |
253 | 254 | {/* Confirmation Dialog */} 255 | {showConfirmation && ( 256 |
257 |
258 |
259 |

Enable Auto-buy?

260 |
261 |

Please review your settings before enabling auto-buy:

262 | 263 |
264 |
265 | Buy Amount: 266 | {buyAmount} SOL 267 |
268 |
269 | Slippage: 270 | {slippage}% 271 |
272 |
273 | Follower Check: 274 | {followerCheckEnabled ? `${minFollowers}+ followers` : 'Disabled'} 275 |
276 |
277 | Age Check: 278 | {creationTimeEnabled ? `Max ${maxCreationTime} mins` : 'Disabled'} 279 |
280 |
281 | 282 |
283 | 289 | 295 |
296 |
297 |
298 |
299 | )} 300 | 301 | {/* Buy Amount and Slippage Settings */} 302 |
303 |
304 | {/* Buy Amount */} 305 |
306 | 307 |
308 |
309 | setBuyAmount(parseFloat(e.target.value) || 0)} 313 | step="0.1" 314 | min="0" 315 | className="w-full bg-gray-700 text-gray-300 rounded px-3 py-1.5 text-right pr-8" 316 | /> 317 | 318 | ◎ 319 | 320 |
321 |
322 | {PRESET_AMOUNTS.map((amount) => ( 323 | 334 | ))} 335 |
336 |
337 |
338 | 339 | {/* Slippage */} 340 |
341 | 342 |
343 |
344 | setSlippage(parseFloat(e.target.value) || 0)} 348 | step="0.5" 349 | min="0" 350 | max="100" 351 | className="w-full bg-gray-700 text-gray-300 rounded px-3 py-1.5 text-right pr-8" 352 | /> 353 | 354 | % 355 | 356 |
357 |
358 | {PRESET_SLIPPAGES.map((value) => ( 359 | 370 | ))} 371 |
372 |
373 |
374 |
375 | 376 | {/* Warning for high slippage */} 377 | {slippage > 10 && ( 378 |
379 | 380 | 381 | 382 | High slippage may result in unfavorable trades 383 |
384 | )} 385 |
386 | 387 | {/* Auto-buy Filters */} 388 |
389 |
390 |
391 | Buy Filters 392 |
393 |
setFollowerCheckEnabled(!followerCheckEnabled)} 396 | > 397 | Followers 398 |
399 | e.stopPropagation()} 403 | className="sr-only peer" 404 | /> 405 |
406 |
407 |
408 |
setCreationTimeEnabled(!creationTimeEnabled)} 411 | > 412 | Age 413 |
414 | e.stopPropagation()} 418 | className="sr-only peer" 419 | /> 420 |
421 |
422 |
423 |
424 |
425 | 426 |
427 |
428 | 429 | setMinFollowers(Number(e.target.value))} 433 | className="w-full px-3 py-1.5 bg-gray-900 text-white border border-gray-700 rounded-lg focus:border-yellow-500 focus:ring-1 focus:ring-yellow-500 text-sm" 434 | min="0" 435 | /> 436 |
437 |
438 | 439 | setMaxCreationTime(Number(e.target.value))} 443 | className="w-full px-3 py-1.5 bg-gray-900 text-white border border-gray-700 rounded-lg focus:border-yellow-500 focus:ring-1 focus:ring-yellow-500 text-sm" 444 | min="1" 445 | step="1" 446 | /> 447 |
448 |
449 |
450 | 451 | {/* Order Status */} 452 |
453 | 454 |
455 |
456 |
457 |
458 |
459 | 460 | {/* Lists Panel */} 461 | 462 | 463 | 464 | 466 | classNames( 467 | 'px-4 py-2 text-sm font-medium rounded-lg focus:outline-none flex-1 transition-colors', 468 | selected 469 | ? 'bg-yellow-500/10 text-yellow-500' 470 | : 'text-gray-400 hover:text-gray-200 hover:bg-gray-800/50' 471 | ) 472 | } 473 | > 474 | Blacklist ({blacklistedUsers.length}) 475 | 476 | 478 | classNames( 479 | 'px-4 py-2 text-sm font-medium rounded-lg focus:outline-none flex-1 transition-colors', 480 | selected 481 | ? 'bg-yellow-500/10 text-yellow-500' 482 | : 'text-gray-400 hover:text-gray-200 hover:bg-gray-800/50' 483 | ) 484 | } 485 | > 486 | Buylist ({buylistedTokens.length}) 487 | 488 | 489 | 490 | 491 |
492 | setNewBlacklistedUser(e.target.value)} 496 | className="flex-1 px-3 py-2 bg-gray-900 text-white border border-gray-700 rounded-lg focus:border-yellow-500 focus:ring-1 focus:ring-yellow-500 text-sm" 497 | placeholder="Add new blacklisted user" 498 | /> 499 | 510 |
511 |
512 | {blacklistedUsers.map((user, idx) => ( 513 |
517 | @{user} 518 | 524 |
525 | ))} 526 | {blacklistedUsers.length === 0 && ( 527 |
528 | No blacklisted users 529 |
530 | )} 531 |
532 |
533 | 534 |
535 | setNewBuylistUser(e.target.value)} 539 | className="flex-1 px-3 py-2 bg-gray-900 text-white border border-gray-700 rounded-lg focus:border-yellow-500 focus:ring-1 focus:ring-yellow-500 text-sm" 540 | placeholder="Add new buylisted user" 541 | /> 542 | 553 |
554 |
555 | {buylistedTokens.map((user, idx) => ( 556 |
560 | @{user} 561 | 567 |
568 | ))} 569 | {buylistedTokens.length === 0 && ( 570 |
571 | No buylisted users 572 |
573 | )} 574 |
575 |
576 |
577 |
578 |
579 | 580 | {/* Holdings Panel */} 581 | 582 |
583 |
584 |

Your Token Holdings

585 |
586 |
587 | 588 |
589 |
590 |
591 | 592 | {/* Wallet Panel */} 593 | 594 |
595 | {/* Private Key Input */} 596 |
597 | 598 |
599 | handlePrivateKeyChange(e.target.value)} 603 | className="w-full px-3 py-2 bg-gray-900 text-white border border-gray-700 rounded-lg focus:border-yellow-500 focus:ring-1 focus:ring-yellow-500 text-sm" 604 | placeholder="Enter your private key" 605 | /> 606 | 617 |
618 | {error && ( 619 |

{error}

620 | )} 621 |
622 | 623 | {/* Public Key Display */} 624 | {publicKey && ( 625 |
626 | 627 | 633 |
634 | )} 635 | 636 | {/* SOL Balance */} 637 | {solBalance !== null && ( 638 |
639 | 640 |
641 | {solBalance.toFixed(4)} SOL 642 |
643 |
644 | )} 645 | 646 | {/* QR Code */} 647 | {qrCodeUrl && ( 648 |
649 | 650 |
651 | Wallet QR Code 652 |
653 |
654 | )} 655 | 656 | {/* Wallet Actions */} 657 |
658 | 664 | 674 |
675 |
676 |
677 |
678 |
679 |
680 | ); 681 | }; 682 | 683 | export default TradingSettings; 684 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | import { ComputeBudgetProgram } from '@solana/web3.js'; 3 | import { TransactionInstruction } from '@solana/web3.js'; 4 | 5 | // Program IDs and Important Accounts 6 | export const PUMP_FUN_PROGRAM = new PublicKey('6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P'); 7 | export const GLOBAL = new PublicKey('4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf'); 8 | export const FEE_RECIPIENT = new PublicKey('CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM'); 9 | export const EVENT_AUTHORITY = new PublicKey('Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1'); 10 | 11 | // System Program IDs 12 | export const SYSTEM_PROGRAM = new PublicKey('11111111111111111111111111111111'); 13 | export const TOKEN_PROGRAM = new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'); 14 | export const ASSOCIATED_TOKEN_PROGRAM = new PublicKey('ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL'); 15 | export const RENT = new PublicKey('SysvarRent111111111111111111111111111111111'); 16 | 17 | // Decimals 18 | export const SOL_DECIMAL = 1_000_000_000; // 10^9 19 | export const TOKEN_DECIMAL = 1_000_000; // 10^6 20 | 21 | // Transaction Settings 22 | export const COMPUTE_UNIT_LIMIT = 400_000; 23 | export const COMPUTE_UNIT_PRICE = 100; 24 | export const PRIORITY_RATE = 100_000; // 10 LAMPORTS per CU for better priority 25 | 26 | // Create the compute budget instruction 27 | export const COMPUTE_BUDGET_IX = ComputeBudgetProgram.setComputeUnitLimit({ 28 | units: COMPUTE_UNIT_LIMIT 29 | }); 30 | 31 | // Create priority fee instruction 32 | export const PRIORITY_FEE_IX = ComputeBudgetProgram.setComputeUnitPrice({ 33 | microLamports: PRIORITY_RATE 34 | }); 35 | 36 | // Commitment Levels 37 | export const COMMITMENT_LEVEL = 'confirmed'; 38 | 39 | // RPC Settings 40 | export const RPC_ENDPOINT = process.env.NEXT_PUBLIC_HELIUS_RPC_URL; 41 | export const RPC_WEBSOCKET_ENDPOINT = process.env.NEXT_PUBLIC_HELIUS_RPC_URL?.replace('https://', 'wss://'); 42 | 43 | // Jito Settings 44 | export const JITO_TIP_PROGRAM_ID = new PublicKey('4P1KYhBSn7RMGG5pYjvKmzGQPRXHBeCkFGfgVzVwGfXg'); 45 | export const JITO_TIP_ACCOUNT = new PublicKey('GZctHpWXmsZC1YHACTGGcHhYxjdRqQvTpYkb9LMvxDib'); 46 | export const JITO_FEE = 1000; // 0.00001 SOL 47 | 48 | // Create JitoTip instruction 49 | export const JITO_TIP_IX = new TransactionInstruction({ 50 | keys: [ 51 | { 52 | pubkey: JITO_TIP_ACCOUNT, 53 | isSigner: false, 54 | isWritable: true, 55 | }, 56 | ], 57 | programId: JITO_TIP_PROGRAM_ID, 58 | data: Buffer.from([]), 59 | }); 60 | -------------------------------------------------------------------------------- /src/contexts/BlacklistContext.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react'; 5 | 6 | // Maintains a persistent list of Twitter usernames to filter out from the feed, helping users avoid known scammers or unreliable token calls. 7 | 8 | interface BlacklistContextType { 9 | blacklistedUsers: string[]; 10 | addToBlacklist: (username: string) => void; 11 | removeFromBlacklist: (username: string) => void; 12 | isBlacklisted: (username: string) => boolean; 13 | } 14 | 15 | const BlacklistContext = createContext({ 16 | blacklistedUsers: [], 17 | addToBlacklist: () => {}, 18 | removeFromBlacklist: () => {}, 19 | isBlacklisted: () => false, 20 | }); 21 | 22 | export function useBlacklistContext() { 23 | const context = useContext(BlacklistContext); 24 | if (!context) { 25 | throw new Error('useBlacklistContext must be used within a BlacklistProvider'); 26 | } 27 | return context; 28 | } 29 | 30 | interface BlacklistProviderProps { 31 | children: ReactNode; 32 | } 33 | 34 | export function BlacklistProvider({ children }: BlacklistProviderProps) { 35 | const [blacklistedUsers, setBlacklistedUsers] = useState(() => { 36 | if (typeof window !== 'undefined') { 37 | try { 38 | const savedBlacklist = localStorage.getItem('pumpfun_blacklistedUsers'); 39 | if (savedBlacklist) { 40 | const parsed = JSON.parse(savedBlacklist); 41 | if (Array.isArray(parsed)) { 42 | return [...new Set(parsed)] 43 | .filter(user => typeof user === 'string' && user.trim().length > 0) 44 | .map(user => user.trim().toLowerCase()); 45 | } 46 | } 47 | } catch (error) { 48 | console.error('Error loading blacklist from localStorage:', error); 49 | try { 50 | localStorage.removeItem('pumpfun_blacklistedUsers'); 51 | } catch (e) { 52 | console.error('Failed to clear corrupted blacklist:', e); 53 | } 54 | } 55 | } 56 | return []; 57 | }); 58 | 59 | // Cache blacklistedUsers changes 60 | useEffect(() => { 61 | if (typeof window === 'undefined') return; 62 | try { 63 | localStorage.setItem('pumpfun_blacklistedUsers', JSON.stringify(blacklistedUsers)); 64 | } catch (error) { 65 | console.error('Error saving blacklist to localStorage:', error); 66 | } 67 | }, [blacklistedUsers]); 68 | 69 | const addToBlacklist = useCallback((username: string) => { 70 | if (!username || typeof username !== 'string') return; 71 | const cleanUsername = username.trim().toLowerCase(); 72 | if (!cleanUsername) return; 73 | setBlacklistedUsers(prev => [...new Set([...prev, cleanUsername])]); 74 | }, []); 75 | 76 | const removeFromBlacklist = useCallback((username: string) => { 77 | if (!username) return; 78 | const cleanUsername = username.trim().toLowerCase(); 79 | setBlacklistedUsers(prev => prev.filter(u => u !== cleanUsername)); 80 | }, []); 81 | 82 | const isBlacklisted = useCallback((username: string) => { 83 | if (!username) return false; 84 | const cleanUsername = username.trim().toLowerCase(); 85 | return blacklistedUsers.includes(cleanUsername); 86 | }, [blacklistedUsers]); 87 | 88 | const value = { 89 | blacklistedUsers, 90 | addToBlacklist, 91 | removeFromBlacklist, 92 | isBlacklisted, 93 | }; 94 | 95 | return ( 96 | 97 | {children} 98 | 99 | ); 100 | } 101 | -------------------------------------------------------------------------------- /src/contexts/BuylistContext.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { createContext, useContext, useState, useCallback, useEffect } from 'react'; 4 | 5 | interface BuylistContextType { 6 | buylistedTokens: string[]; 7 | addToBuylist: (token: string) => void; 8 | removeFromBuylist: (token: string) => void; 9 | isBuylisted: (token: string) => boolean; 10 | } 11 | 12 | const BuylistContext = createContext({ 13 | buylistedTokens: [], 14 | addToBuylist: () => {}, 15 | removeFromBuylist: () => {}, 16 | isBuylisted: () => false, 17 | }); 18 | 19 | export const useBuylistContext = () => useContext(BuylistContext); 20 | 21 | export const BuylistProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { 22 | const [buylistedTokens, setBuylistedTokens] = useState(() => { 23 | if (typeof window !== 'undefined') { 24 | const saved = localStorage.getItem('buylistedTokens'); 25 | return saved ? JSON.parse(saved) : []; 26 | } 27 | return []; 28 | }); 29 | 30 | useEffect(() => { 31 | if (typeof window !== 'undefined') { 32 | localStorage.setItem('buylistedTokens', JSON.stringify(buylistedTokens)); 33 | } 34 | }, [buylistedTokens]); 35 | 36 | const addToBuylist = (token: string) => { 37 | setBuylistedTokens((prev) => [...new Set([...prev, token])]); 38 | }; 39 | 40 | const removeFromBuylist = (token: string) => { 41 | setBuylistedTokens((prev) => prev.filter((t) => t !== token)); 42 | }; 43 | 44 | const isBuylisted = useCallback((token: string) => { 45 | return buylistedTokens.includes(token); 46 | }, [buylistedTokens]); 47 | 48 | return ( 49 | 52 | {children} 53 | 54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /src/contexts/TradingContext.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { createContext, useContext, useState, useEffect, useCallback } from 'react'; 4 | 5 | export interface OrderStatus { 6 | id: string; 7 | tokenSymbol: string; 8 | tokenName: string; 9 | type: 'buy' | 'sell'; 10 | amount: number; 11 | status: 'pending' | 'success' | 'error' | 'removed'; 12 | timestamp: number; 13 | signature?: string; 14 | error?: string; 15 | mintAddress: string; 16 | } 17 | 18 | interface TradingContextType { 19 | privateKey: string; 20 | setPrivateKey: (key: string) => void; 21 | autoBuyEnabled: boolean; 22 | setAutoBuyEnabled: (enabled: boolean) => void; 23 | followerCheckEnabled: boolean; 24 | setFollowerCheckEnabled: (enabled: boolean) => void; 25 | creationTimeEnabled: boolean; 26 | setCreationTimeEnabled: (enabled: boolean) => void; 27 | minFollowers: number; 28 | setMinFollowers: (count: number) => void; 29 | maxCreationTime: number; 30 | setMaxCreationTime: (minutes: number) => void; 31 | buyAmount: number; 32 | setBuyAmount: (amount: number) => void; 33 | slippage: number; 34 | setSlippage: (percentage: number) => void; 35 | orders: OrderStatus[]; 36 | addOrder: (order: Omit) => OrderStatus; 37 | updateOrder: (id: string, updates: Partial) => void; 38 | removeOrder: (id: string) => void; 39 | } 40 | 41 | const TradingContext = createContext({ 42 | privateKey: '', 43 | setPrivateKey: () => {}, 44 | autoBuyEnabled: false, 45 | setAutoBuyEnabled: () => {}, 46 | followerCheckEnabled: false, 47 | setFollowerCheckEnabled: () => {}, 48 | creationTimeEnabled: false, 49 | setCreationTimeEnabled: () => {}, 50 | minFollowers: 1000, 51 | setMinFollowers: () => {}, 52 | maxCreationTime: 5, 53 | setMaxCreationTime: () => {}, 54 | buyAmount: 0.1, 55 | setBuyAmount: () => {}, 56 | slippage: 1, 57 | setSlippage: () => {}, 58 | orders: [], 59 | addOrder: () => ({ 60 | id: '', 61 | tokenSymbol: '', 62 | tokenName: '', 63 | type: 'buy', 64 | amount: 0, 65 | status: 'pending', 66 | timestamp: 0, 67 | mintAddress: '' 68 | }), 69 | updateOrder: () => {}, 70 | removeOrder: () => {}, 71 | }); 72 | 73 | export const useTradingContext = () => useContext(TradingContext); 74 | 75 | export const TradingProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { 76 | const [isHydrated, setIsHydrated] = useState(false); 77 | 78 | // Initialize state from localStorage if available 79 | const [privateKey, setPrivateKey] = useState(() => { 80 | if (typeof window === 'undefined') return ''; 81 | try { 82 | return localStorage.getItem('privateKey') || ''; 83 | } catch { 84 | return ''; 85 | } 86 | }); 87 | 88 | const [autoBuyEnabled, setAutoBuyEnabled] = useState(() => { 89 | if (typeof window === 'undefined') return false; 90 | try { 91 | return localStorage.getItem('autoBuyEnabled') === 'true'; 92 | } catch { 93 | return false; 94 | } 95 | }); 96 | 97 | const [followerCheckEnabled, setFollowerCheckEnabled] = useState(() => { 98 | if (typeof window === 'undefined') return false; 99 | try { 100 | return localStorage.getItem('followerCheckEnabled') === 'true'; 101 | } catch { 102 | return false; 103 | } 104 | }); 105 | 106 | const [creationTimeEnabled, setCreationTimeEnabled] = useState(() => { 107 | if (typeof window === 'undefined') return false; 108 | try { 109 | return localStorage.getItem('creationTimeEnabled') === 'true'; 110 | } catch { 111 | return false; 112 | } 113 | }); 114 | 115 | const [minFollowers, setMinFollowers] = useState(() => { 116 | if (typeof window === 'undefined') return 1000; 117 | try { 118 | return Number(localStorage.getItem('minFollowers')) || 1000; 119 | } catch { 120 | return 1000; 121 | } 122 | }); 123 | 124 | const [maxCreationTime, setMaxCreationTime] = useState(() => { 125 | if (typeof window === 'undefined') return 5; 126 | try { 127 | return Number(localStorage.getItem('maxCreationTime')) || 5; 128 | } catch { 129 | return 5; 130 | } 131 | }); 132 | 133 | const [buyAmount, setBuyAmount] = useState(() => { 134 | if (typeof window === 'undefined') return 0.1; 135 | try { 136 | return Number(localStorage.getItem('buyAmount')) || 0.1; 137 | } catch { 138 | return 0.1; 139 | } 140 | }); 141 | 142 | const [slippage, setSlippage] = useState(() => { 143 | if (typeof window === 'undefined') return 1; 144 | try { 145 | return Number(localStorage.getItem('slippage')) || 1; 146 | } catch { 147 | return 1; 148 | } 149 | }); 150 | 151 | const [orders, setOrders] = useState([]); 152 | 153 | useEffect(() => { 154 | setIsHydrated(true); 155 | }, []); 156 | 157 | // Cleanup removed orders periodically 158 | useEffect(() => { 159 | if (!isHydrated) return; 160 | 161 | const interval = setInterval(() => { 162 | setOrders(prev => prev.filter(order => order.status !== 'removed')); 163 | }, 15000); 164 | return () => clearInterval(interval); 165 | }, [isHydrated]); 166 | 167 | // Update localStorage when settings change 168 | useEffect(() => { 169 | if (!isHydrated) return; 170 | 171 | try { 172 | localStorage.setItem('privateKey', privateKey); 173 | localStorage.setItem('autoBuyEnabled', String(autoBuyEnabled)); 174 | localStorage.setItem('followerCheckEnabled', String(followerCheckEnabled)); 175 | localStorage.setItem('creationTimeEnabled', String(creationTimeEnabled)); 176 | localStorage.setItem('minFollowers', String(minFollowers)); 177 | localStorage.setItem('maxCreationTime', String(maxCreationTime)); 178 | localStorage.setItem('buyAmount', String(buyAmount)); 179 | localStorage.setItem('slippage', String(slippage)); 180 | } catch (error) { 181 | console.error('Error saving to localStorage:', error); 182 | } 183 | }, [ 184 | isHydrated, 185 | privateKey, 186 | autoBuyEnabled, 187 | followerCheckEnabled, 188 | creationTimeEnabled, 189 | minFollowers, 190 | maxCreationTime, 191 | buyAmount, 192 | slippage 193 | ]); 194 | 195 | const addOrder = (order: Omit) => { 196 | const newOrder: OrderStatus = { 197 | ...order, 198 | id: Math.random().toString(36).substr(2, 9), 199 | timestamp: Date.now() 200 | }; 201 | setOrders(prev => [newOrder, ...prev]); 202 | return newOrder; 203 | }; 204 | 205 | const updateOrder = (id: string, updates: Partial) => { 206 | if (!updates) return; 207 | 208 | setOrders(prev => { 209 | // If the status is being updated to 'removed', remove the order 210 | if (updates?.status === 'removed') { 211 | return prev.filter(order => order.id !== id); 212 | } 213 | // Otherwise, update the order normally 214 | return prev.map(order => 215 | order.id === id ? { ...order, ...updates } : order 216 | ); 217 | }); 218 | }; 219 | 220 | const removeOrder = useCallback((id: string) => { 221 | setOrders(prev => prev.filter(order => order.id !== id)); 222 | }, []); 223 | 224 | return ( 225 | 249 | {children} 250 | 251 | ); 252 | }; 253 | -------------------------------------------------------------------------------- /src/contexts/WalletContext.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { createContext, useContext, useState } from 'react'; 4 | 5 | interface WalletContextType { 6 | privateKey: string | null; 7 | setPrivateKey: (key: string | null) => void; 8 | } 9 | 10 | const WalletContext = createContext({ 11 | privateKey: null, 12 | setPrivateKey: () => {}, 13 | }); 14 | 15 | export const useWalletContext = () => useContext(WalletContext); 16 | 17 | export const WalletProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { 18 | const [privateKey, setPrivateKey] = useState(null); 19 | 20 | return ( 21 | 22 | {children} 23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/dexscreenerClient.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Connection, 3 | Keypair, 4 | VersionedTransaction, 5 | LAMPORTS_PER_SOL, 6 | Transaction, 7 | TransactionInstruction, 8 | PublicKey, 9 | } from '@solana/web3.js'; 10 | import { AnchorProvider, Wallet } from '@project-serum/anchor'; 11 | import fetch from 'cross-fetch'; 12 | import axios from 'axios'; 13 | 14 | const WRAPPED_SOL_MINT = 'So11111111111111111111111111111111111111112'; 15 | const PRIORITY_RATE = 100_000; // Adjust this value if you need to set a priority fee 16 | const JUPITER_V6_API = 'https://quote-api.jup.ag/v6'; 17 | 18 | interface SwapQuote { 19 | inputMint: string; 20 | outputMint: string; 21 | amount: number; 22 | slippageBps: number; 23 | } 24 | 25 | interface SwapInfo { 26 | ammKey: string; 27 | label: string; 28 | inputMint: string; 29 | outputMint: string; 30 | inAmount: string; 31 | outAmount: string; 32 | feeAmount: string; 33 | feeMint: string; 34 | } 35 | 36 | interface RoutePlan { 37 | swapInfo: SwapInfo; 38 | percent: number; 39 | } 40 | 41 | interface Quote { 42 | inputMint: string; 43 | inAmount: string; 44 | outputMint: string; 45 | outAmount: string; 46 | otherAmountThreshold: string; 47 | swapMode: string; 48 | slippageBps: number; 49 | platformFee: any; 50 | priceImpactPct: string; 51 | routePlan: RoutePlan[]; 52 | contextSlot: number; 53 | timeTaken: number; 54 | } 55 | 56 | interface DexScreenerToken { 57 | address: string; 58 | name: string; 59 | symbol: string; 60 | } 61 | 62 | interface DexScreenerPair { 63 | chainId: string; 64 | dexId: string; 65 | url: string; 66 | pairAddress: string; 67 | baseToken: DexScreenerToken; 68 | quoteToken: DexScreenerToken; 69 | priceNative: string; 70 | priceUsd: string; 71 | txns: { 72 | m5: { buys: number; sells: number }; 73 | h1: { buys: number; sells: number }; 74 | h6: { buys: number; sells: number }; 75 | h24: { buys: number; sells: number }; 76 | }; 77 | pairCreatedAt: number; 78 | } 79 | 80 | interface DexScreenerResponse { 81 | pairs: DexScreenerPair[]; 82 | pair?: DexScreenerPair; 83 | } 84 | 85 | class DexscreenerClient { 86 | private connection: Connection; 87 | private wallet: Keypair; 88 | private provider: AnchorProvider; 89 | private rpcEndpoint: string; 90 | private tradingSettings: any; 91 | 92 | constructor( 93 | connection: Connection, 94 | wallet: Keypair, 95 | rpcEndpoint?: string, 96 | tradingSettings?: any 97 | ) { 98 | this.connection = connection; 99 | this.wallet = wallet; 100 | this.rpcEndpoint = 101 | rpcEndpoint || process.env.NEXT_PUBLIC_HELIUS_RPC_URL; 102 | this.tradingSettings = tradingSettings; 103 | 104 | // Create a wallet adapter that implements the Wallet interface 105 | const walletAdapter: Wallet = { 106 | publicKey: wallet.publicKey, 107 | signTransaction: async (tx: Transaction): Promise => { 108 | if (tx instanceof VersionedTransaction) { 109 | tx.sign([wallet]); 110 | return tx as unknown as Transaction; 111 | } else { 112 | tx.partialSign(wallet); 113 | return tx; 114 | } 115 | }, 116 | signAllTransactions: async (txs: Transaction[]): Promise => { 117 | return Promise.all( 118 | txs.map(async (tx) => { 119 | if (tx instanceof VersionedTransaction) { 120 | tx.sign([wallet]); 121 | return tx as unknown as Transaction; 122 | } else { 123 | tx.partialSign(wallet); 124 | return tx; 125 | } 126 | }) 127 | ); 128 | }, 129 | payer: wallet, 130 | }; 131 | 132 | // Initialize the provider with our wallet adapter 133 | this.provider = new AnchorProvider(connection, walletAdapter, { 134 | commitment: 'confirmed', 135 | skipPreflight: false, 136 | }); 137 | } 138 | 139 | private async getBaseTokenAddress(pairIdOrAddress: string): Promise { 140 | try { 141 | // First try as a pair address 142 | const response = await fetch( 143 | `https://api.dexscreener.com/latest/dex/pairs/solana/${pairIdOrAddress}` 144 | ); 145 | const data: DexScreenerResponse = await response.json(); 146 | 147 | // Check if we got a valid response with pairs 148 | if (data.pairs && data.pairs.length > 0) { 149 | return data.pairs[0].baseToken.address; 150 | } 151 | 152 | // If no pairs found, try as a token address 153 | const tokenResponse = await fetch( 154 | `https://api.dexscreener.com/latest/dex/tokens/${pairIdOrAddress}` 155 | ); 156 | const tokenData: DexScreenerResponse = await tokenResponse.json(); 157 | 158 | if (tokenData.pairs && tokenData.pairs.length > 0) { 159 | // Find the first Solana pair 160 | const solanaPair = tokenData.pairs.find( 161 | (pair) => pair.chainId === 'solana' 162 | ); 163 | if (solanaPair) { 164 | return solanaPair.baseToken.address; 165 | } 166 | } 167 | 168 | throw new Error(`Could not find base token address for ${pairIdOrAddress}`); 169 | } catch (error) { 170 | console.error('Error getting base token address:', error); 171 | throw error; 172 | } 173 | } 174 | 175 | public async getTokenPrice( 176 | pairIdOrAddress: string 177 | ): Promise { 178 | try { 179 | // Get amount and slippage from trading settings 180 | const amountInSol = this.tradingSettings?.amount || 0.1; // Default to 0.1 SOL if not set 181 | const amountLamports = amountInSol * LAMPORTS_PER_SOL; 182 | 183 | const slippageBps = this.tradingSettings?.slippage 184 | ? Math.floor(this.tradingSettings.slippage * 100) 185 | : 100; // Default to 1% if not set 186 | 187 | console.log( 188 | `Getting price quote for ${amountInSol} SOL with ${slippageBps} bps slippage` 189 | ); 190 | 191 | const quote = await this.getQuote({ 192 | inputMint: WRAPPED_SOL_MINT, 193 | outputMint: pairIdOrAddress, 194 | amount: amountLamports, 195 | slippageBps, 196 | }); 197 | 198 | if (!quote || !quote.outAmount) { 199 | console.log(`No quote available for token ${pairIdOrAddress}`); 200 | return undefined; 201 | } 202 | 203 | // Calculate price in SOL (outAmount will be in the token's smallest unit) 204 | const outAmount = BigInt(quote.outAmount); 205 | if (outAmount === 0n) { 206 | console.log( 207 | `Invalid outAmount from quote for token ${pairIdOrAddress}` 208 | ); 209 | return undefined; 210 | } 211 | 212 | const priceInSol = Number(amountLamports) / Number(outAmount); 213 | return priceInSol; 214 | } catch (error) { 215 | console.error('Error getting token price:', error); 216 | return undefined; 217 | } 218 | } 219 | 220 | private async getQuote(params: SwapQuote): Promise { 221 | try { 222 | const response = await axios.get(`${JUPITER_V6_API}/quote`, { 223 | params: { 224 | inputMint: params.inputMint, 225 | outputMint: params.outputMint, 226 | amount: params.amount, 227 | slippageBps: params.slippageBps || 100, 228 | onlyDirectRoutes: true 229 | }, 230 | }); 231 | return response.data; 232 | } catch (error) { 233 | console.error('Error getting quote:', error); 234 | throw error; 235 | } 236 | } 237 | 238 | private async getSwapTransaction(quoteResponse: any): Promise { 239 | try { 240 | const response = await axios.post(`${JUPITER_V6_API}/swap`, { 241 | quoteResponse, 242 | userPublicKey: this.wallet.publicKey.toString(), 243 | wrapUnwrapSOL: true, 244 | dynamicComputeUnitLimit: true, 245 | prioritizationFeeLamports: PRIORITY_RATE, 246 | }); 247 | 248 | const { swapTransaction } = response.data; 249 | const swapTransactionBuf = Buffer.from(swapTransaction, 'base64'); 250 | return VersionedTransaction.deserialize(swapTransactionBuf); 251 | } catch (error) { 252 | console.error('Error getting swap transaction:', error); 253 | throw error; 254 | } 255 | } 256 | 257 | public async buyToken( 258 | pairIdOrAddress: string, 259 | amountInSol: number 260 | ): Promise<{ success: boolean; signature?: string; error?: string }> { 261 | try { 262 | const baseTokenAddress = await this.getBaseTokenAddress(pairIdOrAddress); 263 | if (!baseTokenAddress) { 264 | return { success: false, error: 'Could not get base token address' }; 265 | } 266 | 267 | const amountLamports = amountInSol * LAMPORTS_PER_SOL; 268 | 269 | // Get quote 270 | const quoteParams: SwapQuote = { 271 | inputMint: WRAPPED_SOL_MINT, 272 | outputMint: baseTokenAddress, 273 | amount: amountLamports, 274 | slippageBps: 100, 275 | }; 276 | 277 | const quoteResponse = await this.getQuote(quoteParams); 278 | if (!quoteResponse) { 279 | return { success: false, error: 'Failed to get quote' }; 280 | } 281 | 282 | // Get and execute swap transaction 283 | const swapTransaction = await this.getSwapTransaction(quoteResponse); 284 | if (!swapTransaction) { 285 | return { success: false, error: 'Failed to get swap transaction' }; 286 | } 287 | 288 | // Execute the transaction 289 | const { success, signature, error } = await this.executeTransaction(swapTransaction); 290 | 291 | if (success && signature) { 292 | console.log(`Buy transaction completed successfully with signature: ${signature}`); 293 | return { success: true, signature }; 294 | } else { 295 | console.error(`Buy transaction failed: ${error}`); 296 | return { success: false, error: error || 'Transaction failed' }; 297 | } 298 | 299 | } catch (error) { 300 | console.error('Error in buyToken:', error); 301 | return { 302 | success: false, 303 | error: error instanceof Error ? error.message : 'Unknown error occurred' 304 | }; 305 | } 306 | } 307 | 308 | private async executeTransaction( 309 | transaction: VersionedTransaction 310 | ): Promise<{ success: boolean; signature?: string; error?: string }> { 311 | try { 312 | // Get latest blockhash 313 | const { blockhash, lastValidBlockHeight } = await this.connection.getLatestBlockhash('confirmed'); 314 | transaction.message.recentBlockhash = blockhash; 315 | 316 | // Sign transaction 317 | try { 318 | transaction.sign([this.wallet]); 319 | } catch (signError) { 320 | // Check if transaction is already signed 321 | const walletKey = this.wallet.publicKey.toBase58(); 322 | const isAlreadySigned = transaction.signatures.some((sig, index) => { 323 | const key = transaction.message.staticAccountKeys[index]?.toBase58(); 324 | return key === walletKey && sig !== null; 325 | }); 326 | 327 | if (!isAlreadySigned) { 328 | console.error('Transaction signing failed:', signError); 329 | return { success: false, error: 'Transaction signing failed' }; 330 | } 331 | } 332 | 333 | // Send transaction 334 | const signature = await this.connection.sendRawTransaction(transaction.serialize(), { 335 | skipPreflight: false, 336 | preflightCommitment: 'confirmed', 337 | maxRetries: 3, 338 | }); 339 | 340 | console.log(`Transaction sent: ${signature}`); 341 | 342 | // Confirm transaction 343 | const confirmation = await this.connection.confirmTransaction({ 344 | signature, 345 | blockhash, 346 | lastValidBlockHeight, 347 | }, 'confirmed'); 348 | 349 | if (confirmation.value.err) { 350 | return { 351 | success: false, 352 | signature, 353 | error: `Transaction failed: ${confirmation.value.err}` 354 | }; 355 | } 356 | 357 | // Double check transaction status 358 | const status = await this.connection.getSignatureStatus(signature); 359 | if (status.value?.err) { 360 | return { 361 | success: false, 362 | signature, 363 | error: `Transaction failed: ${status.value.err}` 364 | }; 365 | } 366 | 367 | return { success: true, signature }; 368 | } catch (error) { 369 | console.error('Transaction execution failed:', error); 370 | return { 371 | success: false, 372 | error: error instanceof Error ? error.message : 'Transaction execution failed' 373 | }; 374 | } 375 | } 376 | 377 | public async getTokenCreationTime( 378 | mintAddress: string 379 | ): Promise { 380 | // For now, return current time as we don't have a reliable way to get token creation time 381 | // This can be enhanced later to fetch actual creation time from chain or other sources 382 | return Math.floor(Date.now() / 1000); 383 | } 384 | 385 | public async shouldBuyToken(mintAddress: string): Promise { 386 | const { 387 | autoBuyEnabled = false, 388 | followerCheckEnabled = false, 389 | minFollowers = 0, 390 | creationTimeEnabled = false, 391 | maxCreationTime = 60, 392 | } = this.tradingSettings || {}; 393 | 394 | if (!autoBuyEnabled) { 395 | console.log('Autobuying is disabled'); 396 | return false; 397 | } 398 | 399 | let creationTimeCheckPassed = true; 400 | 401 | // Check creation time if enabled 402 | if (creationTimeEnabled) { 403 | const tokenCreationTime = await this.getTokenCreationTime(mintAddress); 404 | if (!tokenCreationTime) { 405 | console.log('Could not determine token creation time'); 406 | return false; 407 | } 408 | 409 | const currentTime = Math.floor(Date.now() / 1000); 410 | const tokenAgeInMinutes = (currentTime - tokenCreationTime) / 60; 411 | 412 | creationTimeCheckPassed = tokenAgeInMinutes <= maxCreationTime; 413 | if (!creationTimeCheckPassed) { 414 | console.log( 415 | `Token age (${Math.round( 416 | tokenAgeInMinutes 417 | )} minutes) exceeds maximum allowed age (${maxCreationTime} minutes)` 418 | ); 419 | } 420 | } 421 | 422 | // For DexScreener tokens, we don't have follower information 423 | // So if follower check is enabled, we should not allow the buy 424 | if (followerCheckEnabled) { 425 | console.log( 426 | 'Follower check is enabled but not supported for DexScreener tokens' 427 | ); 428 | return false; 429 | } 430 | 431 | return creationTimeCheckPassed; 432 | } 433 | } 434 | 435 | export { DexscreenerClient }; 436 | -------------------------------------------------------------------------------- /src/pumpFunClient.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Connection, 3 | Keypair, 4 | PublicKey, 5 | Transaction, 6 | TransactionInstruction, 7 | LAMPORTS_PER_SOL, 8 | SystemProgram, 9 | sendAndConfirmTransaction as web3SendAndConfirmTransaction, 10 | ComputeBudgetProgram, 11 | VersionedTransaction, 12 | TransactionMessage 13 | } from '@solana/web3.js'; 14 | import * as token from '@solana/spl-token'; 15 | import BN from 'bn.js'; 16 | import { 17 | PUMP_FUN_PROGRAM, 18 | GLOBAL, 19 | FEE_RECIPIENT, 20 | EVENT_AUTHORITY, 21 | SYSTEM_PROGRAM, 22 | TOKEN_PROGRAM, 23 | ASSOCIATED_TOKEN_PROGRAM, 24 | RENT, 25 | SOL_DECIMAL, 26 | TOKEN_DECIMAL, 27 | COMPUTE_UNIT_LIMIT, 28 | PRIORITY_RATE 29 | } from './constants'; 30 | import axios from 'axios'; 31 | import bs58 from 'bs58'; 32 | 33 | const WRAPPED_SOL_MINT = 'So11111111111111111111111111111111111111112'; 34 | const JUPITER_V6_API = 'https://quote-api.jup.ag/v6'; 35 | 36 | interface SwapQuote { 37 | inputMint: string; 38 | outputMint: string; 39 | amount: number; 40 | slippageBps?: number; 41 | } 42 | 43 | interface Quote { 44 | outAmount: string; 45 | [key: string]: any; 46 | } 47 | 48 | interface CoinData { 49 | mint: string; 50 | name: string; 51 | symbol: string; 52 | bondingCurve: string; 53 | associatedBondingCurve: string; 54 | virtualTokenReserves: number; 55 | virtualSolReserves: number; 56 | tokenTotalSupply: number; 57 | complete: boolean; 58 | usdMarketCap: number; 59 | marketCap: number; 60 | creator: string; 61 | createdTimestamp: number; 62 | } 63 | 64 | enum PumpFunError { 65 | NotAuthorized = 6000, 66 | AlreadyInitialized = 6001, 67 | TooMuchSolRequired = 6002, 68 | TooLittleSolReceived = 6003, 69 | MintDoesNotMatchBondingCurve = 6004, 70 | BondingCurveComplete = 6005, 71 | BondingCurveNotComplete = 6006, 72 | NotInitialized = 6007 73 | } 74 | 75 | class PumpFunClient { 76 | private connection: Connection; 77 | private wallet: Keypair; 78 | private rpcEndpoint: string; 79 | private lastRequestId: number = 0; 80 | private tradingSettings: any; 81 | private lastBuyTimestamp: number = 0; 82 | private buyAttempts: Map = new Map(); 83 | private readonly MIN_BUY_INTERVAL = 2000; 84 | private readonly MAX_BUY_ATTEMPTS = 3; 85 | private readonly BUY_ATTEMPT_WINDOW = 60000; 86 | 87 | constructor(connection: Connection, wallet: Keypair, rpcEndpoint?: string, tradingSettings?: any) { 88 | this.connection = connection; 89 | this.wallet = wallet; 90 | this.rpcEndpoint = rpcEndpoint || process.env.NEXT_PUBLIC_HELIUS_RPC_URL; 91 | this.tradingSettings = tradingSettings; 92 | } 93 | 94 | private async getCoinData(mintStr: string): Promise { 95 | try { 96 | const response = await axios.get(`/api/pump-proxy?mintAddress=${mintStr}`); 97 | 98 | if (response.status === 200) { 99 | const data = response.data; 100 | return { 101 | mint: mintStr, 102 | name: data.name, 103 | symbol: data.symbol, 104 | virtualTokenReserves: data.virtual_token_reserves, 105 | virtualSolReserves: data.virtual_sol_reserves, 106 | bondingCurve: data.bonding_curve, 107 | associatedBondingCurve: data.associated_bonding_curve, 108 | tokenTotalSupply: data.total_supply, 109 | complete: data.complete, 110 | usdMarketCap: data.usd_market_cap, 111 | marketCap: data.market_cap, 112 | creator: data.creator, 113 | createdTimestamp: data.created_timestamp 114 | }; 115 | } 116 | return null; 117 | } catch (error) { 118 | console.error('Error fetching coin data:', error); 119 | return null; 120 | } 121 | } 122 | 123 | private async getQuote(params: SwapQuote): Promise { 124 | try { 125 | const response = await axios.get(`${JUPITER_V6_API}/quote`, { 126 | params: { 127 | inputMint: params.inputMint, 128 | outputMint: params.outputMint, 129 | amount: params.amount, 130 | slippageBps: params.slippageBps || 100, 131 | onlyDirectRoutes: true 132 | }, 133 | }); 134 | return response.data; 135 | } catch (error) { 136 | console.error('Error getting quote:', error); 137 | throw error; 138 | } 139 | } 140 | 141 | private async getSwapTransaction(quoteResponse: any): Promise { 142 | try { 143 | const response = await axios.post(`${JUPITER_V6_API}/swap`, { 144 | quoteResponse, 145 | userPublicKey: this.wallet.publicKey.toString(), 146 | wrapUnwrapSOL: true, 147 | dynamicComputeUnitLimit: true, 148 | prioritizationFeeLamports: PRIORITY_RATE, 149 | }); 150 | 151 | const { swapTransaction } = response.data; 152 | const swapTransactionBuf = Buffer.from(swapTransaction, 'base64'); 153 | return VersionedTransaction.deserialize(swapTransactionBuf); 154 | } catch (error) { 155 | console.error('Error getting swap transaction:', error); 156 | throw error; 157 | } 158 | } 159 | 160 | private async executeTransaction( 161 | transaction: VersionedTransaction 162 | ): Promise<{ success: boolean; signature?: string; error?: string }> { 163 | try { 164 | const { blockhash, lastValidBlockHeight } = await this.connection.getLatestBlockhash('confirmed'); 165 | transaction.message.recentBlockhash = blockhash; 166 | 167 | try { 168 | transaction.sign([this.wallet]); 169 | } catch (signError) { 170 | const walletKey = this.wallet.publicKey.toBase58(); 171 | const isAlreadySigned = transaction.signatures.some((sig, index) => { 172 | const key = transaction.message.staticAccountKeys[index]?.toBase58(); 173 | return key === walletKey && sig !== null; 174 | }); 175 | 176 | if (!isAlreadySigned) { 177 | console.error('Transaction signing failed:', signError); 178 | return { success: false, error: 'Transaction signing failed' }; 179 | } 180 | } 181 | 182 | const signature = await this.connection.sendRawTransaction(transaction.serialize(), { 183 | skipPreflight: false, 184 | preflightCommitment: 'confirmed', 185 | maxRetries: 3, 186 | }); 187 | 188 | console.log(`Transaction sent: ${signature}`); 189 | 190 | const confirmation = await this.connection.confirmTransaction({ 191 | signature, 192 | blockhash, 193 | lastValidBlockHeight, 194 | }, 'confirmed'); 195 | 196 | if (confirmation.value.err) { 197 | return { 198 | success: false, 199 | signature, 200 | error: `Transaction failed: ${confirmation.value.err}` 201 | }; 202 | } 203 | 204 | const status = await this.connection.getSignatureStatus(signature); 205 | if (status.value?.err) { 206 | return { 207 | success: false, 208 | signature, 209 | error: `Transaction failed: ${status.value.err}` 210 | }; 211 | } 212 | 213 | return { success: true, signature }; 214 | } catch (error) { 215 | console.error('Transaction execution failed:', error); 216 | return { 217 | success: false, 218 | error: error instanceof Error ? error.message : 'Transaction execution failed' 219 | }; 220 | } 221 | } 222 | 223 | public async buy( 224 | mintAddress: string, 225 | amountInSol: number, 226 | slippage: number = 0.25 227 | ): Promise<{ success: boolean; signature?: string; error?: string }> { 228 | try { 229 | const amountLamports = amountInSol * LAMPORTS_PER_SOL; 230 | const slippageBps = Math.floor(slippage * 100); 231 | 232 | console.log(`Getting quote for ${amountInSol} SOL with ${slippageBps} bps slippage`); 233 | 234 | const quoteParams: SwapQuote = { 235 | inputMint: WRAPPED_SOL_MINT, 236 | outputMint: mintAddress, 237 | amount: amountLamports, 238 | slippageBps, 239 | }; 240 | 241 | const quoteResponse = await this.getQuote(quoteParams); 242 | if (!quoteResponse) { 243 | return { success: false, error: 'Failed to get quote' }; 244 | } 245 | 246 | const swapTransaction = await this.getSwapTransaction(quoteResponse); 247 | if (!swapTransaction) { 248 | return { success: false, error: 'Failed to get swap transaction' }; 249 | } 250 | 251 | // Get the associated token account 252 | const associatedTokenAccount = token.getAssociatedTokenAddressSync( 253 | new PublicKey(mintAddress), 254 | this.wallet.publicKey 255 | ); 256 | 257 | // Check if the associated token account exists 258 | const accountInfo = await this.connection.getAccountInfo(associatedTokenAccount); 259 | 260 | if (!accountInfo) { 261 | // Create associated token account if it doesn't exist 262 | const ataTransaction = new Transaction().add( 263 | token.createAssociatedTokenAccountInstruction( 264 | this.wallet.publicKey, 265 | associatedTokenAccount, 266 | this.wallet.publicKey, 267 | new PublicKey(mintAddress) 268 | ) 269 | ); 270 | 271 | // Execute ATA creation first 272 | const ataResult = await this.executeTransaction( 273 | new VersionedTransaction( 274 | new TransactionMessage({ 275 | payerKey: this.wallet.publicKey, 276 | recentBlockhash: (await this.connection.getLatestBlockhash()).blockhash, 277 | instructions: ataTransaction.instructions, 278 | }).compileToV0Message() 279 | ) 280 | ); 281 | 282 | if (!ataResult.success) { 283 | return { success: false, error: 'Failed to create token account' }; 284 | } 285 | } 286 | 287 | // Now execute the swap transaction 288 | return await this.executeTransaction(swapTransaction); 289 | 290 | } catch (error) { 291 | console.error('Error in buy:', error); 292 | return { 293 | success: false, 294 | error: error instanceof Error ? error.message : 'Unknown error occurred' 295 | }; 296 | } 297 | } 298 | 299 | public async sell( 300 | mintAddress: string, 301 | amountInTokens: number, 302 | slippage: number = 0.25 303 | ): Promise<{ success: boolean; signature?: string; error?: string }> { 304 | try { 305 | const slippageBps = Math.floor(slippage * 100); 306 | const amountInSmallestUnit = amountInTokens * TOKEN_DECIMAL; 307 | 308 | console.log(`Getting quote for ${amountInTokens} tokens with ${slippageBps} bps slippage`); 309 | 310 | const quoteParams: SwapQuote = { 311 | inputMint: mintAddress, 312 | outputMint: WRAPPED_SOL_MINT, 313 | amount: amountInSmallestUnit, 314 | slippageBps, 315 | }; 316 | 317 | const quoteResponse = await this.getQuote(quoteParams); 318 | if (!quoteResponse) { 319 | return { success: false, error: 'Failed to get quote' }; 320 | } 321 | 322 | const swapTransaction = await this.getSwapTransaction(quoteResponse); 323 | if (!swapTransaction) { 324 | return { success: false, error: 'Failed to get swap transaction' }; 325 | } 326 | 327 | return await this.executeTransaction(swapTransaction); 328 | 329 | } catch (error) { 330 | console.error('Error in sell:', error); 331 | return { 332 | success: false, 333 | error: error instanceof Error ? error.message : 'Unknown error occurred' 334 | }; 335 | } 336 | } 337 | 338 | public async getTokenPrice(mintAddress: string): Promise { 339 | try { 340 | const amountInSol = this.tradingSettings?.amount || 0.1; 341 | const amountLamports = amountInSol * LAMPORTS_PER_SOL; 342 | const slippageBps = this.tradingSettings?.slippage 343 | ? Math.floor(this.tradingSettings.slippage * 100) 344 | : 100; 345 | 346 | console.log( 347 | `Getting price quote for ${amountInSol} SOL with ${slippageBps} bps slippage` 348 | ); 349 | 350 | const quote = await this.getQuote({ 351 | inputMint: WRAPPED_SOL_MINT, 352 | outputMint: mintAddress, 353 | amount: amountLamports, 354 | slippageBps, 355 | }); 356 | 357 | if (!quote || !quote.outAmount) { 358 | console.log(`No quote available for token ${mintAddress}`); 359 | return undefined; 360 | } 361 | 362 | const outAmount = BigInt(quote.outAmount); 363 | if (outAmount === 0n) { 364 | console.log(`Invalid outAmount from quote for token ${mintAddress}`); 365 | return undefined; 366 | } 367 | 368 | const priceInSol = Number(amountLamports) / Number(outAmount); 369 | return priceInSol; 370 | } catch (error) { 371 | console.error('Error getting token price:', error); 372 | return undefined; 373 | } 374 | } 375 | 376 | public shouldBuyToken(coinData: any, twitterData: any): boolean { 377 | // If no trading settings exist, allow the buy (this is a manual buy) 378 | if (!this.tradingSettings) { 379 | return true; 380 | } 381 | 382 | // If this is a manual buy (no twitterData), allow it 383 | if (!twitterData) { 384 | return true; 385 | } 386 | 387 | // From this point on, we're dealing with autobuy 388 | 389 | // First check if autobuy is enabled 390 | if (!this.tradingSettings.autoBuyEnabled) { 391 | console.log('Autobuy is disabled'); 392 | return false; 393 | } 394 | 395 | // If both checks are turned off, no autobuys should happen 396 | if (!this.tradingSettings.followerCheckEnabled && !this.tradingSettings.creationTimeEnabled) { 397 | console.log('Both follower and age checks are disabled - no autobuys will occur'); 398 | return false; 399 | } 400 | 401 | let followerCheckPassed = false; 402 | let ageCheckPassed = false; 403 | 404 | // Check followers if enabled 405 | if (this.tradingSettings.followerCheckEnabled) { 406 | const followerCount = twitterData.user?.followers_count || 0; 407 | followerCheckPassed = followerCount >= this.tradingSettings.minFollowers; 408 | console.log(`Follower check ${followerCheckPassed ? 'passed' : 'failed'}: ${followerCount} ${followerCheckPassed ? '>=' : '<'} ${this.tradingSettings.minFollowers}`); 409 | } 410 | 411 | // Check age if enabled 412 | if (this.tradingSettings.creationTimeEnabled && coinData.createdTimestamp) { 413 | const tokenAge = (Date.now() / 1000) - coinData.createdTimestamp; 414 | const maxAgeInSeconds = this.tradingSettings.maxCreationTime * 60; 415 | ageCheckPassed = tokenAge <= maxAgeInSeconds; 416 | console.log(`Age check ${ageCheckPassed ? 'passed' : 'failed'}: ${Math.round(tokenAge / 60)} minutes ${ageCheckPassed ? '<=' : '>'} ${this.tradingSettings.maxCreationTime}`); 417 | } 418 | 419 | // If both checks are enabled, both must pass 420 | if (this.tradingSettings.followerCheckEnabled && this.tradingSettings.creationTimeEnabled) { 421 | const shouldBuy = followerCheckPassed && ageCheckPassed; 422 | console.log(`Both checks enabled: follower check ${followerCheckPassed}, age check ${ageCheckPassed} - ${shouldBuy ? 'buying' : 'not buying'}`); 423 | return shouldBuy; 424 | } 425 | 426 | // If only follower check is enabled 427 | if (this.tradingSettings.followerCheckEnabled) { 428 | console.log(`Only follower check enabled: ${followerCheckPassed ? 'buying' : 'not buying'}`); 429 | return followerCheckPassed; 430 | } 431 | 432 | // If only age check is enabled 433 | if (this.tradingSettings.creationTimeEnabled) { 434 | console.log(`Only age check enabled: ${ageCheckPassed ? 'buying' : 'not buying'}`); 435 | return ageCheckPassed; 436 | } 437 | 438 | // This line should never be reached due to earlier checks 439 | return false; 440 | } 441 | 442 | public async autoBuy( 443 | mintAddress: string, 444 | twitterData: any = null 445 | ): Promise<{ success: boolean; signature?: string; error?: string }> { 446 | try { 447 | // Check if enough time has passed since last buy 448 | const now = Date.now(); 449 | if (now - this.lastBuyTimestamp < this.MIN_BUY_INTERVAL) { 450 | return { 451 | success: false, 452 | error: 'Rate limit: Too soon since last buy attempt' 453 | }; 454 | } 455 | 456 | // Check and update buy attempts for this token 457 | const buyAttempt = this.buyAttempts.get(mintAddress) || { timestamp: 0, count: 0 }; 458 | if (now - buyAttempt.timestamp > this.BUY_ATTEMPT_WINDOW) { 459 | // Reset if window has expired 460 | buyAttempt.timestamp = now; 461 | buyAttempt.count = 1; 462 | } else if (buyAttempt.count >= this.MAX_BUY_ATTEMPTS) { 463 | return { 464 | success: false, 465 | error: `Max buy attempts (${this.MAX_BUY_ATTEMPTS}) reached for this token` 466 | }; 467 | } else { 468 | buyAttempt.count++; 469 | } 470 | this.buyAttempts.set(mintAddress, buyAttempt); 471 | 472 | // Get coin data and check if it meets criteria 473 | const coinData = await this.getCoinData(mintAddress); 474 | if (!coinData) { 475 | return { 476 | success: false, 477 | error: 'Failed to fetch coin data' 478 | }; 479 | } 480 | 481 | if (!this.shouldBuyToken(coinData, twitterData)) { 482 | return { 483 | success: false, 484 | error: 'Token does not meet buying criteria' 485 | }; 486 | } 487 | 488 | // Update last buy timestamp before attempting purchase 489 | this.lastBuyTimestamp = now; 490 | 491 | // Attempt to buy using settings from trading context 492 | const result = await this.buy( 493 | mintAddress, 494 | this.tradingSettings.buyAmount, 495 | this.tradingSettings.slippage 496 | ); 497 | 498 | if (!result.success || !result.signature) { 499 | return { 500 | success: false, 501 | error: result.error || 'Buy transaction failed' 502 | }; 503 | } 504 | 505 | return { 506 | success: true, 507 | signature: result.signature 508 | }; 509 | 510 | } catch (error) { 511 | console.error('Error in autoBuy:', error); 512 | return { 513 | success: false, 514 | error: error instanceof Error ? error.message : 'Unknown error in autoBuy' 515 | }; 516 | } 517 | } 518 | } 519 | 520 | export { PumpFunClient }; 521 | 522 | function bufferFromUInt64(value: number | string) { 523 | let buffer = Buffer.alloc(8); 524 | buffer.writeBigUInt64LE(BigInt(value.toString())); 525 | return buffer; 526 | } 527 | -------------------------------------------------------------------------------- /src/services/heliusService.ts: -------------------------------------------------------------------------------- 1 | import { TokenInfo } from '@/types'; 2 | 3 | export class HeliusService { 4 | private rpcUrl: string; 5 | 6 | constructor(rpcUrl: string) { 7 | this.rpcUrl = rpcUrl; 8 | } 9 | 10 | async getAsset(mintAddress: string): Promise { 11 | try { 12 | console.log('Sending Helius RPC request for mint:', mintAddress); 13 | const response = await fetch(this.rpcUrl, { 14 | method: 'POST', 15 | headers: { 16 | 'Content-Type': 'application/json', 17 | }, 18 | body: JSON.stringify({ 19 | jsonrpc: '2.0', 20 | id: 'helius-test', 21 | method: 'getAsset', 22 | params: { 23 | id: mintAddress, 24 | displayOptions: { 25 | showFungible: true 26 | } 27 | } 28 | }), 29 | }); 30 | 31 | if (!response.ok) { 32 | throw new Error(`HTTP error! status: ${response.status}`); 33 | } 34 | 35 | const data = await response.json(); 36 | console.log('Helius raw response:', data); 37 | 38 | if (data.error) { 39 | throw new Error(`Helius API error: ${data.error.message}`); 40 | } 41 | 42 | const asset = data.result; 43 | if (!asset) { 44 | console.log('No asset data in Helius response'); 45 | return undefined; 46 | } 47 | 48 | // Extract token info from Helius response 49 | const tokenInfo = { 50 | symbol: asset.symbol || '???', 51 | name: asset.name || 'Unknown Token', 52 | imageUrl: asset.image || '', 53 | price: 0, // Will be updated if price info exists 54 | marketCap: 0, // Will be updated if we can calculate it 55 | createdTimestamp: asset.created_at ? Math.floor(new Date(asset.created_at).getTime() / 1000) : Math.floor(Date.now() / 1000) 56 | }; 57 | 58 | // Try to get price information 59 | if (asset.price_info) { 60 | tokenInfo.price = asset.price_info.price_per_token || 0; 61 | 62 | // Calculate market cap if we have supply info 63 | if (asset.supply && asset.decimals !== undefined) { 64 | const adjustedSupply = Number(asset.supply) / Math.pow(10, asset.decimals); 65 | tokenInfo.marketCap = tokenInfo.price * adjustedSupply; 66 | } 67 | } 68 | 69 | console.log('Processed token info:', tokenInfo); 70 | return tokenInfo; 71 | } catch (error) { 72 | console.error('Error fetching asset:', error); 73 | if (error instanceof Error) { 74 | console.error('Error details:', error.message); 75 | console.error('Stack trace:', error.stack); 76 | } 77 | return undefined; 78 | } 79 | } 80 | 81 | async searchAssets(ownerAddress: string): Promise { 82 | try { 83 | const response = await fetch(this.rpcUrl, { 84 | method: 'POST', 85 | headers: { 86 | 'Content-Type': 'application/json', 87 | }, 88 | body: JSON.stringify({ 89 | jsonrpc: '2.0', 90 | id: 'helius-test', 91 | method: 'searchAssets', 92 | params: { 93 | ownerAddress, 94 | tokenType: "fungible" 95 | } 96 | }), 97 | }); 98 | 99 | if (!response.ok) { 100 | throw new Error(`HTTP error! status: ${response.status}`); 101 | } 102 | 103 | const data = await response.json(); 104 | if (data.error) { 105 | throw new Error(data.error.message); 106 | } 107 | 108 | return data.result.items 109 | .filter((asset: any) => asset.id.toLowerCase().endsWith('pump')) 110 | .map((asset: any) => { 111 | const pricePerToken = asset.token_info?.price_info?.price_per_token; 112 | const supply = asset.token_info?.supply || 0; 113 | const decimals = asset.token_info?.decimals || 0; 114 | const adjustedSupply = supply / Math.pow(10, decimals); 115 | const marketCap = pricePerToken && supply ? pricePerToken * adjustedSupply : 0; 116 | 117 | return { 118 | symbol: asset.content?.metadata?.symbol || '???', 119 | name: asset.content?.metadata?.name || 'Unknown Token', 120 | imageUrl: asset.content?.links?.image || '', 121 | price: pricePerToken, 122 | marketCap, 123 | createdTimestamp: asset.created_at ? Math.floor(new Date(asset.created_at).getTime() / 1000) : Math.floor(Date.now() / 1000) 124 | }; 125 | }); 126 | } catch (error) { 127 | console.error('Error searching assets:', error); 128 | return []; 129 | } 130 | } 131 | 132 | async getPairTokenInfo(pairId: string): Promise { 133 | try { 134 | console.log('Fetching pair info from Dexscreener:', pairId); 135 | const response = await fetch(`https://api.dexscreener.com/latest/dex/pairs/solana/${pairId}`); 136 | 137 | if (!response.ok) { 138 | throw new Error(`Dexscreener API error! status: ${response.status}`); 139 | } 140 | 141 | const data = await response.json(); 142 | console.log('Dexscreener response:', data); 143 | 144 | if (!data.pair) { 145 | console.log('No pair data found in Dexscreener response'); 146 | return undefined; 147 | } 148 | 149 | const { pair } = data; 150 | 151 | return { 152 | symbol: pair.baseToken.symbol || '???', 153 | name: pair.baseToken.name || 'Unknown Token', 154 | imageUrl: pair.info?.imageUrl || '', 155 | price: parseFloat(pair.priceUsd) || 0, 156 | marketCap: pair.marketCap || 0, 157 | createdTimestamp: Math.floor(pair.pairCreatedAt / 1000), // Convert from milliseconds to seconds 158 | mintAddress: pair.baseToken.address // Store the mint address for later use in Jupiter 159 | }; 160 | } catch (error) { 161 | console.error('Error fetching pair info:', error); 162 | if (error instanceof Error) { 163 | console.error('Error details:', error.message); 164 | console.error('Stack trace:', error.stack); 165 | } 166 | return undefined; 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/services/twitterService.ts: -------------------------------------------------------------------------------- 1 | import { Tweet } from '../types'; 2 | 3 | export type TweetType = 'pumpfun' | 'dexscreener'; 4 | 5 | interface TwitterUser { 6 | id: number; 7 | id_str: string; 8 | name: string; 9 | screen_name: string; 10 | profile_image_url_https: string; 11 | followers_count: number; 12 | verified: boolean; 13 | } 14 | 15 | interface TwitterEntities { 16 | urls: Array<{ 17 | display_url: string; 18 | expanded_url: string; 19 | indices: number[]; 20 | url: string; 21 | }>; 22 | media?: Array<{ 23 | display_url: string; 24 | expanded_url: string; 25 | media_url_https: string; 26 | type: string; 27 | url: string; 28 | }>; 29 | } 30 | 31 | interface RawTweet { 32 | created_at: string; 33 | id: number; 34 | id_str: string; 35 | text: string | null; 36 | full_text: string; 37 | user: TwitterUser; 38 | entities: TwitterEntities; 39 | quoted_status?: RawTweet; 40 | retweet_count: number; 41 | favorite_count: number; 42 | views_count: number | null; 43 | bookmark_count: number | null; 44 | tweet_created_at: string; 45 | mint_address?: string; 46 | token_info?: { 47 | symbol: string; 48 | name: string; 49 | image_url: string; 50 | price: number; 51 | market_cap: number; 52 | created_timestamp: number; 53 | }; 54 | } 55 | 56 | interface WebSocketMessage { 57 | type: 'tweets'; 58 | queryType: TweetType; 59 | data: RawTweet[]; 60 | } 61 | 62 | interface SearchConfig { 63 | type: TweetType; 64 | urlPattern: string; 65 | } 66 | 67 | export class TwitterService { 68 | private static instance: TwitterService | null = null; 69 | private ws: WebSocket | null = null; 70 | private reconnectAttempts = 0; 71 | private readonly maxReconnectAttempts = 5; 72 | private readonly wsUrl = process.env.NEXT_PUBLIC_TWITTER_WS_URL; 73 | private subscribers: ((tweets: Tweet[], type: TweetType) => void)[] = []; 74 | private cachedTweets: { [key in TweetType]: Tweet[] } = { 75 | pumpfun: [], 76 | dexscreener: [] 77 | }; 78 | private searchConfigs: SearchConfig[]; 79 | 80 | private constructor() { 81 | this.searchConfigs = [ 82 | { 83 | type: 'pumpfun', 84 | urlPattern: 'pump.fun/coin/' 85 | }, 86 | { 87 | type: 'dexscreener', 88 | urlPattern: 'dexscreener.com/solana/' 89 | } 90 | ]; 91 | 92 | if (typeof window !== 'undefined') { 93 | this.connect(); 94 | } 95 | } 96 | 97 | public static getInstance(): TwitterService { 98 | if (!TwitterService.instance) { 99 | TwitterService.instance = new TwitterService(); 100 | } 101 | return TwitterService.instance; 102 | } 103 | 104 | private transformTweet(rawTweet: RawTweet): Tweet { 105 | // Handle null text content 106 | if (!rawTweet.text && !rawTweet.full_text) { 107 | console.log('Tweet has no text content, skipping transformation:', rawTweet.id_str); 108 | return { 109 | id: rawTweet.id_str, 110 | text: '', 111 | created_at: Date.now().toString(), 112 | user: rawTweet.user, 113 | entities: { 114 | urls: [] 115 | }, 116 | source_type: 'pumpfun', 117 | retweet_count: rawTweet.retweet_count, 118 | favorite_count: rawTweet.favorite_count, 119 | views_count: rawTweet.views_count ?? null, 120 | bookmark_count: rawTweet.bookmark_count ?? null, 121 | mintAddress: rawTweet.mint_address, 122 | tokenInfo: rawTweet.token_info ? { 123 | symbol: rawTweet.token_info.symbol, 124 | name: rawTweet.token_info.name, 125 | imageUrl: rawTweet.token_info.image_url, 126 | price: rawTweet.token_info.price, 127 | marketCap: rawTweet.token_info.market_cap, 128 | createdTimestamp: rawTweet.token_info.created_timestamp 129 | } : undefined 130 | }; 131 | } 132 | 133 | // Combine URLs from both entities.urls and entities.media 134 | const urls = [ 135 | ...(rawTweet.entities?.urls || []), 136 | ...(rawTweet.entities?.media || []) 137 | ].map(url => ({ 138 | display_url: url.display_url, 139 | expanded_url: url.expanded_url, 140 | url: url.url 141 | })); 142 | 143 | console.log('Transforming tweet:', rawTweet.id_str); 144 | 145 | console.log('Raw tweet timestamp:', rawTweet.tweet_created_at); 146 | // Parse the timestamp and convert to current timezone 147 | const createdAtMs = new Date(rawTweet.tweet_created_at?.replace('.000000Z', 'Z') || Date.now()).getTime(); 148 | console.log('Converted timestamp:', createdAtMs); 149 | 150 | const tweet: Tweet = { 151 | id: rawTweet.id_str, 152 | text: rawTweet.full_text || rawTweet.text || '', 153 | created_at: createdAtMs.toString(), 154 | user: { 155 | name: rawTweet.user.name, 156 | screen_name: rawTweet.user.screen_name, 157 | profile_image_url_https: rawTweet.user.profile_image_url_https, 158 | followers_count: rawTweet.user.followers_count, 159 | verified: rawTweet.user.verified 160 | }, 161 | entities: { 162 | urls: urls 163 | }, 164 | source_type: 'pumpfun', 165 | retweet_count: rawTweet.retweet_count, 166 | favorite_count: rawTweet.favorite_count, 167 | views_count: rawTweet.views_count ?? null, 168 | bookmark_count: rawTweet.bookmark_count ?? null, 169 | mintAddress: rawTweet.mint_address, 170 | tokenInfo: rawTweet.token_info ? { 171 | symbol: rawTweet.token_info.symbol, 172 | name: rawTweet.token_info.name, 173 | imageUrl: rawTweet.token_info.image_url, 174 | price: rawTweet.token_info.price, 175 | marketCap: rawTweet.token_info.market_cap, 176 | createdTimestamp: rawTweet.token_info.created_timestamp 177 | } : undefined 178 | }; 179 | 180 | if (rawTweet.quoted_status) { 181 | tweet.quoted_status = this.transformTweet(rawTweet.quoted_status); 182 | } 183 | 184 | console.log('Transformed tweet:', tweet); 185 | return tweet; 186 | } 187 | 188 | private connect() { 189 | if (this.ws?.readyState === WebSocket.OPEN) return; 190 | 191 | console.log('Connecting to tweet stream...'); 192 | this.ws = new WebSocket(this.wsUrl); 193 | this.setupEventHandlers(); 194 | } 195 | 196 | private setupEventHandlers() { 197 | if (!this.ws) return; 198 | 199 | this.ws.onopen = () => { 200 | console.log('Connected to tweet stream'); 201 | this.reconnectAttempts = 0; 202 | this.subscribeToAllTweets(); 203 | }; 204 | 205 | this.ws.onmessage = (event) => { 206 | try { 207 | console.log('Received WebSocket message:', event.data); 208 | const message = JSON.parse(event.data) as WebSocketMessage; 209 | 210 | if (message.type === 'tweets' && Array.isArray(message.data)) { 211 | console.log(`Processing ${message.data.length} tweets of type ${message.queryType}`); 212 | const tweets = message.data.map(tweet => { 213 | const transformedTweet = this.transformTweet(tweet); 214 | transformedTweet.source_type = message.queryType; 215 | return transformedTweet; 216 | }); 217 | 218 | // Cache tweets 219 | this.cachedTweets[message.queryType] = [...this.cachedTweets[message.queryType], ...tweets]; 220 | 221 | // Notify subscribers 222 | console.log('Notifying subscribers with processed tweets:', tweets); 223 | this.subscribers.forEach(callback => { 224 | callback(tweets, message.queryType); 225 | }); 226 | } 227 | } catch (error) { 228 | console.error('Error processing WebSocket message:', error); 229 | } 230 | }; 231 | 232 | this.ws.onclose = () => { 233 | console.log('WebSocket connection closed'); 234 | this.handleReconnect(); 235 | }; 236 | 237 | this.ws.onerror = (error) => { 238 | console.error('WebSocket error:', error); 239 | }; 240 | } 241 | 242 | private subscribeToAllTweets() { 243 | if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return; 244 | 245 | this.searchConfigs.forEach(config => { 246 | const subscription = { 247 | query: config.urlPattern, 248 | type: config.type 249 | }; 250 | console.log('Sent subscription:', subscription); 251 | this.ws?.send(JSON.stringify(subscription)); 252 | }); 253 | } 254 | 255 | private handleReconnect() { 256 | if (this.reconnectAttempts >= this.maxReconnectAttempts) { 257 | console.error('Max reconnection attempts reached'); 258 | return; 259 | } 260 | 261 | const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 10000); 262 | this.reconnectAttempts++; 263 | 264 | console.log(`Attempting to reconnect in ${delay}ms (attempt ${this.reconnectAttempts})`); 265 | setTimeout(() => this.connect(), delay); 266 | } 267 | 268 | public subscribe(callback: (tweets: Tweet[], type: TweetType) => void) { 269 | this.subscribers.push(callback); 270 | return () => this.subscribers = this.subscribers.filter(cb => cb !== callback); 271 | } 272 | 273 | public unsubscribe(callback: (tweets: Tweet[], type: TweetType) => void) { 274 | this.subscribers = this.subscribers.filter(cb => cb !== callback); 275 | } 276 | 277 | public getCachedTweets(type: TweetType): Tweet[] { 278 | return this.cachedTweets[type].sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()); 279 | } 280 | 281 | public disconnect() { 282 | if (this.ws) { 283 | this.ws.close(); 284 | this.ws = null; 285 | } 286 | this.subscribers = []; 287 | this.cachedTweets = { 288 | pumpfun: [], 289 | dexscreener: [] 290 | }; 291 | } 292 | 293 | public reconnect() { 294 | console.log('Forcing reconnection to tweet stream...'); 295 | if (this.ws) { 296 | this.ws.close(); 297 | } 298 | this.reconnectAttempts = 0; 299 | this.connect(); 300 | } 301 | } 302 | 303 | // Export singleton instance 304 | export const twitterService = TwitterService.getInstance(); 305 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | color-scheme: dark; 7 | } 8 | 9 | body { 10 | @apply bg-gray-900 text-gray-100; 11 | font-feature-settings: "rlig" 1, "calt" 1; 12 | } 13 | 14 | /* Custom scrollbar for dark theme */ 15 | ::-webkit-scrollbar { 16 | width: 8px; 17 | height: 8px; 18 | } 19 | 20 | ::-webkit-scrollbar-track { 21 | @apply bg-gray-800; 22 | } 23 | 24 | ::-webkit-scrollbar-thumb { 25 | @apply bg-gray-600 rounded-full; 26 | } 27 | 28 | ::-webkit-scrollbar-thumb:hover { 29 | @apply bg-gray-500; 30 | } 31 | 32 | /* Focus outline for better dark mode visibility */ 33 | *:focus { 34 | @apply outline-none ring-2 ring-yellow-500 ring-opacity-50; 35 | } 36 | 37 | /* Base button styles */ 38 | button { 39 | @apply transition-colors duration-200; 40 | } 41 | 42 | /* Links */ 43 | a { 44 | @apply transition-colors duration-200; 45 | } 46 | 47 | @layer utilities { 48 | /* Firefox */ 49 | .custom-scrollbar { 50 | scrollbar-width: thin; 51 | scrollbar-color: #4b5563 #1f2937; 52 | } 53 | 54 | /* Chrome, Edge, and Safari */ 55 | .custom-scrollbar::-webkit-scrollbar { 56 | width: 6px; 57 | } 58 | 59 | .custom-scrollbar::-webkit-scrollbar-track { 60 | background: #1f2937; 61 | border-radius: 3px; 62 | } 63 | 64 | .custom-scrollbar::-webkit-scrollbar-thumb { 65 | background-color: #4b5563; 66 | border-radius: 3px; 67 | } 68 | 69 | .custom-scrollbar::-webkit-scrollbar-thumb:hover { 70 | background-color: #6b7280; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface TokenInfo { 2 | symbol: string; 3 | name: string; 4 | imageUrl: string; 5 | price: number; 6 | marketCap: number; 7 | createdTimestamp: number; 8 | mintAddress?: string; // Optional since pump.fun tokens might not have it 9 | } 10 | 11 | export interface Tweet { 12 | id: string; 13 | text: string; 14 | created_at: string; 15 | user: { 16 | name: string; 17 | screen_name: string; 18 | profile_image_url_https: string; 19 | verified: boolean; 20 | followers_count: number; 21 | }; 22 | entities: { 23 | urls: Array<{ 24 | display_url: string; 25 | expanded_url: string; 26 | url: string; 27 | }>; 28 | }; 29 | retweet_count: number; 30 | favorite_count: number; 31 | views_count: number | null; 32 | bookmark_count: number | null; 33 | quoted_status?: Tweet; 34 | tokenInfo?: TokenInfo; 35 | mintAddress?: string; 36 | pricePerToken?: number; 37 | lastPriceCheck?: number; 38 | source_type?: 'pumpfun' | 'dexscreener'; 39 | } 40 | 41 | export interface VirtualReserves { 42 | virtualTokenReserves: bigint; 43 | virtualSolReserves: bigint; 44 | realTokenReserves: bigint; 45 | realSolReserves: bigint; 46 | tokenTotalSupply: bigint; 47 | complete: boolean; 48 | } 49 | 50 | export interface CoinData { 51 | mint: string; 52 | bondingCurve: string; 53 | associatedBondingCurve: string; 54 | virtualTokenReserves: number; 55 | virtualSolReserves: number; 56 | tokenTotalSupply: number; 57 | complete: boolean; 58 | } 59 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface TokenInfo { 2 | symbol: string; 3 | name: string; 4 | imageUrl: string; 5 | price: number; 6 | marketCap: number; 7 | createdTimestamp: number; 8 | } 9 | 10 | export interface Tweet { 11 | id: string; 12 | text: string; 13 | created_at: string; 14 | user: { 15 | name: string; 16 | screen_name: string; 17 | profile_image_url_https: string; 18 | followers_count: number; 19 | verified: boolean; 20 | }; 21 | entities: { 22 | urls: Array<{ 23 | display_url: string; 24 | expanded_url: string; 25 | url: string; 26 | }>; 27 | }; 28 | source_type: 'pumpfun' | 'dexscreener'; 29 | retweet_count: number; 30 | favorite_count: number; 31 | views_count?: number; 32 | bookmark_count?: number; 33 | quoted_status?: Tweet; 34 | mintAddress?: string; 35 | tokenInfo?: TokenInfo; 36 | pricePerToken?: number; 37 | lastPriceCheck?: number; 38 | } 39 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}', 5 | './src/components/**/*.{js,ts,jsx,tsx,mdx}', 6 | './src/app/**/*.{js,ts,jsx,tsx,mdx}', 7 | ], 8 | theme: { 9 | extend: {}, 10 | }, 11 | plugins: [ 12 | require('@tailwindcss/forms'), 13 | ], 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "lib": [ 5 | "es2020", 6 | "dom" 7 | ], 8 | "declaration": true, 9 | "outDir": "./dist", 10 | "rootDir": ".", 11 | "strict": true, 12 | "esModuleInterop": true, 13 | "skipLibCheck": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "moduleResolution": "node", 16 | "allowJs": true, 17 | "noEmit": true, 18 | "incremental": true, 19 | "resolveJsonModule": true, 20 | "isolatedModules": true, 21 | "jsx": "preserve", 22 | "module": "esnext", 23 | "baseUrl": ".", 24 | "paths": { 25 | "@/*": [ 26 | "./src/*" 27 | ] 28 | }, 29 | "plugins": [ 30 | { 31 | "name": "next" 32 | } 33 | ] 34 | }, 35 | "include": [ 36 | "next-env.d.ts", 37 | "**/*.ts", 38 | "**/*.tsx", 39 | ".next/types/**/*.ts" 40 | ], 41 | "exclude": [ 42 | "node_modules" 43 | ] 44 | } 45 | --------------------------------------------------------------------------------