├── 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 |
111 |
112 | 113 | 114 |
115 |
116 | 117 | 118 |
119 |
120 |
121 | Price: 122 |
123 | 124 | ~ 125 | 126 | 127 |
128 |
129 |
130 | Rarity: 131 |
132 | 133 | ~ 134 | 135 | 136 |
137 |
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 | --------------------------------------------------------------------------------