├── jsconfig.json
├── public
├── robots.txt
├── favicon.ico
├── logo192.png
├── logo512.png
├── manifest.json
└── index.html
├── src
├── assets
│ ├── img
│ │ └── solana-logo.png
│ └── css
│ │ ├── index.css
│ │ └── pagination.css
├── setupTests.js
├── App.test.js
├── index.js
├── App.js
├── components
│ ├── NFTCard.jsx
│ ├── NFTDetail.jsx
│ └── Header.jsx
└── pages
│ ├── Home.jsx
│ └── Collection.jsx
├── .gitignore
├── package.json
├── README.md
└── bot.js
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "src"
4 | }
5 | }
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supercoolx/solana-nft-sniper/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supercoolx/solana-nft-sniper/HEAD/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supercoolx/solana-nft-sniper/HEAD/public/logo512.png
--------------------------------------------------------------------------------
/src/assets/img/solana-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supercoolx/solana-nft-sniper/HEAD/src/assets/img/solana-logo.png
--------------------------------------------------------------------------------
/src/assets/css/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: #303030;
3 | color: white;
4 | }
5 |
6 | select, .css-26l3qy-menu * {
7 | color: black
8 | }
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import App from './App';
3 |
4 | test('renders learn react link', () => {
5 | render();
6 | const linkElement = screen.getByText(/learn react/i);
7 | expect(linkElement).toBeInTheDocument();
8 | });
9 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from 'App';
4 | import 'assets/css/index.css';
5 |
6 | ReactDOM.render(
7 |
8 |
9 | ,
10 | document.getElementById('root')
11 | );
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 |
22 | test.js
23 |
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | tailwind.config.js
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Header from 'components/Header';
3 | import Home from 'pages/Home';
4 | import Collection from 'pages/Collection';
5 | import { BrowserRouter, Routes, Route } from 'react-router-dom';
6 |
7 | function App() {
8 | return (
9 |
10 |
11 |
12 | } />
13 | } />
14 |
15 |
16 | )
17 | }
18 |
19 | export default App;
20 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | React App
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/assets/css/pagination.css:
--------------------------------------------------------------------------------
1 | .rc-pagination{
2 | display: flex;
3 | gap: 10px;
4 | }
5 | .rc-pagination-item{
6 | padding: 3px 10px;
7 | border: 1px solid;
8 | cursor: pointer;
9 | }
10 | .rc-pagination-prev, .rc-pagination-next{
11 | padding: 3px 10px;
12 | border: 1px solid rgb(54, 230, 0);
13 | color: rgb(54, 230, 0);
14 | cursor: pointer;
15 | }
16 | .rc-pagination-item.rc-pagination-item-active{
17 | border: 2px solid rgb(54, 230, 0);
18 | cursor: default;
19 | }
20 | .rc-pagination-jump-prev, .rc-pagination-jump-next{
21 | padding: 3px 5px;
22 | cursor: pointer;
23 | }
24 | .rc-pagination-disabled, .rc-pagination-item-disabled{
25 | border: 1px solid #aaaaaa;
26 | color: #aaaaaa;
27 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "magic-eden-sniper",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@metaplex/js": "^4.12.0",
7 | "@solana/web3.js": "^1.44.3",
8 | "@testing-library/jest-dom": "^5.16.4",
9 | "@testing-library/react": "^12.1.4",
10 | "@testing-library/user-event": "^13.5.0",
11 | "axios": "^0.27.2",
12 | "react": "^17.0.2",
13 | "react-dom": "^17.0.2",
14 | "react-image-appear": "^1.3.26",
15 | "react-infinite-scroll-component": "^6.1.0",
16 | "react-router-dom": "^6.3.0",
17 | "react-scripts": "^4.0.3",
18 | "react-select": "^5.3.2",
19 | "wallet-connect-buttons": "^0.2.6"
20 | },
21 | "scripts": {
22 | "start": "react-scripts start",
23 | "build": "react-scripts build",
24 | "test": "react-scripts test",
25 | "eject": "react-scripts eject"
26 | },
27 | "eslintConfig": {
28 | "extends": [
29 | "react-app",
30 | "react-app/jest"
31 | ]
32 | },
33 | "browserslist": {
34 | "production": [
35 | ">0.2%",
36 | "not dead",
37 | "not op_mini all"
38 | ],
39 | "development": [
40 | "last 1 chrome version",
41 | "last 1 firefox version",
42 | "last 1 safari version"
43 | ]
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/components/NFTCard.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactImageAppear from 'react-image-appear';
3 |
4 | const NFTCard = ({data}) => {
5 | const onClick = () => {
6 | if (data.buy_link) window.open(data.buy_link);
7 | }
8 |
9 | return (
10 |
11 |
12 |
13 |
14 |
15 |
{data.name}
16 |
17 |
18 |
Rarity rank: {data.rarity_rank}
19 |
20 |
21 |
22 | )
23 | }
24 |
25 | export default NFTCard;
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Magic Eden NFT Sniper Bot.
2 |
3 | ```
4 |
5 | @@@@@@% @@@@@@@@@@@@@@@@@@@@@@@@
6 | @@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@(
7 | @@@@@@@@@@@@@ .@@@@@@@@@@@@@@
8 | @@@@@@@@@@@@@@@ @@@@@@@@ @@@@@@@@@
9 | @@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@
10 | @@@@@@@@ @@@@@@@@@@@@ /@@@@@@@@
11 | @@@@@@@@ @@@@@@@ @@@@@@@@#
12 | @@@@@@@@ @@@@@@@@
13 | @@@@@@@@ @@@@@@@@@@@@@@@@@@@@/
14 | @@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@
15 | @@@@@@ @@@@@@@@@@@@@@@@@@@@@@@
16 |
17 |
18 | ```
19 |
20 | ## Magic Eden Apis
21 |
22 | ```
23 | https://api-mainnet.magiceden.io/rpc/getListedNFTsByQueryLite?q={"$match":{"collectionSymbol":"kenoko"},"$skip":0,"$limit":20,"status":[]}
24 | https://api-mainnet.magiceden.io/rpc/getNFTByMintAddress/5K3UqhkWjuzLyVnXYmNzx1u19mhxpvozt1ybUBZ6pSHA?useRarity=true
25 | https://api-mainnet.magiceden.io/rpc/getNFTStatsByMintAddress/5K3UqhkWjuzLyVnXYmNzx1u19mhxpvozt1ybUBZ6pSHA
26 | https://api-mainnet.magiceden.io/rpc/getCollectionEscrowStats/solhellcats
27 | https://api-mainnet.magiceden.io/rpc/getListedNFTsByQueryLite?q={"$match":{"collectionSymbol":"degenerate_ape_academy","$text":{"$search":"ape"},"rarity.howrare":{"$exists":true}},"$sort":{"rarity.howrare.rank":1},"$skip":0,"$limit":20,"status":[]}
28 | ```
--------------------------------------------------------------------------------
/src/components/NFTDetail.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 | import ReactImageAppear from 'react-image-appear';
4 |
5 | const style = {
6 | display: '-webkit-box',
7 | overflow: 'hidden',
8 | WebkitBoxOrient: 'vertical',
9 | WebkitLineClamp: 1
10 | }
11 |
12 | const NFTDetail = ({ collection }) => {
13 | if(!collection) return null;
14 | return (
15 |
16 |
17 |
18 |
19 |
20 |
{collection.name}
21 |
Floor price: @{collection.floor_price / 1000000000}
22 |
Volume: @{collection.volume.toLocaleString()}
23 |
Listed: {collection.listed_count}
24 |
25 |
26 | )
27 | }
28 |
29 | export default NFTDetail;
30 |
--------------------------------------------------------------------------------
/src/pages/Home.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { useSearchParams } from 'react-router-dom';
3 | import axios from 'axios';
4 | import NFTDetail from 'components/NFTDetail';
5 | import InfiniteScroll from 'react-infinite-scroll-component';
6 |
7 | function Home() {
8 | const pageSize = 20;
9 | const [collections, setCollections] = useState([]);
10 | const [page, setPage] = useState(1);
11 | const [hasMore, setHasMore] = useState(true);
12 | const [params] = useSearchParams();
13 |
14 | const fetch = () => {
15 | axios.get(`https://api.coralcube.io/v1/getCollections?offset=${page * pageSize}&page_size=${pageSize}&name=${params.get('q') || ''}`)
16 | .then(res => {
17 | setCollections(collections.concat(res.data));
18 | setPage(page + 1);
19 | res.data.length < pageSize && setHasMore(false);
20 | })
21 | .catch(console.error);
22 | }
23 |
24 | useEffect(() => {
25 | setCollections([]);
26 | setPage(1);
27 | setHasMore(true);
28 | axios.get(`https://api.coralcube.io/v1/getCollections?offset=0&page_size=${pageSize}&name=${params.get('q') || ''}`)
29 | .then(res => {
30 | setCollections(res.data);
31 | res.data.length < pageSize && setHasMore(false);
32 | })
33 | .catch(console.error);
34 | }, [params]);
35 |
36 | return (
37 |
38 |
39 | {
40 | collections.map((col, key) => )
41 | }
42 |
43 |
44 | );
45 | }
46 |
47 | export default Home;
48 |
--------------------------------------------------------------------------------
/bot.js:
--------------------------------------------------------------------------------
1 | const WebSocket = require('ws');
2 | const Event = require('events');
3 | const axios = require('axios');
4 | const solanaWeb3 = require('@solana/web3.js');
5 | const { Connection, programs } = require('@metaplex/js');
6 |
7 | const magicEdenAddress = 'MEisE1HzehtrDpAAT8PnLHjpSSkRYakotTuJRPjTpo8';
8 | const httpUrl = solanaWeb3.clusterApiUrl('mainnet-beta');
9 | const wsUrl = 'wss://api.mainnet-beta.solana.com/';
10 |
11 | const solanaConnection = new solanaWeb3.Connection(httpUrl, 'confirmed');
12 | const metaplexConnection = new Connection('mainnet-beta');
13 | const Metadata = programs.metadata.Metadata;
14 |
15 | var ws;
16 | const event = new Event();
17 |
18 | const monitor = () => {
19 | ws = new WebSocket(wsUrl);
20 | ws.onopen = () => {
21 | ws.send(JSON.stringify({
22 | jsonrpc: '2.0',
23 | id: 1,
24 | method: 'programSubscribe',
25 | params: [
26 | magicEdenAddress,
27 | {
28 | encoding: 'base64',
29 | commitment: 'processed'
30 | }
31 | ]
32 | }));
33 | }
34 | ws.on('message', (event) => {
35 | try {
36 | var data = JSON.parse(event).params.result;
37 | data.slot = JSON.parse(event).params.result.context.slot;
38 | event.emit('task', data);
39 | }
40 | catch (e) {
41 | console.log('Error:', e.message);
42 | }
43 | });
44 | ws.onclose = () => {
45 | console.log('Closed.');
46 | }
47 | return ws;
48 | }
49 |
50 | const getMetadata = async (tokenPubKey) => {
51 | try {
52 | const addr = await Metadata.getPDA(tokenPubKey)
53 | const resp = await Metadata.load(metaplexConnection, addr);
54 | const { data } = await axios.get(resp.data.data.uri);
55 |
56 | return data;
57 | } catch (error) {
58 | console.log("error fetching metadata: ", error)
59 | }
60 | }
61 |
62 | monitor();
--------------------------------------------------------------------------------
/src/components/Header.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react';
2 | import { Link, useNavigate } from 'react-router-dom';
3 | import { PhantomButton } from 'wallet-connect-buttons';
4 | import logo from 'assets/img/solana-logo.png';
5 |
6 | const Header = () => {
7 | const navigate = useNavigate();
8 | const inputDom = useRef();
9 | const [query, setQuery] = useState('');
10 | const [publicKey, setPublicKey] = useState('');
11 |
12 | const onChange = e => setQuery(e.target.value);
13 | const onKeyDown = e => {
14 | e.stopPropagation();
15 | if (e.keyCode === 13) {
16 | if (e.target.value.trim()) navigate('/?q=' + e.target.value.trim());
17 | else navigate('/');
18 | }
19 | }
20 |
21 | useEffect(() => {
22 | window.addEventListener('keydown', (e) => {
23 | if (e.keyCode === 191) {
24 | e.preventDefault();
25 | inputDom.current.focus();
26 | }
27 | })
28 | return () => window.removeEventListener('keydown', null);
29 | }, []);
30 |
31 | return (
32 |
33 |
34 |

35 |
36 |
37 |
38 |
Search:
39 |
40 |
/
41 |
42 | {
43 | publicKey ?
44 |
{publicKey.slice(0, 4)}...{publicKey.slice(-4)}
:
45 |
46 | }
47 |
48 |
49 | )
50 | }
51 |
52 | export default Header;
--------------------------------------------------------------------------------
/src/pages/Collection.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import axios from 'axios';
3 | import NFTCard from 'components/NFTCard';
4 | import { useParams } from 'react-router-dom';
5 | import InfiniteScroll from 'react-infinite-scroll-component';
6 |
7 | const Collection = () => {
8 | const pageSize = 20;
9 | const { symbol } = useParams();
10 | const [collection, setCollection] = useState(null);
11 | const [items, setItems] = useState([]);
12 | const [filter, setFilter] = useState({price_range:{currency:'sol'},rarity_range:{},traits:{},listing_status:[]});
13 | const [trait, setTrait] = useState({});
14 | const [order, setOrder] = useState('price_asc');
15 | const [attr, setAttr] = useState('');
16 | const [page, setPage] = useState(1);
17 | const [hasMore, setHasMore] = useState(true);
18 |
19 | const onChangeAttribute = e => setAttr(e.target.value);
20 | const onClickListed = e => {
21 | setFilter(filter => {
22 | let _filter = Object.assign({}, filter);
23 | let index = _filter.listing_status.indexOf(e.target.value);
24 | if (!e.target.checked && index > -1) _filter.listing_status.splice(index, 1);
25 | if (e.target.checked) _filter.listing_status.push(e.target.value);
26 | return _filter;
27 | });
28 | }
29 | const onChangePriceRange = (e) => {
30 | e.preventDefault();
31 | setFilter(filter => {
32 | let _filter = Object.assign({}, filter);
33 | e.target.min_price.value ? _filter.price_range.min_price = e.target.min_price.value : delete _filter.price_range.min_price;
34 | e.target.max_price.value ? _filter.price_range.max_price = e.target.max_price.value : delete _filter.price_range.max_price;
35 | return _filter;
36 | });
37 | }
38 | const onChangeRarityRange = (e) => {
39 | e.preventDefault();
40 | setFilter(filter => {
41 | let _filter = Object.assign({}, filter);
42 | e.target.min.value ? _filter.rarity_range.min = e.target.min.value : delete _filter.rarity_range.min;
43 | e.target.max.value ? _filter.rarity_range.max = e.target.max.value : delete _filter.rarity_range.max;
44 | return _filter;
45 | });
46 | }
47 | const changeTraits = (key) => {
48 | return (e) => {
49 | let _filter = Object.assign({}, filter);
50 | if (e.target.checked) {
51 | if (!_filter.traits[key]) _filter.traits[key] = [];
52 | _filter.traits[key].push(e.target.value);
53 | }
54 | else {
55 | let index = _filter.traits[key].indexOf(e.target.value);
56 | index > -1 && _filter.traits[key].splice(index, 1);
57 | !_filter.traits[key].length && delete _filter.traits[key];
58 | }
59 | setFilter(_filter);
60 | }
61 | }
62 | const onChangeOrder = (e) => setOrder(e.target.value);
63 | const fetch = () => {
64 | axios.post(`https://api.coralcube.io/v1/getItems?offset=${page * pageSize}&page_size=${pageSize}&ranking=${order}&symbol=${symbol}`, filter)
65 | .then(res => {
66 | setItems(items.concat(res.data.items));
67 | setPage(page + 1);
68 | res.data.items.length < pageSize && setHasMore(false);
69 | })
70 | .catch(console.error);
71 | }
72 |
73 | useEffect(() => {
74 | axios.get(`https://api.coralcube.io/v1/getCollectionAttributes?symbol=${symbol}`)
75 | .then(res => {
76 | setTrait(res.data.schema.properties.traits.properties);
77 | })
78 | .catch(() => setTrait({}));
79 | }, [symbol]);
80 | useEffect(() => {
81 | setItems([]);
82 | setPage(1);
83 | setHasMore(true);
84 | axios.post(`https://api.coralcube.io/v1/getItems?offset=0&page_size=${pageSize}&ranking=${order}&symbol=${symbol}`, filter)
85 | .then(res => {
86 | setCollection(res.data.collection);
87 | setItems(res.data.items);
88 | res.data.items.length < pageSize && setHasMore(false);
89 | })
90 | .catch(() => setCollection({}));
91 | }, [filter, order]);
92 |
93 | if (!collection) return Loading..
94 |
95 | return (
96 |
97 |
98 |
99 |

100 |
101 |
{collection.name}
102 | {collection.website &&
{collection.website}}
103 |
Floor Price: {collection.floor_price / 1000000000 || '---'} SOL
104 |
Listed Count: {collection.listed_count}
105 |
Total Count: {collection.total_count}
106 |
{collection.description}
107 |
108 |
109 |
110 |
120 |
129 |
138 |
139 |
143 |
144 | {
145 | attr && Object.keys(trait[attr].trait_count).map(key => (
146 |
150 | ))
151 | }
152 |
153 |
154 |
155 |
156 |
157 |
158 |
165 |
166 |
167 | { items.map((item, key) => ) }
168 |
169 |
170 |
171 | )
172 | }
173 |
174 | export default Collection;
175 |
--------------------------------------------------------------------------------