├── .gitignore ├── README.md ├── package.json └── src └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Magic Eden listings bot 2 | 3 | ## Purpose 4 | 5 | Fetching listings to Magic Eden marketplace 6 | 7 | ## Installation 8 | 9 | ```bash 10 | npm i 11 | node src/index.js 12 | ``` 13 | 14 | ## To-do 15 | 16 | - [ ] compare with similar NFTs from the same collection to benefit from mispricing 17 | - [ ] buy directly from web3 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "magic-eden-listings", 3 | "version": "0.1.0", 4 | "description": "Fetch new listings on-chain from Magic Eden", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@metaplex/js": "^4.7.0", 13 | "@project-serum/anchor": "^0.18.2", 14 | "@solana/web3.js": "^1.31.0", 15 | "axios": "^0.24.0", 16 | "bs58": "^4.0.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const anchor = require("@project-serum/anchor"); 2 | const solanaWeb3 = require("@solana/web3.js"); 3 | const axios = require("axios"); 4 | const bs58 = require("bs58"); 5 | const { programs } = require("@metaplex/js"); 6 | const { 7 | metadata: { Metadata }, 8 | } = programs; 9 | 10 | /* if (!process.env.PROJECT_ADDRESS || !process.env.DISCORD_URL) { 11 | console.log("please set your environment variables!"); 12 | return; 13 | } */ 14 | 15 | const updateAuthorities = { 16 | "GenoS3ck8xbDvYEZ8RxMG3Ln2qcyoAN8CTeZuaWgAoEA": "Genopets", 17 | "3pMvTLUA9NzZQd4gi725p89mvND1wRNQM3C8XEv1hTdA": "Famous Fox Federation", 18 | "976smoW7LjLZgzpj3UmVNmHUzbVHJUyLKvp9uqTFtZnp": "Fenix Danjon", 19 | "EmdsWm9dJ1d6BgQzHDcMJkDvB5SVvpfrAtpiGMVW1gxx": "Portals", 20 | "aury7LJUae7a92PBo35vVbP61GX8VbyxFKausvUtBrt": "Aurory" 21 | }; 22 | 23 | //pour les sales l'authority des fox suffit --> logique y'a pas de cession lors d'un listing 24 | const magicEdenKey = new anchor.web3.PublicKey( 25 | "MEisE1HzehtrDpAAT8PnLHjpSSkRYakotTuJRPjTpo8" 26 | ); 27 | 28 | const anchorConnection = new anchor.web3.Connection( 29 | "https://api.mainnet-beta.solana.com" 30 | ); 31 | 32 | const signaturesPollingInterval = 3000; // ms 33 | const signaturePollingInterval = 1300; // ms 34 | 35 | const runSalesBot = async () => { 36 | console.log("starting listing bot..."); 37 | 38 | const options = { limit: 1000 }; 39 | let signatures; 40 | 41 | while (true) { 42 | try { 43 | signatures = await anchorConnection.getSignaturesForAddress( 44 | magicEdenKey, 45 | options 46 | ); 47 | if (!signatures.length) { 48 | console.log("polling..."); 49 | await timer(signaturesPollingInterval); 50 | continue; 51 | } 52 | } catch (err) { 53 | console.log("error fetching signatures: ", err); 54 | continue; 55 | } 56 | 57 | for (let signature of signatures) { 58 | try { 59 | signature = signature.signature; 60 | const tx = await anchorConnection.getTransaction(signature); 61 | 62 | if (tx.meta == null && tx.meta.err != null) { 63 | continue; 64 | } 65 | 66 | //get listing price 67 | const listingPriceData = tx.transaction.message.instructions[0].data; 68 | const listingPriceHex = bs58.decode(listingPriceData).toString("hex"); 69 | 70 | if (listingPriceHex.length !== 34) { 71 | continue; 72 | } 73 | 74 | const reversedHex = reverseHex(listingPriceHex); 75 | 76 | //listing date 77 | const dateString = new Date(tx.blockTime * 1000).toLocaleString(); 78 | 79 | const listingPrice = 80 | parseInt(reversedHex, 16) / solanaWeb3.LAMPORTS_PER_SOL; 81 | 82 | //get token metadata 83 | try { 84 | const metadata = await getMetadata(tx.meta.postTokenBalances[0].mint); 85 | 86 | //get update authority 87 | const updateAuthority = metadata.data.updateAuthority; 88 | 89 | if (!updateAuthorities[updateAuthority]) { 90 | console.log("don't care tx") 91 | await timer(signaturePollingInterval); 92 | continue; 93 | } 94 | 95 | //get ipfs/arweave data 96 | const tokenInfo = await axios.get(metadata.data.data.uri); 97 | 98 | //get mint address 99 | const mintAddress = metadata.data.mint; 100 | 101 | printListingInfo( 102 | dateString, 103 | listingPrice, 104 | signature, 105 | metadata.data.data.name, 106 | updateAuthority, 107 | mintAddress, 108 | tokenInfo.data.image 109 | ); 110 | await postSaleToDiscord( 111 | metadata.data.data.name, 112 | listingPrice, 113 | dateString, 114 | mintAddress, 115 | tokenInfo.data.image 116 | ); 117 | } catch (err) { 118 | console.log("couldn't read metadata", err); 119 | } 120 | } catch (err) { 121 | console.log("error getting transaction: ", err); 122 | } 123 | 124 | lastKnownSignature = signatures[0].signature; 125 | 126 | if (lastKnownSignature) { 127 | options.until = lastKnownSignature; 128 | } 129 | await timer(signaturePollingInterval); 130 | } 131 | } 132 | }; 133 | 134 | runSalesBot(); 135 | 136 | const getMetadata = async (tokenPubKey) => { 137 | try { 138 | const addr = await Metadata.getPDA(tokenPubKey); 139 | const metadata = await Metadata.load(anchorConnection, addr); 140 | 141 | return metadata; 142 | } catch (error) { 143 | console.log("error fetching metadata: ", error); 144 | } 145 | }; 146 | 147 | const reverseHex = (hexString) => { 148 | hexString = hexString.slice(16, hexString.length - 2); 149 | var reversedHex = ""; 150 | for (let i = hexString.length; i >= 0; i = i - 2) { 151 | const tmp = hexString.substring(i - 2, i); 152 | reversedHex += tmp; 153 | } 154 | 155 | return reversedHex; 156 | }; 157 | 158 | const timer = (ms) => new Promise((res) => setTimeout(res, ms)); 159 | 160 | const printListingInfo = ( 161 | date, 162 | price, 163 | signature, 164 | title, 165 | updateAuthority, 166 | mintAddress, 167 | imageURL 168 | ) => { 169 | console.log("-------------------------------------------"); 170 | console.log(`Listing le ${date} ---> ${price} SOL`); 171 | console.log("Signature: ", signature); 172 | console.log("Name: ", title); 173 | console.log("Image: ", imageURL); 174 | console.log("Update Authority: ", updateAuthority); 175 | console.log("Mint address : ", mintAddress); 176 | }; 177 | 178 | const postSaleToDiscord = (title, price, date, mintAddress, imageURL) => { 179 | axios.post(process.env.DISCORD_BOT, { 180 | embeds: [ 181 | { 182 | title: `NEW LISTING`, 183 | description: `${title}`, 184 | fields: [ 185 | { 186 | name: "Price", 187 | value: `${price} SOL`, 188 | inline: true, 189 | }, 190 | { 191 | name: "Date", 192 | value: `${date}`, 193 | inline: true, 194 | }, 195 | { 196 | name: "Magic Eden Link", 197 | value: `https://magiceden.io/item-details/${mintAddress}`, 198 | }, 199 | ], 200 | image: { 201 | url: `${imageURL}`, 202 | }, 203 | }, 204 | ], 205 | }); 206 | }; 207 | --------------------------------------------------------------------------------