├── .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 |
75 |
76 | {/* Trading Settings - Compact version */}
77 |
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 |
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 |
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 |
14 | );
15 | }
16 | if (status === 'success') {
17 | return (
18 |
21 | );
22 | }
23 | return (
24 |
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 |
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 |
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 |

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 |
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 |
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 |
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 |
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 |

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 |
--------------------------------------------------------------------------------