├── backend ├── .env.example ├── layers │ ├── Goo │ │ └── Green#1.png │ ├── Iris │ │ ├── Large#20.png │ │ ├── Small#60.png │ │ └── Medium#20.png │ ├── Background │ │ ├── Black.png │ │ ├── Blue.png │ │ ├── Orange.png │ │ └── Yellow.png │ ├── Eye color │ │ ├── Cyan#1.png │ │ ├── Pink#1.png │ │ ├── Red#1.png │ │ ├── Green#1.png │ │ ├── Purple#1.png │ │ └── Yellow#10.png │ ├── Eyeball │ │ ├── Red#50.png │ │ └── White#50.png │ ├── Shine │ │ └── Shapes#100.png │ ├── Top lid │ │ ├── High#30.png │ │ ├── Low#20.png │ │ └── Middle#50.png │ └── Bottom lid │ │ ├── Low#40.png │ │ ├── Middle#40.png │ │ └── High High #20.png ├── constants │ ├── network.js │ └── blend_mode.js ├── index.js ├── utils │ ├── nftport │ │ ├── genericMetas.js │ │ ├── uploadFiles.js │ │ ├── retrieveContract.js │ │ ├── uploadMetas.js │ │ ├── deployContract.js │ │ └── updateContract.js │ ├── update_info.js │ ├── functions │ │ ├── fetchWithRetry.js │ │ ├── rarity_rank.js │ │ ├── refreshOpensea.js │ │ └── getRarity_fromMetadata.js │ ├── preview.js │ ├── pixelate.js │ ├── rarity.js │ └── preview_gif.js ├── modules │ └── HashlipsGiffer.js ├── package.json └── src │ ├── config.js │ └── main.js ├── frontend ├── js │ ├── abi.js │ ├── countdown.js │ ├── constants.js │ ├── app.js │ └── splide.min.js ├── netlify.toml ├── .gitignore ├── images │ ├── slider │ │ ├── 0.png │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ ├── 6.png │ │ ├── 7.png │ │ ├── 8.png │ │ └── 9.png │ ├── header │ │ ├── discord.webp │ │ ├── opensea.webp │ │ └── twitter.webp │ └── x-icon │ │ ├── favicon.ico │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── apple-touch-icon.png │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ └── site.webmanifest ├── package.json ├── functions │ └── merkleProof.js ├── css │ ├── splide-core.min.css │ ├── themes │ │ ├── splide-skyblue.min.css │ │ ├── splide-default.min.css │ │ └── splide-sea-green.min.css │ ├── splide.min.css │ ├── style.css │ └── three-dots.min.css └── index.html ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ └── bug_report.yml └── FUNDING.yml ├── LICENSE ├── .gitignore └── README.md /backend/.env.example: -------------------------------------------------------------------------------- 1 | NFTPORT_API_KEY= -------------------------------------------------------------------------------- /frontend/js/abi.js: -------------------------------------------------------------------------------- 1 | const abi = [] 2 | -------------------------------------------------------------------------------- /frontend/netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | functions = "functions" -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | package-lock.json 4 | # Local Netlify folder 5 | .netlify -------------------------------------------------------------------------------- /backend/layers/Goo/Green#1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/backend/layers/Goo/Green#1.png -------------------------------------------------------------------------------- /frontend/images/slider/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/frontend/images/slider/0.png -------------------------------------------------------------------------------- /frontend/images/slider/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/frontend/images/slider/1.png -------------------------------------------------------------------------------- /frontend/images/slider/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/frontend/images/slider/2.png -------------------------------------------------------------------------------- /frontend/images/slider/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/frontend/images/slider/3.png -------------------------------------------------------------------------------- /frontend/images/slider/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/frontend/images/slider/4.png -------------------------------------------------------------------------------- /frontend/images/slider/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/frontend/images/slider/5.png -------------------------------------------------------------------------------- /frontend/images/slider/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/frontend/images/slider/6.png -------------------------------------------------------------------------------- /frontend/images/slider/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/frontend/images/slider/7.png -------------------------------------------------------------------------------- /frontend/images/slider/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/frontend/images/slider/8.png -------------------------------------------------------------------------------- /frontend/images/slider/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/frontend/images/slider/9.png -------------------------------------------------------------------------------- /backend/layers/Iris/Large#20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/backend/layers/Iris/Large#20.png -------------------------------------------------------------------------------- /backend/layers/Iris/Small#60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/backend/layers/Iris/Small#60.png -------------------------------------------------------------------------------- /backend/layers/Background/Black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/backend/layers/Background/Black.png -------------------------------------------------------------------------------- /backend/layers/Background/Blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/backend/layers/Background/Blue.png -------------------------------------------------------------------------------- /backend/layers/Eye color/Cyan#1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/backend/layers/Eye color/Cyan#1.png -------------------------------------------------------------------------------- /backend/layers/Eye color/Pink#1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/backend/layers/Eye color/Pink#1.png -------------------------------------------------------------------------------- /backend/layers/Eye color/Red#1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/backend/layers/Eye color/Red#1.png -------------------------------------------------------------------------------- /backend/layers/Eyeball/Red#50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/backend/layers/Eyeball/Red#50.png -------------------------------------------------------------------------------- /backend/layers/Eyeball/White#50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/backend/layers/Eyeball/White#50.png -------------------------------------------------------------------------------- /backend/layers/Iris/Medium#20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/backend/layers/Iris/Medium#20.png -------------------------------------------------------------------------------- /backend/layers/Shine/Shapes#100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/backend/layers/Shine/Shapes#100.png -------------------------------------------------------------------------------- /backend/layers/Top lid/High#30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/backend/layers/Top lid/High#30.png -------------------------------------------------------------------------------- /backend/layers/Top lid/Low#20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/backend/layers/Top lid/Low#20.png -------------------------------------------------------------------------------- /frontend/images/header/discord.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/frontend/images/header/discord.webp -------------------------------------------------------------------------------- /frontend/images/header/opensea.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/frontend/images/header/opensea.webp -------------------------------------------------------------------------------- /frontend/images/header/twitter.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/frontend/images/header/twitter.webp -------------------------------------------------------------------------------- /frontend/images/x-icon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/frontend/images/x-icon/favicon.ico -------------------------------------------------------------------------------- /backend/layers/Background/Orange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/backend/layers/Background/Orange.png -------------------------------------------------------------------------------- /backend/layers/Background/Yellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/backend/layers/Background/Yellow.png -------------------------------------------------------------------------------- /backend/layers/Bottom lid/Low#40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/backend/layers/Bottom lid/Low#40.png -------------------------------------------------------------------------------- /backend/layers/Eye color/Green#1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/backend/layers/Eye color/Green#1.png -------------------------------------------------------------------------------- /backend/layers/Eye color/Purple#1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/backend/layers/Eye color/Purple#1.png -------------------------------------------------------------------------------- /backend/layers/Top lid/Middle#50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/backend/layers/Top lid/Middle#50.png -------------------------------------------------------------------------------- /backend/layers/Bottom lid/Middle#40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/backend/layers/Bottom lid/Middle#40.png -------------------------------------------------------------------------------- /backend/layers/Eye color/Yellow#10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/backend/layers/Eye color/Yellow#10.png -------------------------------------------------------------------------------- /frontend/images/x-icon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/frontend/images/x-icon/favicon-16x16.png -------------------------------------------------------------------------------- /frontend/images/x-icon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/frontend/images/x-icon/favicon-32x32.png -------------------------------------------------------------------------------- /backend/constants/network.js: -------------------------------------------------------------------------------- 1 | const NETWORK = { 2 | eth: "eth", 3 | sol: "sol", 4 | }; 5 | 6 | module.exports = { 7 | NETWORK, 8 | }; 9 | -------------------------------------------------------------------------------- /backend/layers/Bottom lid/High High #20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/backend/layers/Bottom lid/High High #20.png -------------------------------------------------------------------------------- /frontend/images/x-icon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/frontend/images/x-icon/apple-touch-icon.png -------------------------------------------------------------------------------- /frontend/images/x-icon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/frontend/images/x-icon/android-chrome-192x192.png -------------------------------------------------------------------------------- /frontend/images/x-icon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeSTACKr/minter-dapp/HEAD/frontend/images/x-icon/android-chrome-512x512.png -------------------------------------------------------------------------------- /backend/index.js: -------------------------------------------------------------------------------- 1 | const basePath = process.cwd(); 2 | const { startCreating, buildSetup } = require(`${basePath}/src/main.js`); 3 | 4 | (() => { 5 | buildSetup(); 6 | startCreating(); 7 | })(); 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: NFT Community Support 4 | url: https://discord.gg/A9CnsVzzkZ 5 | about: Please ask and answer general, non-bug related questions here. 6 | -------------------------------------------------------------------------------- /frontend/images/x-icon/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nft-landing-page", 3 | "version": "1.1.0", 4 | "description": "Landing page to be used with an NFTPort Collection Contract", 5 | "main": "index.html", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/codeSTACKr/nft-landing-page.git" 12 | }, 13 | "keywords": [], 14 | "author": "Jesse Hall (codeSTACKr)", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/codeSTACKr/nft-landing-page/issues" 18 | }, 19 | "homepage": "https://github.com/codeSTACKr/nft-landing-page#readme", 20 | "dependencies": { 21 | "node-fetch": "^2.6.7" 22 | } 23 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: codeSTACKr 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /backend/constants/blend_mode.js: -------------------------------------------------------------------------------- 1 | const MODE = { 2 | sourceOver: "source-over", 3 | sourceIn: "source-in", 4 | sourceOut: "source-out", 5 | sourceAtop: "source-out", 6 | destinationOver: "destination-over", 7 | destinationIn: "destination-in", 8 | destinationOut: "destination-out", 9 | destinationAtop: "destination-atop", 10 | lighter: "lighter", 11 | copy: "copy", 12 | xor: "xor", 13 | multiply: "multiply", 14 | screen: "screen", 15 | overlay: "overlay", 16 | darken: "darken", 17 | lighten: "lighten", 18 | colorDodge: "color-dodge", 19 | colorBurn: "color-burn", 20 | hardLight: "hard-light", 21 | softLight: "soft-light", 22 | difference: "difference", 23 | exclusion: "exclusion", 24 | hue: "hue", 25 | saturation: "saturation", 26 | color: "color", 27 | luminosity: "luminosity", 28 | }; 29 | 30 | module.exports = { 31 | MODE, 32 | }; 33 | -------------------------------------------------------------------------------- /backend/utils/nftport/genericMetas.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const basePath = process.cwd(); 3 | const fs = require("fs"); 4 | const buildDir = path.join(basePath, "/build"); 5 | 6 | const { 7 | GENERIC_TITLE, 8 | GENERIC_DESCRIPTION, 9 | GENERIC_IMAGE, 10 | extraMetadata 11 | } = require(`${basePath}/src/config.js`); 12 | 13 | if (!fs.existsSync(path.join(buildDir, "/genericJson"))) { 14 | fs.mkdirSync(path.join(buildDir, "/genericJson")); 15 | } 16 | 17 | console.log("Starting generic metadata creation."); 18 | 19 | const genericObject = { 20 | "name": GENERIC_TITLE, 21 | "description": GENERIC_DESCRIPTION, 22 | "image": GENERIC_IMAGE, 23 | "external_url": extraMetadata.external_url || null, 24 | "date": 1647039293429, 25 | "compiler": "HashLips Art Engine - codeSTACKr Modified" 26 | } 27 | 28 | fs.writeFileSync( 29 | `${buildDir}/genericJson/_metadata.json`, 30 | JSON.stringify(genericObject, null, 2) 31 | ); 32 | 33 | console.log("Generic metadata created!"); -------------------------------------------------------------------------------- /frontend/js/countdown.js: -------------------------------------------------------------------------------- 1 | //Countdown Timer 2 | function countdown() { 3 | const clockdiv = document.getElementById("countdown"); 4 | const countDownTime = clockdiv.getAttribute("data-date") * 1000 5 | 6 | const countdownfunction = setInterval(function () { 7 | const now = new Date().getTime(); 8 | const diff = countDownTime - now; 9 | const days = Math.floor(diff / (1000 * 60 * 60 * 24)); 10 | const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); 11 | const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); 12 | const seconds = Math.floor((diff % (1000 * 60)) / 1000); 13 | 14 | if (diff < 0) { 15 | clockdiv.style.display = "none"; 16 | clearInterval(countdownfunction); 17 | } else { 18 | clockdiv.style.display = "block"; 19 | clockdiv.querySelector(".days").innerHTML = days; 20 | clockdiv.querySelector(".hours").innerHTML = hours; 21 | clockdiv.querySelector(".minutes").innerHTML = minutes; 22 | clockdiv.querySelector(".seconds").innerHTML = seconds; 23 | } 24 | }, 1000); 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 codeSTACKr 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /backend/modules/HashlipsGiffer.js: -------------------------------------------------------------------------------- 1 | const GifEncoder = require("gif-encoder-2"); 2 | const { writeFile } = require("fs"); 3 | 4 | class HashLipsGiffer { 5 | constructor(_canvas, _ctx, _fileName, _repeat, _quality, _delay) { 6 | this.canvas = _canvas; 7 | this.ctx = _ctx; 8 | this.fileName = _fileName; 9 | this.repeat = _repeat; 10 | this.quality = _quality; 11 | this.delay = _delay; 12 | this.initGifEncoder(); 13 | } 14 | 15 | initGifEncoder = () => { 16 | this.gifEncoder = new GifEncoder(this.canvas.width, this.canvas.height); 17 | this.gifEncoder.setQuality(this.quality); 18 | this.gifEncoder.setRepeat(this.repeat); 19 | this.gifEncoder.setDelay(this.delay); 20 | }; 21 | 22 | start = () => { 23 | this.gifEncoder.start(); 24 | }; 25 | 26 | add = () => { 27 | this.gifEncoder.addFrame(this.ctx); 28 | }; 29 | 30 | stop = () => { 31 | this.gifEncoder.finish(); 32 | const buffer = this.gifEncoder.out.getData(); 33 | writeFile(this.fileName, buffer, (error) => {}); 34 | console.log(`Created gif at ${this.fileName}`); 35 | }; 36 | } 37 | 38 | module.exports = HashLipsGiffer; 39 | -------------------------------------------------------------------------------- /frontend/functions/merkleProof.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch') 2 | 3 | const AUTH = process.env.NFTPORT_API_KEY; 4 | const include = "merkle_proofs"; 5 | 6 | exports.handler = async (event, context) => { 7 | const wallet = event.queryStringParameters && event.queryStringParameters.wallet 8 | const chain = event.queryStringParameters && event.queryStringParameters.chain 9 | const contract_address = event.queryStringParameters && event.queryStringParameters.contract 10 | const url = 'https://api.nftport.xyz/v0/me/contracts/collections?'; 11 | 12 | const options = { 13 | method: 'GET', 14 | headers: { 15 | 'Content-Type': 'application/json', 16 | Authorization: AUTH 17 | } 18 | }; 19 | const query = new URLSearchParams({ 20 | chain: chain, 21 | include 22 | }); 23 | 24 | const data = await fetch(url + query, options) 25 | const json = await data.json(); 26 | const contractInfo = json.contracts.filter(contract => contract.address.toLowerCase() === contract_address.toLowerCase()); 27 | const merkleProofs = contractInfo[0].merkle_proofs || {}; 28 | const merkleProof = merkleProofs[wallet.toLowerCase()] || []; 29 | 30 | return { 31 | 'statusCode': 200, 32 | 'headers': { 33 | 'Cache-Control': 'no-cache', 34 | 'Content-Type': 'application/json', 35 | }, 36 | 'body': JSON.stringify(merkleProof) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /backend/utils/update_info.js: -------------------------------------------------------------------------------- 1 | const basePath = process.cwd(); 2 | const fs = require("fs"); 3 | const yesno = require('yesno'); 4 | 5 | const { 6 | baseUri, 7 | description, 8 | namePrefix, 9 | } = require(`${basePath}/src/config.js`); 10 | 11 | (async () => { 12 | // read json data 13 | let rawdata = fs.readFileSync(`${basePath}/build/json/_metadata.json`); 14 | let data = JSON.parse(rawdata); 15 | 16 | console.log("Info will be updated using the config.js data."); 17 | const updateName = await yesno(`Update names?`); 18 | const updateDescription = await yesno(`Update descriptions?`); 19 | const updateBaseUri = await yesno(`Update images base URI?`); 20 | 21 | data.forEach((item) => { 22 | if(updateName) item.name = `${namePrefix} #${item.edition}`; 23 | if(updateDescription) item.description = description; 24 | if(updateBaseUri) item.image = `${baseUri}/${item.edition}.png`; 25 | 26 | fs.writeFileSync( 27 | `${basePath}/build/json/${item.edition}.json`, 28 | JSON.stringify(item, null, 2) 29 | ); 30 | }); 31 | 32 | fs.writeFileSync( 33 | `${basePath}/build/json/_metadata.json`, 34 | JSON.stringify(data, null, 2) 35 | ); 36 | 37 | if(updateName) console.log(`Updated name prefix for images to ===> ${namePrefix}`); 38 | if(updateBaseUri) console.log(`Updated baseUri for images to ===> ${baseUri}`); 39 | if(updateDescription) console.log(`Updated description for images to ===> ${description}`); 40 | })() 41 | -------------------------------------------------------------------------------- /frontend/js/constants.js: -------------------------------------------------------------------------------- 1 | const contractAddress = "YOUR CONTRACT ADDRESS"; // Replace with your own contract address 2 | const chain = 'goerli'; // goerli, polygon, or ethereum 3 | 4 | const welcome_h1 = "Welcome to the CodeCats NFT Project!!"; 5 | const welcome_h2 = "Connect to MetaMask to Get Started"; 6 | const welcome_p = 'The CodeCats NFT Project is a decentralized, open-source project that aims to demonstrate how to develope and launch your own NFT Collection. Follow step by step on the codeSTACKr YouTube channel.'; 7 | const h1_presale_coming_soon = "NFT Drop Coming Soon!!"; 8 | const h1_presale_mint = "Pre-Sale Minting Open!!"; 9 | const h1_public_mint = "Public Minting Open!!"; 10 | const h2_presale_coming_soon = "Pre-Sale Minting Countdown"; 11 | const h2_presale_mint = "Public Minting Countdown"; 12 | const p_presale_coming_soon = "We are working hard to launch the NFT Collection. Stay tuned for updates!"; 13 | const p_presale_mint_not_whitelisted = "You are not whitelisted for the pre-sale.. 😢"; 14 | const p_presale_mint_whitelisted = "You're on the whitelist for the pre-sale! 🎉"; 15 | const p_presale_mint_already_minted = "You've already claimed your whitelist mint. Thank you! 🎉"; 16 | const p_public_mint = "No whitelist needed. Public minting is now open! 🎉"; 17 | const button_presale_coming_soon = "Get on the Whitelist"; 18 | const button_presale_mint_whitelisted = "Mint Your Special NFT"; 19 | const button_presale_mint_not_whitelisted = "Get on the Whitelist"; 20 | const button_presale_already_minted = "Join The Community"; 21 | const button_public_mint = "Mint Your NFT"; 22 | const mint_failed = "Minting failed. 😢 Please try again."; 23 | -------------------------------------------------------------------------------- /backend/utils/functions/fetchWithRetry.js: -------------------------------------------------------------------------------- 1 | const basePath = process.cwd(); 2 | const fetch = require("node-fetch"); 3 | const { AUTH } = require(`${basePath}/src/config.js`); 4 | 5 | function fetchNoRetry(url, options) { 6 | return new Promise((resolve, reject) => { 7 | options.headers.Authorization = AUTH; 8 | 9 | fetch(url, options) 10 | .then((res) => { 11 | const status = res.status; 12 | 13 | if (status === 200) { 14 | return res.json(); 15 | } else { 16 | throw `ERROR STATUS: ${status}`; 17 | } 18 | }) 19 | .then((json) => { 20 | if (json.response === "OK") { 21 | return resolve(json); 22 | } else { 23 | throw `NOK: ${json.error}`; 24 | } 25 | }) 26 | .catch((error) => { 27 | console.error(`CATCH ERROR: ${error}`); 28 | }); 29 | }); 30 | } 31 | 32 | function fetchWithRetry(url, options) { 33 | return new Promise((resolve, reject) => { 34 | const fetch_retry = () => { 35 | options.headers.Authorization = AUTH; 36 | 37 | return fetch(url, options) 38 | .then((res) => { 39 | const status = res.status; 40 | 41 | if (status === 200) { 42 | return res.json(); 43 | } else { 44 | throw `ERROR STATUS: ${status}`; 45 | } 46 | }) 47 | .then((json) => { 48 | if (json.response === "OK") { 49 | return resolve(json); 50 | } else { 51 | throw `NOK: ${json.error}`; 52 | } 53 | }) 54 | .catch((error) => { 55 | console.error(`CATCH ERROR: ${error}`); 56 | console.log("Retrying"); 57 | fetch_retry(); 58 | }); 59 | }; 60 | return fetch_retry(); 61 | }); 62 | } 63 | 64 | module.exports = { fetchNoRetry, fetchWithRetry }; -------------------------------------------------------------------------------- /backend/utils/functions/rarity_rank.js: -------------------------------------------------------------------------------- 1 | const basePath = process.cwd(); 2 | const fs = require("fs"); 3 | 4 | // initialize readline to prompt user for input 5 | const readline = require("readline"); 6 | const rl = readline.createInterface({ 7 | input: process.stdin, 8 | output: process.stdout, 9 | }); 10 | const prompt = (query) => new Promise((resolve) => rl.question(query, resolve)); 11 | 12 | (async () => { 13 | try { 14 | // read json data 15 | const rawdata = fs.readFileSync( 16 | `${basePath}/build/json/_metadata_with_rarity.json` 17 | ); 18 | const nfts = JSON.parse(rawdata); 19 | 20 | // prompt user to choose how to list nfts 21 | // 1. get top ## nfts 22 | // 2. get a specific nft by edition 23 | const choice = await prompt( 24 | "Enter 1 to get top ## NFTs by rarity or 2 to get a specific NFTs rarity: " 25 | ); 26 | 27 | if (choice === "1") { 28 | const top = await prompt("Enter the number of NFTs you want to get: "); 29 | const sortedNfts = nfts.sort( 30 | (a, b) => b.total_rarity_score - a.total_rarity_score 31 | ); 32 | const topNfts = sortedNfts.slice(0, top); 33 | console.log( 34 | topNfts.map(({ rank, total_rarity_score, name }) => { 35 | return { 36 | name, 37 | rank, 38 | total_rarity_score, 39 | }; 40 | }) 41 | ); 42 | } else if (choice === "2") { 43 | const nftEdition = await prompt("Enter the NFT Edition: "); 44 | const nft = nfts.find((nft) => nft.custom_fields.edition === +nftEdition); 45 | console.log({ 46 | name: nft.name, 47 | rank: nft.rank, 48 | total_rarity_score: nft.total_rarity_score, 49 | }); 50 | } else { 51 | console.log("Invalid choice. Enter either 1 or 2."); 52 | } 53 | 54 | // close readline 55 | rl.close(); 56 | } catch (e) { 57 | console.error("unable to prompt", e); 58 | } 59 | })(); 60 | -------------------------------------------------------------------------------- /backend/utils/preview.js: -------------------------------------------------------------------------------- 1 | const basePath = process.cwd(); 2 | const fs = require("fs"); 3 | const { createCanvas, loadImage } = require("canvas"); 4 | const buildDir = `${basePath}/build`; 5 | 6 | const { preview } = require(`${basePath}/src/config.js`); 7 | 8 | // read json data 9 | const rawdata = fs.readFileSync(`${basePath}/build/json/_metadata.json`); 10 | const metadataList = JSON.parse(rawdata); 11 | 12 | const saveProjectPreviewImage = async (_data) => { 13 | // Extract from preview config 14 | const { thumbWidth, thumbPerRow, imageRatio, imageName } = preview; 15 | // Calculate height on the fly 16 | const thumbHeight = thumbWidth * imageRatio; 17 | // Prepare canvas 18 | const previewCanvasWidth = thumbWidth * thumbPerRow; 19 | const previewCanvasHeight = 20 | thumbHeight * Math.ceil(_data.length / thumbPerRow); 21 | // Shout from the mountain tops 22 | console.log( 23 | `Preparing a ${previewCanvasWidth}x${previewCanvasHeight} project preview with ${_data.length} thumbnails.` 24 | ); 25 | 26 | // Initiate the canvas now that we have calculated everything 27 | const previewPath = `${buildDir}/${imageName}`; 28 | const previewCanvas = createCanvas(previewCanvasWidth, previewCanvasHeight); 29 | const previewCtx = previewCanvas.getContext("2d"); 30 | 31 | // Iterate all NFTs and insert thumbnail into preview image 32 | // Don't want to rely on "edition" for assuming index 33 | for (let index = 0; index < _data.length; index++) { 34 | const nft = _data[index]; 35 | await loadImage(`${buildDir}/images/${nft.custom_fields.edition}.png`).then((image) => { 36 | previewCtx.drawImage( 37 | image, 38 | thumbWidth * (index % thumbPerRow), 39 | thumbHeight * Math.trunc(index / thumbPerRow), 40 | thumbWidth, 41 | thumbHeight 42 | ); 43 | }); 44 | } 45 | 46 | // Write Project Preview to file 47 | fs.writeFileSync(previewPath, previewCanvas.toBuffer("image/png")); 48 | console.log(`Project preview image located at: ${previewPath}`); 49 | }; 50 | 51 | saveProjectPreviewImage(metadataList); 52 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | title: "[Bug]: " 4 | labels: ["bug", "triage"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill out this bug report! 10 | - type: checkboxes 11 | id: version 12 | attributes: 13 | label: Version 14 | description: Are you using the latest version of this repository? 15 | options: 16 | - label: "Yes" 17 | required: true 18 | validations: 19 | required: true 20 | - type: checkboxes 21 | id: general 22 | attributes: 23 | label: General Question 24 | description: Is this a general usage question? If so, visit the [discord server](https://discord.gg/A9CnsVzzkZ). 25 | options: 26 | - label: "No" 27 | required: true 28 | validations: 29 | required: true 30 | - type: textarea 31 | id: what-happened 32 | attributes: 33 | label: What happened? 34 | description: Also tell us, what did you expect to happen? 35 | placeholder: Tell us what you see! 36 | value: "A bug happened!" 37 | validations: 38 | required: true 39 | - type: textarea 40 | id: troubleshoot 41 | attributes: 42 | label: What have you tried? 43 | description: What steps have you taken to find a solution? 44 | placeholder: Tell us what you've done! 45 | value: "I Googled and asked the community in the discord server but could not find a solution." 46 | validations: 47 | required: true 48 | - type: textarea 49 | id: logs 50 | attributes: 51 | label: Relevant log output 52 | description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. 53 | render: shell 54 | - type: checkboxes 55 | id: ppi 56 | attributes: 57 | label: Protect your information 58 | description: I have not included any personal information in this form. 59 | options: 60 | - label: "I Agree" 61 | required: true 62 | validations: 63 | required: true 64 | -------------------------------------------------------------------------------- /frontend/css/splide-core.min.css: -------------------------------------------------------------------------------- 1 | @keyframes splide-loading{0%{transform:rotate(0)}to{transform:rotate(1turn)}}.splide--draggable>.splide__slider>.splide__track,.splide--draggable>.splide__track{-webkit-user-select:none;-ms-user-select:none;user-select:none}.splide--fade>.splide__slider>.splide__track>.splide__list,.splide--fade>.splide__track>.splide__list{display:block}.splide--fade>.splide__slider>.splide__track>.splide__list>.splide__slide,.splide--fade>.splide__track>.splide__list>.splide__slide{left:0;opacity:0;position:absolute;top:0;z-index:0}.splide--fade>.splide__slider>.splide__track>.splide__list>.splide__slide.is-active,.splide--fade>.splide__track>.splide__list>.splide__slide.is-active{opacity:1;position:relative;z-index:1}.splide--rtl{direction:rtl}.splide--ttb.is-active>.splide__slider>.splide__track>.splide__list,.splide--ttb.is-active>.splide__track>.splide__list{display:block}.splide__container{box-sizing:border-box;position:relative}.splide__list{-webkit-backface-visibility:hidden;backface-visibility:hidden;display:-ms-flexbox;display:flex;height:100%;margin:0!important;padding:0!important;transform-style:preserve-3d}.splide.is-initialized:not(.is-active) .splide__list{display:block}.splide__pagination{-ms-flex-align:center;align-items:center;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:center;justify-content:center;margin:0;pointer-events:none}.splide__pagination li{display:inline-block;line-height:1;list-style-type:none;margin:0;pointer-events:auto}.splide__progress__bar{width:0}.splide{outline:none;position:relative;visibility:hidden}.splide.is-initialized,.splide.is-rendered{visibility:visible}.splide__slide{-webkit-backface-visibility:hidden;backface-visibility:hidden;box-sizing:border-box;-ms-flex-negative:0;flex-shrink:0;list-style-type:none!important;margin:0;outline:none;position:relative}.splide__slide img{vertical-align:bottom}.splide__slider{position:relative}.splide__spinner{animation:splide-loading 1s linear infinite;border:2px solid #999;border-left-color:transparent;border-radius:50%;bottom:0;contain:strict;display:inline-block;height:20px;left:0;margin:auto;position:absolute;right:0;top:0;width:20px}.splide__track{overflow:hidden;position:relative;z-index:0} -------------------------------------------------------------------------------- /backend/utils/nftport/uploadFiles.js: -------------------------------------------------------------------------------- 1 | const FormData = require("form-data"); 2 | const path = require("path"); 3 | const basePath = process.cwd(); 4 | const fs = require("fs"); 5 | 6 | const { RateLimit } = require('async-sema'); 7 | const { fetchWithRetry } = require(`${basePath}/utils/functions/fetchWithRetry.js`); 8 | 9 | const { LIMIT } = require(`${basePath}/src/config.js`); 10 | const _limit = RateLimit(LIMIT); 11 | 12 | const allMetadata = []; 13 | const regex = new RegExp("^([0-9]+).png"); 14 | 15 | async function main() { 16 | console.log("Starting upload of images..."); 17 | const files = fs.readdirSync(`${basePath}/build/images`); 18 | files.sort(function(a, b){ 19 | return a.split(".")[0] - b.split(".")[0]; 20 | }); 21 | for (const file of files) { 22 | try { 23 | if (regex.test(file)) { 24 | const fileName = path.parse(file).name; 25 | let jsonFile = fs.readFileSync(`${basePath}/build/json/${fileName}.json`); 26 | let metaData = JSON.parse(jsonFile); 27 | 28 | if(!metaData.image.includes('https://')) { 29 | await _limit() 30 | const url = "https://api.nftport.xyz/v0/files"; 31 | const formData = new FormData(); 32 | const fileStream = fs.createReadStream(`${basePath}/build/images/${file}`); 33 | formData.append("file", fileStream); 34 | const options = { 35 | method: "POST", 36 | headers: {}, 37 | body: formData, 38 | }; 39 | const response = await fetchWithRetry(url, options); 40 | metaData.image = response.ipfs_url; 41 | 42 | fs.writeFileSync( 43 | `${basePath}/build/json/${fileName}.json`, 44 | JSON.stringify(metaData, null, 2) 45 | ); 46 | console.log(`${response.file_name} uploaded & ${fileName}.json updated!`); 47 | } else { 48 | console.log(`${fileName} already uploaded.`); 49 | } 50 | 51 | allMetadata.push(metaData); 52 | } 53 | } catch (error) { 54 | console.log(`Catch: ${error}`); 55 | } 56 | } 57 | 58 | fs.writeFileSync( 59 | `${basePath}/build/json/_metadata.json`, 60 | JSON.stringify(allMetadata, null, 2) 61 | ); 62 | } 63 | 64 | main(); 65 | -------------------------------------------------------------------------------- /backend/utils/functions/refreshOpensea.js: -------------------------------------------------------------------------------- 1 | const basePath = process.cwd(); 2 | const puppeteer = require("puppeteer-extra"); 3 | const StealthPlugin = require("puppeteer-extra-plugin-stealth"); 4 | puppeteer.use(StealthPlugin()); 5 | const AdblockerPlugin = require("puppeteer-extra-plugin-adblocker"); 6 | puppeteer.use(AdblockerPlugin({ blockTrackers: true })); 7 | let [START, END] = process.argv.slice(2); 8 | const { CONTRACT_ADDRESS, CHAIN } = require(`${basePath}/src/config.js`); 9 | 10 | START = parseInt(START); 11 | END = parseInt(END); 12 | if (!START || !END) { 13 | console.log( 14 | "Please provide a start and end edition number. Example: npm run refresh_os --start 1 --end 10" 15 | ); 16 | process.exit(1); 17 | } 18 | 19 | const COLLECTION_BASE_URL = 20 | CHAIN.toLowerCase() === "goerli" 21 | ? `https://testnets.opensea.io/assets/goerli` 22 | : "https://opensea.io/assets/matic"; 23 | 24 | async function main() { 25 | const notFound = []; 26 | const errors = []; 27 | const browser = await puppeteer.launch({ 28 | headless: false, 29 | }); 30 | 31 | console.log(`Beginning OpenSea Refresh`); 32 | const page = await browser.newPage(); 33 | 34 | for (let i = START; i <= END; i++) { 35 | try { 36 | console.log(`Refreshing Edition: ${i}`); 37 | 38 | const url = `${COLLECTION_BASE_URL}/${CONTRACT_ADDRESS}/${i}`; 39 | 40 | await page.goto(url); 41 | 42 | await page.waitForSelector('button>div>i[value="refresh"]'); 43 | let pageTitle = await page.$$eval("title", (title) => 44 | title.map((title) => title.textContent) 45 | ); 46 | if (pageTitle[0].includes("Not Found")) { 47 | console.log(`Edition ${i} not found!`); 48 | notFound.push(i); 49 | } 50 | 51 | await page.click('button>div>i[value="refresh"]'); 52 | await page.waitForTimeout(5000); 53 | 54 | console.log(`Refreshed Edition: ${i}`); 55 | } catch (error) { 56 | console.log(`Error refreshing edition ${i}: ${error}`); 57 | errors.push(i); 58 | } 59 | } 60 | 61 | await browser.close(); 62 | 63 | if (notFound.length > 0 || errors.length > 0) { 64 | console.log(`Not Found: ${notFound}`); 65 | console.log(`Errors: ${errors}`); 66 | } 67 | console.log(`Finished OpenSea Refresh`); 68 | } 69 | 70 | main(); 71 | -------------------------------------------------------------------------------- /backend/utils/functions/getRarity_fromMetadata.js: -------------------------------------------------------------------------------- 1 | const basePath = process.cwd(); 2 | const fs = require("fs"); 3 | 4 | const getRarity = () => { 5 | // read json data 6 | const rawdata = fs.readFileSync(`${basePath}/build/json/_metadata.json`); 7 | const nfts = JSON.parse(rawdata); 8 | 9 | processRarity(nfts) 10 | } 11 | 12 | function processRarity(nfts) { 13 | const rarity = {} 14 | 15 | // loop through all nfts 16 | for(const nft of nfts) { 17 | // check if attributes exist 18 | if(nft?.attributes?.length > 0) { 19 | // loop through all attributes 20 | for(attribute of nft.attributes) { 21 | // add trait type to rarity object if it doesn't exist 22 | if(!rarity[attribute.trait_type]) { 23 | rarity[attribute.trait_type] = {} 24 | } 25 | // add attribute value to rarity object if it doesn't exist and set count to 0 26 | if(!rarity[attribute.trait_type][attribute.value]) { 27 | rarity[attribute.trait_type][attribute.value] = { 28 | count: 0 29 | } 30 | } 31 | // increment count of trait type 32 | rarity[attribute.trait_type][attribute.value].count++ 33 | // add rarity score to rarity object for each trait type 34 | rarity[attribute.trait_type][attribute.value].rarityScore = (1 / (rarity[attribute.trait_type][attribute.value].count / nfts.length)).toFixed(2) 35 | } 36 | } 37 | } 38 | 39 | // create a total rarity score for each nft by adding up all the rarity scores for each trait type 40 | nfts.map(nft => { 41 | if(nft?.attributes?.length > 0) { 42 | let totalScore = 0; 43 | for(attribute of nft.attributes) { 44 | attribute.rarity_score = rarity[attribute.trait_type][attribute.value].rarityScore 45 | totalScore += parseFloat(attribute.rarity_score) 46 | } 47 | nft.total_rarity_score = +parseFloat(totalScore).toFixed(2) 48 | } 49 | }) 50 | 51 | // sort nfts by total rarity score 52 | nfts.sort((a, b) => b.total_rarity_score - a.total_rarity_score) 53 | 54 | // add rank to nfts 55 | nfts.map((nft, index) => { 56 | nft.rank = index + 1 57 | }) 58 | 59 | // sort nfts by edition again 60 | nfts.sort((a, b) => a.custom_fields.edition - b.custom_fields.edition) 61 | 62 | fs.writeFileSync(`${basePath}/build/json/_metadata_with_rarity.json`, JSON.stringify(nfts, null, 2)); 63 | } 64 | 65 | getRarity(); 66 | -------------------------------------------------------------------------------- /backend/utils/nftport/retrieveContract.js: -------------------------------------------------------------------------------- 1 | const basePath = process.cwd(); 2 | const fs = require("fs"); 3 | 4 | const { 5 | fetchNoRetry, 6 | } = require(`${basePath}/utils/functions/fetchWithRetry.js`); 7 | const { CHAIN, CONTRACT_NAME } = require(`${basePath}/src/config.js`); 8 | 9 | const retrieveContract = async () => { 10 | try { 11 | const rawDeployData = fs.readFileSync( 12 | `${basePath}/build/contract/_deployContractResponse.json` 13 | ); 14 | const deployData = JSON.parse(rawDeployData); 15 | if (deployData.response === "OK") { 16 | const txnHash = deployData.transaction_hash; 17 | const chain = CHAIN.toLowerCase(); 18 | const url = `https://api.nftport.xyz/v0/contracts/${txnHash}?chain=${chain}`; 19 | const options = { 20 | method: "GET", 21 | headers: { 22 | "Content-Type": "application/json", 23 | }, 24 | }; 25 | const response = await fetchNoRetry(url, options); 26 | fs.writeFileSync( 27 | `${basePath}/build/contract/_contract.json`, 28 | JSON.stringify(response, null, 2) 29 | ); 30 | if (response.response === "OK") { 31 | console.log(`Contract ${CONTRACT_NAME} deployed successfully`); 32 | retrieveABI(response.contract_address); 33 | } else { 34 | console.log(`Contract ${CONTRACT_NAME} deployment failed`); 35 | } 36 | } else { 37 | console.log(`Contract ${CONTRACT_NAME} deployment failed`); 38 | } 39 | } catch (error) { 40 | console.log(`CATCH: Contract ${CONTRACT_NAME} deployment failed`, `ERROR: ${error}`); 41 | } 42 | }; 43 | 44 | const retrieveABI = async (contract_address) => { 45 | try { 46 | const chain = CHAIN.toLowerCase(); 47 | const url = `https://api.nftport.xyz/v0/me/contracts/abis/${contract_address}?chain=${chain}`; 48 | const options = { 49 | method: "GET", 50 | headers: { 51 | "Content-Type": "application/json", 52 | }, 53 | }; 54 | const response = await fetchNoRetry(url, options); 55 | fs.writeFileSync( 56 | `${basePath}/build/contract/_contract_abi.json`, 57 | JSON.stringify(response.abi, null, 2) 58 | ); 59 | if (response.response === "OK") { 60 | console.log(`Contract ${CONTRACT_NAME} ABI successful`); 61 | } else { 62 | console.log(`Contract ${CONTRACT_NAME} ABI failed`); 63 | } 64 | } catch (error) { 65 | console.log(`CATCH: Contract ${CONTRACT_NAME} ABI failed`, `ERROR: ${error}`); 66 | } 67 | }; 68 | 69 | retrieveContract(); 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | 3 | logs 4 | _.log 5 | npm-debug.log_ 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log\* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | 12 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 13 | 14 | # Runtime data 15 | 16 | pids 17 | _.pid 18 | _.seed 19 | \*.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | 27 | coverage 28 | \*.lcov 29 | 30 | # nyc test coverage 31 | 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 35 | 36 | .grunt 37 | 38 | # Bower dependency directory (https://bower.io/) 39 | 40 | bower_components 41 | 42 | # node-waf configuration 43 | 44 | .lock-wscript 45 | 46 | # Compiled binary addons (https://nodejs.org/api/addons.html) 47 | 48 | build/Release 49 | 50 | # Dependency directories 51 | 52 | build/ 53 | dist/ 54 | node_modules/ 55 | jspm_packages/ 56 | package-lock.json 57 | yarn.lock 58 | 59 | # TypeScript v1 declaration files 60 | 61 | typings/ 62 | 63 | # TypeScript cache 64 | 65 | \*.tsbuildinfo 66 | 67 | # Optional npm cache directory 68 | 69 | .npm 70 | 71 | # Optional eslint cache 72 | 73 | .eslintcache 74 | 75 | # Microbundle cache 76 | 77 | .rpt2_cache/ 78 | .rts2_cache_cjs/ 79 | .rts2_cache_es/ 80 | .rts2_cache_umd/ 81 | 82 | # Optional REPL history 83 | 84 | .node_repl_history 85 | 86 | # Output of 'npm pack' 87 | 88 | \*.tgz 89 | 90 | # Yarn Integrity file 91 | 92 | .yarn-integrity 93 | 94 | # dotenv environment variables file 95 | 96 | .env 97 | .env.test 98 | 99 | # parcel-bundler cache (https://parceljs.org/) 100 | 101 | .cache 102 | 103 | # Next.js build output 104 | 105 | .next 106 | 107 | # Nuxt.js build / generate output 108 | 109 | .nuxt 110 | dist 111 | 112 | # Gatsby files 113 | 114 | .cache/ 115 | 116 | # Comment in the public line in if your project uses Gatsby and _not_ Next.js 117 | 118 | # https://nextjs.org/blog/next-9-1#public-directory-support 119 | 120 | # public 121 | 122 | # vuepress build output 123 | 124 | .vuepress/dist 125 | 126 | # Serverless directories 127 | 128 | .serverless/ 129 | 130 | # FuseBox cache 131 | 132 | .fusebox/ 133 | 134 | # DynamoDB Local files 135 | 136 | .dynamodb/ 137 | 138 | # TernJS port file 139 | 140 | .tern-port 141 | 142 | # OSX 143 | 144 | .DS_Store 145 | 146 | build* 147 | # Local Netlify folder 148 | .netlify 149 | -------------------------------------------------------------------------------- /backend/utils/pixelate.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const { createCanvas, loadImage } = require("canvas"); 4 | const basePath = process.cwd(); 5 | const buildDir = `${basePath}/build/pixel_images`; 6 | const inputDir = `${basePath}/build/images`; 7 | const { format, pixelFormat } = require(`${basePath}/src/config.js`); 8 | const console = require("console"); 9 | const canvas = createCanvas(format.width, format.height); 10 | const ctx = canvas.getContext("2d"); 11 | 12 | const buildSetup = () => { 13 | if (fs.existsSync(buildDir)) { 14 | fs.rmdirSync(buildDir, { recursive: true }); 15 | } 16 | fs.mkdirSync(buildDir); 17 | }; 18 | 19 | const getImages = (_dir) => { 20 | try { 21 | return fs 22 | .readdirSync(_dir) 23 | .filter((item) => { 24 | let extension = path.extname(`${_dir}${item}`); 25 | if (extension == ".png" || extension == ".jpg") { 26 | return item; 27 | } 28 | }) 29 | .map((i) => { 30 | return { 31 | filename: i, 32 | path: `${_dir}/${i}`, 33 | }; 34 | }); 35 | } catch { 36 | return null; 37 | } 38 | }; 39 | 40 | const loadImgData = async (_imgObject) => { 41 | return new Promise(async (resolve) => { 42 | const image = await loadImage(`${_imgObject.path}`); 43 | resolve({ imgObject: _imgObject, loadedImage: image }); 44 | }); 45 | }; 46 | 47 | const draw = (_imgObject) => { 48 | let size = pixelFormat.ratio; 49 | let w = canvas.width * size; 50 | let h = canvas.height * size; 51 | ctx.imageSmoothingEnabled = false; 52 | ctx.drawImage(_imgObject.loadedImage, 0, 0, w, h); 53 | ctx.drawImage(canvas, 0, 0, w, h, 0, 0, canvas.width, canvas.height); 54 | }; 55 | 56 | const saveImage = (_loadedImageObject) => { 57 | fs.writeFileSync( 58 | `${buildDir}/${_loadedImageObject.imgObject.filename}`, 59 | canvas.toBuffer("image/png") 60 | ); 61 | }; 62 | 63 | const startCreating = async () => { 64 | const images = getImages(inputDir); 65 | if (images == null) { 66 | console.log("Please generate collection first."); 67 | return; 68 | } 69 | let loadedImageObjects = []; 70 | images.forEach((imgObject) => { 71 | loadedImageObjects.push(loadImgData(imgObject)); 72 | }); 73 | await Promise.all(loadedImageObjects).then((loadedImageObjectArray) => { 74 | loadedImageObjectArray.forEach((loadedImageObject) => { 75 | draw(loadedImageObject); 76 | saveImage(loadedImageObject); 77 | console.log(`Pixelated image: ${loadedImageObject.imgObject.filename}`); 78 | }); 79 | }); 80 | }; 81 | 82 | buildSetup(); 83 | startCreating(); 84 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "minter-dapp-backend", 3 | "version": "0.1.0", 4 | "description": "Generate an NFT collection and deploy a minting dapp easy!!", 5 | "main": "index.js", 6 | "bin": "index.js", 7 | "pkg": { 8 | "assets": [ 9 | "layers/**/*", 10 | "node_modules/**/*", 11 | "src/**/*" 12 | ] 13 | }, 14 | "scripts": { 15 | "build": "node index.js", 16 | "generate": "node index.js", 17 | "rarity": "node utils/rarity", 18 | "rarity_md": "node utils/functions/getRarity_fromMetadata", 19 | "rarity_rank": "node utils/functions/rarity_rank.js", 20 | "preview": "node utils/preview.js", 21 | "pixelate": "node utils/pixelate.js", 22 | "update_info": "node utils/update_info.js", 23 | "preview_gif": "node utils/preview_gif.js", 24 | "create_generic": "node utils/nftport/genericMetas", 25 | "upload_files": "node utils/nftport/uploadFiles", 26 | "upload_metadata": "node utils/nftport/uploadMetas", 27 | "deploy_contract": "node utils/nftport/deployContract", 28 | "get_contract": "node utils/nftport/retrieveContract", 29 | "update_public_mint_start_date": "node utils/nftport/updateContract.js -u public_mint_start_date", 30 | "update_presale_mint_start_date": "node utils/nftport/updateContract.js -u presale_mint_start_date", 31 | "update_presale_whitelisted_addresses": "node utils/nftport/updateContract.js -u presale_whitelisted_addresses", 32 | "update_presale_whitelisted_addresses_remove": "node utils/nftport/updateContract.js -u presale_whitelisted_addresses_remove", 33 | "update_royalty_share": "node utils/nftport/updateContract.js -u royalty_share", 34 | "update_royalty_address": "node utils/nftport/updateContract.js -u royalty_address", 35 | "update_base_uri": "node utils/nftport/updateContract.js -u base_uri", 36 | "update_prereveal_token_uri": "node utils/nftport/updateContract.js -u prereveal_token_uri", 37 | "⏬⏬⏬": "FOR MAC USERS: Replace %npm_config_xyz% with $npm_config_xyz ⏬⏬⏬", 38 | "refresh_os": "node utils/functions/refreshOpensea %npm_config_start% %npm_config_end%" 39 | }, 40 | "author": "Jesse Hall (codeSTACKr)", 41 | "license": "MIT", 42 | "dependencies": { 43 | "async-sema": "^3.1.1", 44 | "canvas": "^2.8.0", 45 | "commander": "^9.0.0", 46 | "dotenv": "^16.0.0", 47 | "form-data": "^4.0.0", 48 | "gif-encoder-2": "^1.0.5", 49 | "graceful-fs": "^4.2.9", 50 | "node-fetch": "^2.6.6", 51 | "puppeteer": "^13.4.1", 52 | "puppeteer-extra": "^3.2.3", 53 | "puppeteer-extra-plugin-adblocker": "^2.12.0", 54 | "puppeteer-extra-plugin-stealth": "^2.9.0", 55 | "sha1": "^1.1.1", 56 | "yesno": "^0.3.1" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /backend/utils/rarity.js: -------------------------------------------------------------------------------- 1 | const basePath = process.cwd(); 2 | const fs = require("fs"); 3 | const layersDir = `${basePath}/layers`; 4 | 5 | const { layerConfigurations } = require(`${basePath}/src/config.js`); 6 | 7 | const { getElements } = require("../src/main.js"); 8 | 9 | // read json data 10 | let rawdata = fs.readFileSync(`${basePath}/build/json/_metadata.json`); 11 | let data = JSON.parse(rawdata); 12 | let editionSize = data.length; 13 | 14 | let rarityData = []; 15 | 16 | // intialize layers to chart 17 | layerConfigurations.forEach((config) => { 18 | let layers = config.layersOrder; 19 | 20 | layers.forEach((layer) => { 21 | // get elements for each layer 22 | let elementsForLayer = []; 23 | let elements = getElements(`${layersDir}/${layer.name}/`); 24 | elements.forEach((element) => { 25 | // just get name and weight for each element 26 | let rarityDataElement = { 27 | trait: element.name, 28 | chance: element.weight.toFixed(0), 29 | occurrence: 0, // initialize at 0 30 | }; 31 | elementsForLayer.push(rarityDataElement); 32 | }); 33 | let layerName = 34 | layer.options?.["displayName"] != undefined 35 | ? layer.options?.["displayName"] 36 | : layer.name; 37 | // don't include duplicate layers 38 | if (!rarityData.includes(layer.name)) { 39 | // add elements for each layer to chart 40 | rarityData[layerName] = elementsForLayer; 41 | } 42 | }); 43 | }); 44 | 45 | // fill up rarity chart with occurrences from metadata 46 | data.forEach((element) => { 47 | let attributes = element.attributes; 48 | attributes.forEach((attribute) => { 49 | let traitType = attribute.trait_type; 50 | let value = attribute.value; 51 | 52 | let rarityDataTraits = rarityData[traitType]; 53 | rarityDataTraits.forEach((rarityDataTrait) => { 54 | if (rarityDataTrait.trait == value) { 55 | // keep track of occurrences 56 | rarityDataTrait.occurrence++; 57 | } 58 | }); 59 | }); 60 | }); 61 | 62 | // convert occurrences to percentages 63 | for (var layer in rarityData) { 64 | for (var attribute in rarityData[layer]) { 65 | // convert to percentage 66 | rarityData[layer][attribute].occurrence = 67 | (rarityData[layer][attribute].occurrence / editionSize) * 100; 68 | 69 | // show two decimal places in percent 70 | rarityData[layer][attribute].occurrence = 71 | rarityData[layer][attribute].occurrence.toFixed(0) + "% out of 100%"; 72 | } 73 | } 74 | 75 | // print out rarity data 76 | for (var layer in rarityData) { 77 | console.log(`Trait type: ${layer}`); 78 | for (var trait in rarityData[layer]) { 79 | console.log(rarityData[layer][trait]); 80 | } 81 | console.log(); 82 | } 83 | -------------------------------------------------------------------------------- /backend/utils/preview_gif.js: -------------------------------------------------------------------------------- 1 | const basePath = process.cwd(); 2 | const fs = require("fs"); 3 | const { createCanvas, loadImage } = require("canvas"); 4 | const buildDir = `${basePath}/build`; 5 | const imageDir = `${buildDir}/images`; 6 | const { format, preview_gif } = require(`${basePath}/src/config.js`); 7 | const canvas = createCanvas(format.width, format.height); 8 | const ctx = canvas.getContext("2d"); 9 | 10 | const HashlipsGiffer = require(`${basePath}/modules/HashlipsGiffer.js`); 11 | let hashlipsGiffer = null; 12 | 13 | const loadImg = async (_img) => { 14 | return new Promise(async (resolve) => { 15 | const loadedImage = await loadImage(`${_img}`); 16 | resolve({ loadedImage: loadedImage }); 17 | }); 18 | }; 19 | 20 | // read image paths 21 | const imageList = []; 22 | const rawdata = fs.readdirSync(imageDir).forEach((file) => { 23 | imageList.push(loadImg(`${imageDir}/${file}`)); 24 | }); 25 | 26 | const saveProjectPreviewGIF = async (_data) => { 27 | // Extract from preview config 28 | const { numberOfImages, order, repeat, quality, delay, imageName } = 29 | preview_gif; 30 | // Extract from format config 31 | const { width, height } = format; 32 | // Prepare canvas 33 | const previewCanvasWidth = width; 34 | const previewCanvasHeight = height; 35 | 36 | if (_data.length < numberOfImages) { 37 | console.log( 38 | `You do not have enough images to create a gif with ${numberOfImages} images.` 39 | ); 40 | } else { 41 | // Shout from the mountain tops 42 | console.log( 43 | `Preparing a ${previewCanvasWidth}x${previewCanvasHeight} project preview with ${_data.length} images.` 44 | ); 45 | const previewPath = `${buildDir}/${imageName}`; 46 | 47 | ctx.clearRect(0, 0, width, height); 48 | 49 | hashlipsGiffer = new HashlipsGiffer( 50 | canvas, 51 | ctx, 52 | `${previewPath}`, 53 | repeat, 54 | quality, 55 | delay 56 | ); 57 | hashlipsGiffer.start(); 58 | 59 | await Promise.all(_data).then((renderObjectArray) => { 60 | // Determin the order of the Images before creating the gif 61 | if (order == "ASC") { 62 | // Do nothing 63 | } else if (order == "DESC") { 64 | renderObjectArray.reverse(); 65 | } else if (order == "MIXED") { 66 | renderObjectArray = renderObjectArray.sort(() => Math.random() - 0.5); 67 | } 68 | 69 | // Reduce the size of the array of Images to the desired amount 70 | if (parseInt(numberOfImages) > 0) { 71 | renderObjectArray = renderObjectArray.slice(0, numberOfImages); 72 | } 73 | 74 | renderObjectArray.forEach((renderObject, index) => { 75 | ctx.globalAlpha = 1; 76 | ctx.globalCompositeOperation = "source-over"; 77 | ctx.drawImage( 78 | renderObject.loadedImage, 79 | 0, 80 | 0, 81 | previewCanvasWidth, 82 | previewCanvasHeight 83 | ); 84 | hashlipsGiffer.add(); 85 | }); 86 | }); 87 | hashlipsGiffer.stop(); 88 | } 89 | }; 90 | 91 | saveProjectPreviewGIF(imageList); 92 | -------------------------------------------------------------------------------- /backend/utils/nftport/uploadMetas.js: -------------------------------------------------------------------------------- 1 | const FormData = require("form-data"); 2 | const path = require("path"); 3 | const basePath = process.cwd(); 4 | const fs = require("graceful-fs"); 5 | 6 | const { fetchNoRetry } = require(`${basePath}/utils/functions/fetchWithRetry.js`); 7 | 8 | const { GENERIC } = require(`${basePath}/src/config.js`); 9 | 10 | const regex = new RegExp("^([0-9]+).json$"); 11 | 12 | if (!fs.existsSync(path.join(`${basePath}/build`, "/ipfsMetas"))) { 13 | fs.mkdirSync(path.join(`${basePath}/build`, "ipfsMetas")); 14 | } 15 | 16 | let readDir = `${basePath}/build/json`; 17 | let writeDir = `${basePath}/build/ipfsMetas`; 18 | 19 | function getFileStreamForJSONFiles() { 20 | const jsonArray = []; 21 | const files = fs.readdirSync(readDir); 22 | files.sort(function (a, b) { 23 | return a.split(".")[0] - b.split(".")[0]; 24 | }); 25 | files.forEach((file) => { 26 | if (!regex.test(file)) return; 27 | const fileData = fs.createReadStream(path.join(readDir, file)); 28 | jsonArray.push(fileData); 29 | }); 30 | return jsonArray; 31 | } 32 | 33 | async function main() { 34 | console.log(`Starting upload of metadata...`); 35 | try { 36 | const metadataFileStreams = getFileStreamForJSONFiles(); 37 | const formData = new FormData(); 38 | metadataFileStreams.forEach((file) => { 39 | formData.append("metadata_files", file); 40 | }); 41 | 42 | const url = "https://api.nftport.xyz/v0/metadata/directory"; 43 | const options = { 44 | method: "POST", 45 | headers: {}, 46 | body: formData, 47 | }; 48 | const response = await fetchNoRetry(url, options); 49 | 50 | fs.writeFileSync( 51 | `${writeDir}/_ipfsMetasResponse.json`, 52 | JSON.stringify(response, null, 2) 53 | ); 54 | console.log(`Metadata uploaded!`); 55 | } catch (err) { 56 | console.log(`Catch: ${err}`); 57 | } 58 | 59 | // Upload Generic Metadata if GENERIC is true 60 | if (GENERIC) { 61 | console.log(`Starting upload of generic metadata...`); 62 | if (!fs.existsSync(path.join(`${basePath}/build`, "/ipfsMetasGeneric"))) { 63 | fs.mkdirSync(path.join(`${basePath}/build`, "ipfsMetasGeneric")); 64 | } 65 | readDir = `${basePath}/build/genericJson`; 66 | writeDir = `${basePath}/build/ipfsMetasGeneric`; 67 | 68 | let jsonFile = fs.readFileSync(`${readDir}/_metadata.json`); 69 | let metaData = JSON.parse(jsonFile); 70 | const uploadedMeta = `${writeDir}/_ipfsMetasResponse.json`; 71 | 72 | const genericObject = { 73 | "name": metaData.name, 74 | "description": metaData.description, 75 | "file_url": metaData.image, 76 | "external_url": metaData?.external_url, 77 | "custom_fields": { 78 | "date": metaData.date, 79 | "compiler": "HashLips Art Engine - codeSTACKr Modified" 80 | } 81 | } 82 | 83 | try { 84 | const url = "https://api.nftport.xyz/v0/metadata"; 85 | const options = { 86 | method: "POST", 87 | headers: { 88 | "Content-Type": "application/json", 89 | }, 90 | body: JSON.stringify(genericObject), 91 | }; 92 | const response = await fetchNoRetry(url, options); 93 | fs.writeFileSync(uploadedMeta, JSON.stringify(response, null, 2)); 94 | console.log(`Generic metadata uploaded!`); 95 | } catch (err) { 96 | console.log(`Catch: ${err}`); 97 | } 98 | } 99 | } 100 | 101 | main(); 102 | -------------------------------------------------------------------------------- /backend/utils/nftport/deployContract.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const basePath = process.cwd(); 3 | const fs = require("fs"); 4 | const yesno = require('yesno'); 5 | 6 | const { 7 | fetchNoRetry, 8 | } = require(`${basePath}/utils/functions/fetchWithRetry.js`); 9 | let { 10 | CHAIN, 11 | GENERIC, 12 | CONTRACT_NAME, 13 | CONTRACT_SYMBOL, 14 | METADATA_UPDATABLE, 15 | ROYALTY_SHARE, 16 | ROYALTY_ADDRESS, 17 | MAX_SUPPLY, 18 | MINT_PRICE, 19 | TOKENS_PER_MINT, 20 | OWNER_ADDRESS, 21 | TREASURY_ADDRESS, 22 | PUBLIC_MINT_START_DATE, 23 | BASE_URI, 24 | PREREVEAL_TOKEN_URI, 25 | PRESALE_MINT_START_DATE, 26 | PRESALE_WHITELISTED_ADDRESSES 27 | } = require(`${basePath}/src/config.js`); 28 | 29 | const deployContract = async () => { 30 | const ok = await yesno({ 31 | question: `Is all REQUIRED contract information correct in config.js? (y/n):`, 32 | default: null, 33 | }); 34 | 35 | if(!ok) { 36 | console.log("Exiting..."); 37 | process.exit(0); 38 | } 39 | 40 | if(GENERIC) { 41 | try { 42 | let jsonFile = fs.readFileSync(`${basePath}/build/ipfsMetasGeneric/_ipfsMetasResponse.json`); 43 | let metaData = JSON.parse(jsonFile); 44 | if(metaData.response === "OK") { 45 | if(!PREREVEAL_TOKEN_URI) { 46 | PREREVEAL_TOKEN_URI = metaData.metadata_uri; 47 | } 48 | } else { 49 | console.log('There is an issue with the metadata upload. Please check the /build/_ipfsMetasGeneric/_ipfsMetasResponse.json file for more information. Running "npm run upload_metadata" may fix this issue.'); 50 | } 51 | } catch (err) { 52 | console.log(`/build/_ipfsMetasGeneric/_ipfsMetasResponse.json file not found. Run "npm run upload_metadata" first.`); 53 | console.log(`Catch: ${err}`); 54 | process.exit(0); 55 | } 56 | } else { 57 | try { 58 | let jsonFile = fs.readFileSync(`${basePath}/build/ipfsMetas/_ipfsMetasResponse.json`); 59 | let metaData = JSON.parse(jsonFile); 60 | if(metaData.response === "OK") { 61 | if(!BASE_URI) { 62 | BASE_URI = metaData.metadata_directory_ipfs_uri; 63 | } 64 | } else { 65 | console.log('There is an issue with the metadata upload. Please check the /build/_ipfsMetas/_ipfsMetasResponse.json file for more information. Running "npm run upload_metadata" may fix this issue.'); 66 | } 67 | } catch (err) { 68 | console.log(`/build/_ipfsMetasGeneric/_ipfsMetasResponse.json file not found. Run "npm run upload_metadata" first.`); 69 | process.exit(0); 70 | } 71 | } 72 | 73 | if (!fs.existsSync(path.join(`${basePath}/build`, "/contract"))) { 74 | fs.mkdirSync(path.join(`${basePath}/build`, "contract")); 75 | } 76 | 77 | try { 78 | const url = `https://api.nftport.xyz/v0/contracts/collections`; 79 | const contract = { 80 | chain: CHAIN.toLowerCase(), 81 | name: CONTRACT_NAME, 82 | symbol: CONTRACT_SYMBOL, 83 | owner_address: OWNER_ADDRESS, 84 | metadata_updatable: METADATA_UPDATABLE, 85 | royalties_share: ROYALTY_SHARE, 86 | royalties_address: ROYALTY_ADDRESS, 87 | max_supply: MAX_SUPPLY, 88 | mint_price: MINT_PRICE, 89 | tokens_per_mint: TOKENS_PER_MINT, 90 | treasury_address: TREASURY_ADDRESS, 91 | public_mint_start_date: PUBLIC_MINT_START_DATE, 92 | presale_mint_start_date: PRESALE_MINT_START_DATE, 93 | base_uri: BASE_URI, 94 | prereveal_token_uri: PREREVEAL_TOKEN_URI, 95 | presale_whitelisted_addresses: PRESALE_WHITELISTED_ADDRESSES 96 | }; 97 | const options = { 98 | method: "POST", 99 | headers: { 100 | "Content-Type": "application/json", 101 | }, 102 | body: JSON.stringify(contract), 103 | }; 104 | const response = await fetchNoRetry(url, options); 105 | fs.writeFileSync(`${basePath}/build/contract/_deployContractResponse.json`, JSON.stringify(response, null, 2)); 106 | if(response.response === "OK") { 107 | console.log(`Contract deployment started.`); 108 | } else { 109 | console.log(`Contract deployment failed`); 110 | } 111 | console.log(`Check /build/contract/_deployContractResponse.json for more information. Run "npm run get_contract" to get the contract details.`); 112 | } catch (error) { 113 | console.log(`CATCH: Contract deployment failed`, `ERROR: ${error}`); 114 | } 115 | }; 116 | 117 | deployContract(); 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Minting DAPP 2 | 3 | ## Table of Contents 4 | 5 | - [RESOURCES](#resources) 6 | - [INSTALLATION](#installation) 7 | - [BACKEND](#backend) 8 | - [BACKEND COMMANDS](#backend-commands) 9 | - [FRONTEND](#frontend) 10 | 11 | ## RESOURCES 12 | 13 | ### Main Video 14 | 15 | 🌟 [EASY Minting dApp | Whitelisting | Entire Process!! Create an Entire NFT Collection (10,000+)](https://youtu.be/cLB7u0KQFIs) 16 | 17 | ### Update Video adding Ethereum support! 18 | 19 | 🚀 [How To Deploy a Smart Contract to Ethereum!! (Updated Minting dApp)](https://youtu.be/-EB2TTQxSWc) 20 | 21 | Base art generator code is from [hashlips_art_engine](https://github.com/HashLips/hashlips_art_engine) 22 | 23 | Contract uses [NFTPort](https://nftport.xyz) 24 | 25 | Join the Discord server for more help from the community: [codeSTACKr Discord](https://discord.gg/A9CnsVzzkZ) 26 | 27 | ## INSTALLATION 28 | 29 | ### Backend 30 | 31 | - Clone this repo or download the latest release zip file. 32 | - Unzip, if needed, and open the folder in VS Code. 33 | - From the terminal run: 34 | ``` 35 | cd backend 36 | npm install 37 | ``` 38 | - Copy your image layers into the `/backend/layers` folder. 39 | - Use the `/backend/src/config.js` file to set up your layers and NFT information. 40 | 41 | ### Backend Commands 42 | 43 | Generate: 44 | ``` 45 | $ npm run generate 46 | ``` 47 | - Generates unique images based on the layers in the `/backend/layers` folder. 48 | - WARNING: This command deletes the `/backend/build` folder if it exists! 49 | 50 | Rarity (Hashlips): 51 | ``` 52 | $ npm run rarity 53 | ``` 54 | - Calculates the rarity of NFT properties based on layer files. 55 | 56 | Rarity (codeSTACKr): 57 | ``` 58 | $ npm run rarity_md 59 | ``` 60 | 61 | - Calculates the rarity of NFT properties based on metadata. 62 | 63 | Rarity Rank (codeSTACKr): 64 | ``` 65 | $ npm run rarity_rank 66 | ``` 67 | 68 | - Provides ranking details through a user interface after calculating using the codeSTACKr Rarity command. 69 | 70 | Update Info: 71 | ``` 72 | $ npm run update_info 73 | ``` 74 | 75 | - Allows you to update `namePrefix`, `description`, and/or `baseUri` for metadata after it was already generated. 76 | 77 | Create Generic Metadata: 78 | ``` 79 | $ npm run create_generic 80 | ``` 81 | 82 | - Creates generic metadata using the settings from the `/backend/src/config.js` file. 83 | 84 | Upload Files/Images: 85 | ``` 86 | $ npm run upload_files 87 | ``` 88 | 89 | - Uploads all files in the `/backend/build/images` folder. 90 | 91 | Upload Metadata: 92 | ``` 93 | $ npm run upload_metadata 94 | ``` 95 | 96 | - Uploads all `.json` files in both the `/backend/build/json` folder and, if it exists, the `/backend/build/genericJson` folder as well. 97 | 98 | Deploy Contract: 99 | ``` 100 | $ npm run deploy_contract 101 | ``` 102 | 103 | - Deploys a contract to the blockchain using the settings from the `/backend/src/config.js` file. 104 | 105 | Get Contract: 106 | ``` 107 | $ npm run get_contract 108 | ``` 109 | 110 | - Gets the deployed contract details including the contracts ABI using the transactions hash from the Deploy Contract command. 111 | 112 | Update Contract: 113 | ``` 114 | $ npm run update_public_mint_start_date 115 | $ npm run update_presale_mint_start_date 116 | $ npm run update_presale_whitelisted_addresses 117 | $ npm run update_presale_whitelisted_addresses_remove 118 | $ npm run update_royalty_share 119 | $ npm run update_royalty_address 120 | $ npm run update_base_uri 121 | $ npm run update_prereveal_token_uri 122 | ``` 123 | 124 | - Updates specific fields of the contract using the settings from the `/backend/src/config.js` file. 125 | - Available fields to update: 126 | - `prereveal_token_uri` - This will update the pre-reveal token uri for all NFTs. (Hidden image) 127 | - `base_uri` - This will update the base uri for all NFTs and reveal all. 128 | - `public_mint_start_date` - Eg: 2022-02-08T11:30:48+00:00 129 | - `presale_mint_start_date` - Eg: 2022-02-08T11:30:48+00:00 130 | - `presale_whitelisted_addresses` - Adds address(es) to the whitelist 131 | - `presale_whitelisted_addresses_remove` - Removes address(es) from the whitelist 132 | - `royalties_share` - Updates the royalty share 133 | - `royalties_address` - Updates the royalty wallet address 134 | 135 | Refresh OpenSea: 136 | ``` 137 | $ npm run refresh_os --start=1 --end=100 138 | ``` 139 | 140 | - Refreshes the listing for the specified editions on OpenSea. 141 | - Both the `--start` and `--end` flags are required. 142 | 143 | ### Frontend 144 | 145 | - Update the `frontend/js/abi.js` file with the ABI from `backend/build/contract/_contract_abi.json`. 146 | - Update your information in the `frontend/js/constants.js` file. 147 | - Deploy your dApp to Netlify. (Reference the video for full instructions.) 148 | 149 | 150 | ## Reference the [main video](https://youtu.be/cLB7u0KQFIs) and [update video](https://youtu.be/-EB2TTQxSWc) for more details. 151 | -------------------------------------------------------------------------------- /frontend/css/themes/splide-skyblue.min.css: -------------------------------------------------------------------------------- 1 | .splide--nav>.splide__slider>.splide__track>.splide__list>.splide__slide,.splide--nav>.splide__track>.splide__list>.splide__slide{border:3px solid transparent;cursor:pointer;opacity:.7}.splide--nav>.splide__slider>.splide__track>.splide__list>.splide__slide.is-active,.splide--nav>.splide__track>.splide__list>.splide__slide.is-active{border:3px solid #00bfff;opacity:1}.splide--nav>.splide__slider>.splide__track>.splide__list>.splide__slide:focus,.splide--nav>.splide__track>.splide__list>.splide__slide:focus{outline:none}.splide__arrow{background:transparent;border:0;cursor:pointer;padding:0;position:absolute;top:50%;transform:translateY(-50%);z-index:1}.splide__arrow svg{fill:#00bfff;height:2.5em;transition:fill .2s linear;width:2.5em}.splide__arrow:hover svg{fill:#66d9ff}.splide__arrow:focus{outline:none}.splide__arrow--prev{left:1em}.splide__arrow--prev svg{transform:scaleX(-1)}.splide__arrow--next{right:1em}.splide__pagination{bottom:.5em;left:0;padding:0 1em;position:absolute;right:0;z-index:1}.splide__pagination__page{background:#ccc;border:0;border-radius:50%;display:inline-block;height:10px;margin:3px;padding:0;transition:all .2s linear;width:10px}.splide__pagination__page.is-active{background:#00bfff;transform:scale(1.4)}.splide__pagination__page:hover{background:#66d9ff;cursor:pointer;opacity:.9}.splide__pagination__page:focus{outline:none}.splide__container{box-sizing:border-box;position:relative}.splide__list{-webkit-backface-visibility:hidden;backface-visibility:hidden;display:-ms-flexbox;display:flex;height:100%;margin:0!important;padding:0!important;transform-style:preserve-3d}.splide.is-initialized:not(.is-active) .splide__list{display:block}.splide__pagination{-ms-flex-align:center;align-items:center;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:center;justify-content:center;margin:0;pointer-events:none}.splide__pagination li{display:inline-block;line-height:1;list-style-type:none;margin:0;pointer-events:auto}.splide__progress__bar{width:0}.splide{outline:none;position:relative;visibility:hidden}.splide.is-initialized,.splide.is-rendered{visibility:visible}.splide__slide{-webkit-backface-visibility:hidden;backface-visibility:hidden;box-sizing:border-box;-ms-flex-negative:0;flex-shrink:0;list-style-type:none!important;margin:0;outline:none;position:relative}.splide__slide img{vertical-align:bottom}.splide__slider{position:relative}.splide__spinner{animation:splide-loading 1s linear infinite;border:2px solid #00bfff;border-left-color:transparent;border-radius:50%;bottom:0;contain:strict;display:inline-block;height:20px;left:0;margin:auto;position:absolute;right:0;top:0;width:20px}.splide__track{overflow:hidden;position:relative;z-index:0}@keyframes splide-loading{0%{transform:rotate(0)}to{transform:rotate(1turn)}}.splide--draggable>.splide__slider>.splide__track,.splide--draggable>.splide__track{-webkit-user-select:none;-ms-user-select:none;user-select:none}.splide--fade>.splide__slider>.splide__track>.splide__list,.splide--fade>.splide__track>.splide__list{display:block}.splide--fade>.splide__slider>.splide__track>.splide__list>.splide__slide,.splide--fade>.splide__track>.splide__list>.splide__slide{left:0;opacity:0;position:absolute;top:0;z-index:0}.splide--fade>.splide__slider>.splide__track>.splide__list>.splide__slide.is-active,.splide--fade>.splide__track>.splide__list>.splide__slide.is-active{opacity:1;position:relative;z-index:1}.splide--rtl{direction:rtl}.splide--ttb.is-active>.splide__slider>.splide__track>.splide__list,.splide--ttb.is-active>.splide__track>.splide__list{display:block}.splide__progress__bar{background:#ccc;height:3px}.splide--rtl>.splide__arrows .splide__arrow--prev,.splide--rtl>.splide__slider>.splide__track>.splide__arrows .splide__arrow--prev,.splide--rtl>.splide__track>.splide__arrows .splide__arrow--prev{left:auto;right:1em}.splide--rtl>.splide__arrows .splide__arrow--prev svg,.splide--rtl>.splide__slider>.splide__track>.splide__arrows .splide__arrow--prev svg,.splide--rtl>.splide__track>.splide__arrows .splide__arrow--prev svg{transform:scaleX(1)}.splide--rtl>.splide__arrows .splide__arrow--next,.splide--rtl>.splide__slider>.splide__track>.splide__arrows .splide__arrow--next,.splide--rtl>.splide__track>.splide__arrows .splide__arrow--next{left:1em;right:auto}.splide--rtl>.splide__arrows .splide__arrow--next svg,.splide--rtl>.splide__slider>.splide__track>.splide__arrows .splide__arrow--next svg,.splide--rtl>.splide__track>.splide__arrows .splide__arrow--next svg{transform:scaleX(-1)}.splide--ttb>.splide__arrows .splide__arrow,.splide--ttb>.splide__slider>.splide__track>.splide__arrows .splide__arrow,.splide--ttb>.splide__track>.splide__arrows .splide__arrow{left:50%;transform:translate(-50%)}.splide--ttb>.splide__arrows .splide__arrow--prev,.splide--ttb>.splide__slider>.splide__track>.splide__arrows .splide__arrow--prev,.splide--ttb>.splide__track>.splide__arrows .splide__arrow--prev{top:1em}.splide--ttb>.splide__arrows .splide__arrow--prev svg,.splide--ttb>.splide__slider>.splide__track>.splide__arrows .splide__arrow--prev svg,.splide--ttb>.splide__track>.splide__arrows .splide__arrow--prev svg{transform:rotate(-90deg)}.splide--ttb>.splide__arrows .splide__arrow--next,.splide--ttb>.splide__slider>.splide__track>.splide__arrows .splide__arrow--next,.splide--ttb>.splide__track>.splide__arrows .splide__arrow--next{bottom:1em;top:auto}.splide--ttb>.splide__arrows .splide__arrow--next svg,.splide--ttb>.splide__slider>.splide__track>.splide__arrows .splide__arrow--next svg,.splide--ttb>.splide__track>.splide__arrows .splide__arrow--next svg{transform:rotate(90deg)}.splide--ttb>.splide__pagination,.splide--ttb>.splide__slider>.splide__pagination{bottom:0;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;left:auto;padding:1em 0;right:.5em;top:0} -------------------------------------------------------------------------------- /frontend/css/splide.min.css: -------------------------------------------------------------------------------- 1 | .splide__container{box-sizing:border-box;position:relative}.splide__list{-webkit-backface-visibility:hidden;backface-visibility:hidden;display:-ms-flexbox;display:flex;height:100%;margin:0!important;padding:0!important;transform-style:preserve-3d}.splide.is-initialized:not(.is-active) .splide__list{display:block}.splide__pagination{-ms-flex-align:center;align-items:center;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:center;justify-content:center;margin:0;pointer-events:none}.splide__pagination li{display:inline-block;line-height:1;list-style-type:none;margin:0;pointer-events:auto}.splide__progress__bar{width:0}.splide{outline:none;position:relative;visibility:hidden}.splide.is-initialized,.splide.is-rendered{visibility:visible}.splide__slide{-webkit-backface-visibility:hidden;backface-visibility:hidden;box-sizing:border-box;-ms-flex-negative:0;flex-shrink:0;list-style-type:none!important;margin:0;outline:none;position:relative}.splide__slide img{vertical-align:bottom}.splide__slider{position:relative}.splide__spinner{animation:splide-loading 1s linear infinite;border:2px solid #999;border-left-color:transparent;border-radius:50%;bottom:0;contain:strict;display:inline-block;height:20px;left:0;margin:auto;position:absolute;right:0;top:0;width:20px}.splide__track{overflow:hidden;position:relative;z-index:0}@keyframes splide-loading{0%{transform:rotate(0)}to{transform:rotate(1turn)}}.splide--draggable>.splide__slider>.splide__track,.splide--draggable>.splide__track{-webkit-user-select:none;-ms-user-select:none;user-select:none}.splide--fade>.splide__slider>.splide__track>.splide__list,.splide--fade>.splide__track>.splide__list{display:block}.splide--fade>.splide__slider>.splide__track>.splide__list>.splide__slide,.splide--fade>.splide__track>.splide__list>.splide__slide{left:0;opacity:0;position:absolute;top:0;z-index:0}.splide--fade>.splide__slider>.splide__track>.splide__list>.splide__slide.is-active,.splide--fade>.splide__track>.splide__list>.splide__slide.is-active{opacity:1;position:relative;z-index:1}.splide--rtl{direction:rtl}.splide--ttb.is-active>.splide__slider>.splide__track>.splide__list,.splide--ttb.is-active>.splide__track>.splide__list{display:block}.splide__arrow{-ms-flex-align:center;align-items:center;background:#ccc;border:0;border-radius:50%;cursor:pointer;display:-ms-flexbox;display:flex;height:2em;-ms-flex-pack:center;justify-content:center;opacity:.7;padding:0;position:absolute;top:50%;transform:translateY(-50%);width:2em;z-index:1}.splide__arrow svg{fill:#000;height:1.2em;width:1.2em}.splide__arrow:hover{opacity:.9}.splide__arrow:focus{outline:none}.splide__arrow--prev{left:1em}.splide__arrow--prev svg{transform:scaleX(-1)}.splide__arrow--next{right:1em}.splide__pagination{bottom:.5em;left:0;padding:0 1em;position:absolute;right:0;z-index:1}.splide__pagination__page{background:#ccc;border:0;border-radius:50%;display:inline-block;height:8px;margin:3px;opacity:.7;padding:0;transition:transform .2s linear;width:8px}.splide__pagination__page.is-active{background:#fff;transform:scale(1.4)}.splide__pagination__page:hover{cursor:pointer;opacity:.9}.splide__pagination__page:focus{outline:none}.splide__progress__bar{background:#ccc;height:3px}.splide--nav>.splide__slider>.splide__track>.splide__list>.splide__slide,.splide--nav>.splide__track>.splide__list>.splide__slide{border:3px solid transparent;cursor:pointer}.splide--nav>.splide__slider>.splide__track>.splide__list>.splide__slide.is-active,.splide--nav>.splide__track>.splide__list>.splide__slide.is-active{border:3px solid #000}.splide--nav>.splide__slider>.splide__track>.splide__list>.splide__slide:focus,.splide--nav>.splide__track>.splide__list>.splide__slide:focus{outline:none}.splide--rtl>.splide__arrows .splide__arrow--prev,.splide--rtl>.splide__slider>.splide__track>.splide__arrows .splide__arrow--prev,.splide--rtl>.splide__track>.splide__arrows .splide__arrow--prev{left:auto;right:1em}.splide--rtl>.splide__arrows .splide__arrow--prev svg,.splide--rtl>.splide__slider>.splide__track>.splide__arrows .splide__arrow--prev svg,.splide--rtl>.splide__track>.splide__arrows .splide__arrow--prev svg{transform:scaleX(1)}.splide--rtl>.splide__arrows .splide__arrow--next,.splide--rtl>.splide__slider>.splide__track>.splide__arrows .splide__arrow--next,.splide--rtl>.splide__track>.splide__arrows .splide__arrow--next{left:1em;right:auto}.splide--rtl>.splide__arrows .splide__arrow--next svg,.splide--rtl>.splide__slider>.splide__track>.splide__arrows .splide__arrow--next svg,.splide--rtl>.splide__track>.splide__arrows .splide__arrow--next svg{transform:scaleX(-1)}.splide--ttb>.splide__arrows .splide__arrow,.splide--ttb>.splide__slider>.splide__track>.splide__arrows .splide__arrow,.splide--ttb>.splide__track>.splide__arrows .splide__arrow{left:50%;transform:translate(-50%)}.splide--ttb>.splide__arrows .splide__arrow--prev,.splide--ttb>.splide__slider>.splide__track>.splide__arrows .splide__arrow--prev,.splide--ttb>.splide__track>.splide__arrows .splide__arrow--prev{top:1em}.splide--ttb>.splide__arrows .splide__arrow--prev svg,.splide--ttb>.splide__slider>.splide__track>.splide__arrows .splide__arrow--prev svg,.splide--ttb>.splide__track>.splide__arrows .splide__arrow--prev svg{transform:rotate(-90deg)}.splide--ttb>.splide__arrows .splide__arrow--next,.splide--ttb>.splide__slider>.splide__track>.splide__arrows .splide__arrow--next,.splide--ttb>.splide__track>.splide__arrows .splide__arrow--next{bottom:1em;top:auto}.splide--ttb>.splide__arrows .splide__arrow--next svg,.splide--ttb>.splide__slider>.splide__track>.splide__arrows .splide__arrow--next svg,.splide--ttb>.splide__track>.splide__arrows .splide__arrow--next svg{transform:rotate(90deg)}.splide--ttb>.splide__pagination,.splide--ttb>.splide__slider>.splide__pagination{bottom:0;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;left:auto;padding:1em 0;right:.5em;top:0} -------------------------------------------------------------------------------- /frontend/css/themes/splide-default.min.css: -------------------------------------------------------------------------------- 1 | .splide__container{box-sizing:border-box;position:relative}.splide__list{-webkit-backface-visibility:hidden;backface-visibility:hidden;display:-ms-flexbox;display:flex;height:100%;margin:0!important;padding:0!important;transform-style:preserve-3d}.splide.is-initialized:not(.is-active) .splide__list{display:block}.splide__pagination{-ms-flex-align:center;align-items:center;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:center;justify-content:center;margin:0;pointer-events:none}.splide__pagination li{display:inline-block;line-height:1;list-style-type:none;margin:0;pointer-events:auto}.splide__progress__bar{width:0}.splide{outline:none;position:relative;visibility:hidden}.splide.is-initialized,.splide.is-rendered{visibility:visible}.splide__slide{-webkit-backface-visibility:hidden;backface-visibility:hidden;box-sizing:border-box;-ms-flex-negative:0;flex-shrink:0;list-style-type:none!important;margin:0;outline:none;position:relative}.splide__slide img{vertical-align:bottom}.splide__slider{position:relative}.splide__spinner{animation:splide-loading 1s linear infinite;border:2px solid #999;border-left-color:transparent;border-radius:50%;bottom:0;contain:strict;display:inline-block;height:20px;left:0;margin:auto;position:absolute;right:0;top:0;width:20px}.splide__track{overflow:hidden;position:relative;z-index:0}@keyframes splide-loading{0%{transform:rotate(0)}to{transform:rotate(1turn)}}.splide--draggable>.splide__slider>.splide__track,.splide--draggable>.splide__track{-webkit-user-select:none;-ms-user-select:none;user-select:none}.splide--fade>.splide__slider>.splide__track>.splide__list,.splide--fade>.splide__track>.splide__list{display:block}.splide--fade>.splide__slider>.splide__track>.splide__list>.splide__slide,.splide--fade>.splide__track>.splide__list>.splide__slide{left:0;opacity:0;position:absolute;top:0;z-index:0}.splide--fade>.splide__slider>.splide__track>.splide__list>.splide__slide.is-active,.splide--fade>.splide__track>.splide__list>.splide__slide.is-active{opacity:1;position:relative;z-index:1}.splide--rtl{direction:rtl}.splide--ttb.is-active>.splide__slider>.splide__track>.splide__list,.splide--ttb.is-active>.splide__track>.splide__list{display:block}.splide__arrow{-ms-flex-align:center;align-items:center;background:#ccc;border:0;border-radius:50%;cursor:pointer;display:-ms-flexbox;display:flex;height:2em;-ms-flex-pack:center;justify-content:center;opacity:.7;padding:0;position:absolute;top:50%;transform:translateY(-50%);width:2em;z-index:1}.splide__arrow svg{fill:#000;height:1.2em;width:1.2em}.splide__arrow:hover{opacity:.9}.splide__arrow:focus{outline:none}.splide__arrow--prev{left:1em}.splide__arrow--prev svg{transform:scaleX(-1)}.splide__arrow--next{right:1em}.splide__pagination{bottom:.5em;left:0;padding:0 1em;position:absolute;right:0;z-index:1}.splide__pagination__page{background:#ccc;border:0;border-radius:50%;display:inline-block;height:8px;margin:3px;opacity:.7;padding:0;transition:transform .2s linear;width:8px}.splide__pagination__page.is-active{background:#fff;transform:scale(1.4)}.splide__pagination__page:hover{cursor:pointer;opacity:.9}.splide__pagination__page:focus{outline:none}.splide__progress__bar{background:#ccc;height:3px}.splide--nav>.splide__slider>.splide__track>.splide__list>.splide__slide,.splide--nav>.splide__track>.splide__list>.splide__slide{border:3px solid transparent;cursor:pointer}.splide--nav>.splide__slider>.splide__track>.splide__list>.splide__slide.is-active,.splide--nav>.splide__track>.splide__list>.splide__slide.is-active{border:3px solid #000}.splide--nav>.splide__slider>.splide__track>.splide__list>.splide__slide:focus,.splide--nav>.splide__track>.splide__list>.splide__slide:focus{outline:none}.splide--rtl>.splide__arrows .splide__arrow--prev,.splide--rtl>.splide__slider>.splide__track>.splide__arrows .splide__arrow--prev,.splide--rtl>.splide__track>.splide__arrows .splide__arrow--prev{left:auto;right:1em}.splide--rtl>.splide__arrows .splide__arrow--prev svg,.splide--rtl>.splide__slider>.splide__track>.splide__arrows .splide__arrow--prev svg,.splide--rtl>.splide__track>.splide__arrows .splide__arrow--prev svg{transform:scaleX(1)}.splide--rtl>.splide__arrows .splide__arrow--next,.splide--rtl>.splide__slider>.splide__track>.splide__arrows .splide__arrow--next,.splide--rtl>.splide__track>.splide__arrows .splide__arrow--next{left:1em;right:auto}.splide--rtl>.splide__arrows .splide__arrow--next svg,.splide--rtl>.splide__slider>.splide__track>.splide__arrows .splide__arrow--next svg,.splide--rtl>.splide__track>.splide__arrows .splide__arrow--next svg{transform:scaleX(-1)}.splide--ttb>.splide__arrows .splide__arrow,.splide--ttb>.splide__slider>.splide__track>.splide__arrows .splide__arrow,.splide--ttb>.splide__track>.splide__arrows .splide__arrow{left:50%;transform:translate(-50%)}.splide--ttb>.splide__arrows .splide__arrow--prev,.splide--ttb>.splide__slider>.splide__track>.splide__arrows .splide__arrow--prev,.splide--ttb>.splide__track>.splide__arrows .splide__arrow--prev{top:1em}.splide--ttb>.splide__arrows .splide__arrow--prev svg,.splide--ttb>.splide__slider>.splide__track>.splide__arrows .splide__arrow--prev svg,.splide--ttb>.splide__track>.splide__arrows .splide__arrow--prev svg{transform:rotate(-90deg)}.splide--ttb>.splide__arrows .splide__arrow--next,.splide--ttb>.splide__slider>.splide__track>.splide__arrows .splide__arrow--next,.splide--ttb>.splide__track>.splide__arrows .splide__arrow--next{bottom:1em;top:auto}.splide--ttb>.splide__arrows .splide__arrow--next svg,.splide--ttb>.splide__slider>.splide__track>.splide__arrows .splide__arrow--next svg,.splide--ttb>.splide__track>.splide__arrows .splide__arrow--next svg{transform:rotate(90deg)}.splide--ttb>.splide__pagination,.splide--ttb>.splide__slider>.splide__pagination{bottom:0;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;left:auto;padding:1em 0;right:.5em;top:0} -------------------------------------------------------------------------------- /frontend/css/themes/splide-sea-green.min.css: -------------------------------------------------------------------------------- 1 | .splide--nav>.splide__slider>.splide__track>.splide__list>.splide__slide,.splide--nav>.splide__track>.splide__list>.splide__slide{border:3px solid transparent;border-radius:4px;cursor:pointer;opacity:.7}.splide--nav>.splide__slider>.splide__track>.splide__list>.splide__slide.is-active,.splide--nav>.splide__track>.splide__list>.splide__slide.is-active{border:3px solid #20b2aa;opacity:1}.splide--nav>.splide__slider>.splide__track>.splide__list>.splide__slide:focus,.splide--nav>.splide__track>.splide__list>.splide__slide:focus{outline:none}.splide--ttb>.splide__arrows .splide__arrow,.splide--ttb>.splide__slider>.splide__track>.splide__arrows .splide__arrow,.splide--ttb>.splide__track>.splide__arrows .splide__arrow{left:50%;transform:translate(-50%)}.splide--ttb>.splide__arrows .splide__arrow--prev,.splide--ttb>.splide__slider>.splide__track>.splide__arrows .splide__arrow--prev,.splide--ttb>.splide__track>.splide__arrows .splide__arrow--prev{top:1em}.splide--ttb>.splide__arrows .splide__arrow--prev svg,.splide--ttb>.splide__slider>.splide__track>.splide__arrows .splide__arrow--prev svg,.splide--ttb>.splide__track>.splide__arrows .splide__arrow--prev svg{transform:rotate(-90deg)}.splide--ttb>.splide__arrows .splide__arrow--next,.splide--ttb>.splide__slider>.splide__track>.splide__arrows .splide__arrow--next,.splide--ttb>.splide__track>.splide__arrows .splide__arrow--next{bottom:1em;top:auto}.splide--ttb>.splide__arrows .splide__arrow--next svg,.splide--ttb>.splide__slider>.splide__track>.splide__arrows .splide__arrow--next svg,.splide--ttb>.splide__track>.splide__arrows .splide__arrow--next svg{transform:rotate(90deg)}.splide--ttb>.splide__pagination,.splide--ttb>.splide__slider>.splide__pagination{bottom:0;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;left:auto;padding:1em 0;right:1em;top:0}.splide--ttb>.splide__pagination .splide__pagination__page,.splide--ttb>.splide__slider>.splide__pagination .splide__pagination__page{height:20px;width:5px}.splide__arrow{background:transparent;border:0;cursor:pointer;padding:0;position:absolute;top:50%;transform:translateY(-50%);z-index:1}.splide__arrow svg{fill:#20b2aa;height:2.5em;transition:fill .2s linear;width:2.5em}.splide__arrow:hover svg{fill:#57e1d9}.splide__arrow:focus{outline:none}.splide__arrow--prev{left:1em}.splide__arrow--prev svg{transform:scaleX(-1)}.splide__arrow--next{right:1em}.splide__pagination{bottom:1em;left:0;padding:0 1em;position:absolute;right:0;z-index:1}.splide__pagination__page{background:#ccc;border:0;border-radius:2.5px;display:inline-block;height:5px;margin:3px;padding:0;transition:all .2s linear;width:20px}.splide__pagination__page.is-active{background:#20b2aa}.splide__pagination__page:hover{background:#57e1d9;cursor:pointer;opacity:.9}.splide__pagination__page:focus{outline:none}.splide__slide{border-radius:4px}.splide__container{box-sizing:border-box;position:relative}.splide__list{-webkit-backface-visibility:hidden;backface-visibility:hidden;display:-ms-flexbox;display:flex;height:100%;margin:0!important;padding:0!important;transform-style:preserve-3d}.splide.is-initialized:not(.is-active) .splide__list{display:block}.splide__pagination{-ms-flex-align:center;align-items:center;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:center;justify-content:center;margin:0;pointer-events:none}.splide__pagination li{display:inline-block;line-height:1;list-style-type:none;margin:0;pointer-events:auto}.splide__progress__bar{width:0}.splide{outline:none;position:relative;visibility:hidden}.splide.is-initialized,.splide.is-rendered{visibility:visible}.splide__slide{-webkit-backface-visibility:hidden;backface-visibility:hidden;box-sizing:border-box;-ms-flex-negative:0;flex-shrink:0;list-style-type:none!important;margin:0;outline:none;position:relative}.splide__slide img{vertical-align:bottom}.splide__slider{position:relative}.splide__spinner{animation:splide-loading 1s linear infinite;border:2px solid #20b2aa;border-left-color:transparent;border-radius:50%;bottom:0;contain:strict;display:inline-block;height:20px;left:0;margin:auto;position:absolute;right:0;top:0;width:20px}.splide__track{overflow:hidden;position:relative;z-index:0}@keyframes splide-loading{0%{transform:rotate(0)}to{transform:rotate(1turn)}}.splide--draggable>.splide__slider>.splide__track,.splide--draggable>.splide__track{-webkit-user-select:none;-ms-user-select:none;user-select:none}.splide--fade>.splide__slider>.splide__track>.splide__list,.splide--fade>.splide__track>.splide__list{display:block}.splide--fade>.splide__slider>.splide__track>.splide__list>.splide__slide,.splide--fade>.splide__track>.splide__list>.splide__slide{left:0;opacity:0;position:absolute;top:0;z-index:0}.splide--fade>.splide__slider>.splide__track>.splide__list>.splide__slide.is-active,.splide--fade>.splide__track>.splide__list>.splide__slide.is-active{opacity:1;position:relative;z-index:1}.splide--rtl{direction:rtl}.splide--ttb.is-active>.splide__slider>.splide__track>.splide__list,.splide--ttb.is-active>.splide__track>.splide__list{display:block}.splide__progress__bar{background:#ccc;height:3px}.splide--rtl>.splide__arrows .splide__arrow--prev,.splide--rtl>.splide__slider>.splide__track>.splide__arrows .splide__arrow--prev,.splide--rtl>.splide__track>.splide__arrows .splide__arrow--prev{left:auto;right:1em}.splide--rtl>.splide__arrows .splide__arrow--prev svg,.splide--rtl>.splide__slider>.splide__track>.splide__arrows .splide__arrow--prev svg,.splide--rtl>.splide__track>.splide__arrows .splide__arrow--prev svg{transform:scaleX(1)}.splide--rtl>.splide__arrows .splide__arrow--next,.splide--rtl>.splide__slider>.splide__track>.splide__arrows .splide__arrow--next,.splide--rtl>.splide__track>.splide__arrows .splide__arrow--next{left:1em;right:auto}.splide--rtl>.splide__arrows .splide__arrow--next svg,.splide--rtl>.splide__slider>.splide__track>.splide__arrows .splide__arrow--next svg,.splide--rtl>.splide__track>.splide__arrows .splide__arrow--next svg{transform:scaleX(-1)}.splide__slider>.splide__arrows .splide__arrow--prev{left:-2.5em}.splide__slider>.splide__arrows .splide__arrow--next{right:-2.5em}.splide{padding:3em} -------------------------------------------------------------------------------- /backend/src/config.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const basePath = process.cwd(); 3 | const fs = require("fs"); 4 | const { MODE } = require(`${basePath}/constants/blend_mode.js`); 5 | const { NETWORK } = require(`${basePath}/constants/network.js`); 6 | 7 | const network = NETWORK.eth; 8 | 9 | // General metadata for Ethereum 10 | const namePrefix = "YOUR COLLECTION NAME"; 11 | const description = "Remember to replace this description"; 12 | const baseUri = "ipfs://NewUriToReplace"; // This will be replaced automatically 13 | 14 | const layerConfigurations = [ 15 | { 16 | growEditionSizeTo: 5, 17 | layersOrder: [ 18 | { name: "Background" }, 19 | { name: "Eyeball" }, 20 | { name: "Eye color" }, 21 | { name: "Iris" }, 22 | { name: "Shine" }, 23 | { name: "Bottom lid" }, 24 | { name: "Top lid" }, 25 | ], 26 | }, 27 | ]; 28 | 29 | const shuffleLayerConfigurations = true; 30 | 31 | const debugLogs = false; 32 | 33 | const format = { 34 | width: 512, 35 | height: 512, 36 | smoothing: false, 37 | }; 38 | 39 | const extraMetadata = { 40 | external_url: "https://codecats.xyz", // Replace with your website or remove this line if you do not have one. 41 | }; 42 | 43 | // NFTPort Info 44 | 45 | // ** REQUIRED ** 46 | const AUTH = process.env.NFTPORT_API_KEY; // Set this in the .env file to prevent exposing your API key when pushing to Github 47 | const LIMIT = 2; // Your API key rate limit 48 | const CHAIN = 'goerli'; // only goerli, polygon, or ethereum 49 | 50 | // REQUIRED CONTRACT DETAILS THAT CANNOT BE UPDATED LATER! 51 | const CONTRACT_NAME = 'CRYPTOPUNKS'; 52 | const CONTRACT_SYMBOL = 'CP'; 53 | const METADATA_UPDATABLE = true; // set to false if you don't want to allow metadata updates after minting 54 | const OWNER_ADDRESS = 'YOUR WALLET ADDRESS HERE'; 55 | const TREASURY_ADDRESS = 'YOUR WALLET ADDRESS HERE'; 56 | const MAX_SUPPLY = 5000; // The maximum number of NFTs that can be minted. CANNOT BE UPDATED! 57 | const MINT_PRICE = 0.01; // Minting price per NFT. Goerli = ETH, Ethereum = ETH, Polygon = MATIC. CANNOT BE UPDATED! 58 | const TOKENS_PER_MINT = 10; // maximum number of NFTs a user can mint in a single transaction. CANNOT BE UPDATED! 59 | 60 | // REQUIRED CONTRACT DETAILS THAT CAN BE UPDATED LATER. 61 | const PUBLIC_MINT_START_DATE = "2022-03-20T11:30:48+00:00"; // This is required. Eg: 2022-02-08T11:30:48+00:00 62 | 63 | // OPTIONAL CONTRACT DETAILS THAT CAN BE UPDATED LATER. 64 | const PRESALE_MINT_START_DATE = null; // Optional. Eg: 2022-02-08T11:30:48+00:00 65 | const ROYALTY_SHARE = 1000; // Percentage of the token price that goes to the royalty address. 100 bps = 1% 66 | const ROYALTY_ADDRESS = "0xd8B808A887326F45B2D0cd999709Aa6264CeF919"; // Address that will receive the royalty 67 | const BASE_URI = null; // only update if you want to manually set the base uri 68 | const PREREVEAL_TOKEN_URI = null; // only update if you want to manually set the prereveal token uri 69 | const PRESALE_WHITELISTED_ADDRESSES = []; // only update if you want to manually set the whitelisted addresses 70 | 71 | // ** OPTIONAL ** 72 | let CONTRACT_ADDRESS = "YOUR CONTRACT ADDRESS"; // If you want to manually include it 73 | 74 | // Generic Metadata is optional if you want to reveal your NFTs 75 | const GENERIC = true; // Set to true if you want to upload generic metas and reveal the real NFTs in the future 76 | const GENERIC_TITLE = CONTRACT_NAME; // Replace with what you want the generic titles to say if you want it to be different from the contract name. 77 | const GENERIC_DESCRIPTION = "REPLACE THIS"; // Replace with what you want the generic descriptions to say. 78 | const GENERIC_IMAGE = "https://ipfs.io/ipfs/QmUf9tDbkqnfHkQaMdFWSGAeXwVXWA61pFED7ypx4hcsfh"; // Replace with your generic image that will display for all NFTs pre-reveal. 79 | 80 | // Automatically set contract address if deployed using the deployContract.js script 81 | try { 82 | const rawContractData = fs.readFileSync( 83 | `${basePath}/build/contract/_contract.json` 84 | ); 85 | const contractData = JSON.parse(rawContractData); 86 | if (contractData.response === "OK") { 87 | CONTRACT_ADDRESS = contractData.contract_address; 88 | } 89 | } catch (error) { 90 | // Do nothing, falling back to manual contract address 91 | } 92 | // END NFTPort Info 93 | 94 | const solanaMetadata = { 95 | symbol: "YC", 96 | seller_fee_basis_points: 1000, // Define how much % you want from secondary market sales 1000 = 10% 97 | external_url: "https://www.youtube.com/c/hashlipsnft", 98 | creators: [ 99 | { 100 | address: "7fXNuer5sbZtaTEPhtJ5g5gNtuyRoKkvxdjEjEnPN4mC", 101 | share: 100, 102 | }, 103 | ], 104 | }; 105 | 106 | const gif = { 107 | export: false, 108 | repeat: 0, 109 | quality: 100, 110 | delay: 500, 111 | }; 112 | 113 | const text = { 114 | only: false, 115 | color: "#ffffff", 116 | size: 20, 117 | xGap: 40, 118 | yGap: 40, 119 | align: "left", 120 | baseline: "top", 121 | weight: "regular", 122 | family: "Courier", 123 | spacer: " => ", 124 | }; 125 | 126 | const pixelFormat = { 127 | ratio: 2 / 128, 128 | }; 129 | 130 | const background = { 131 | generate: true, 132 | brightness: "80%", 133 | static: false, 134 | default: "#000000", 135 | }; 136 | 137 | const rarityDelimiter = "#"; 138 | 139 | const uniqueDnaTorrance = 10000; 140 | 141 | const preview = { 142 | thumbPerRow: 5, 143 | thumbWidth: 50, 144 | imageRatio: format.height / format.width, 145 | imageName: "preview.png", 146 | }; 147 | 148 | const preview_gif = { 149 | numberOfImages: 5, 150 | order: "ASC", // ASC, DESC, MIXED 151 | repeat: 0, 152 | quality: 100, 153 | delay: 500, 154 | imageName: "preview.gif", 155 | }; 156 | 157 | module.exports = { 158 | format, 159 | baseUri, 160 | description, 161 | background, 162 | uniqueDnaTorrance, 163 | layerConfigurations, 164 | rarityDelimiter, 165 | preview, 166 | shuffleLayerConfigurations, 167 | debugLogs, 168 | extraMetadata, 169 | pixelFormat, 170 | text, 171 | namePrefix, 172 | network, 173 | solanaMetadata, 174 | gif, 175 | preview_gif, 176 | AUTH, 177 | LIMIT, 178 | CONTRACT_ADDRESS, 179 | OWNER_ADDRESS, 180 | TREASURY_ADDRESS, 181 | CHAIN, 182 | GENERIC, 183 | GENERIC_TITLE, 184 | GENERIC_DESCRIPTION, 185 | GENERIC_IMAGE, 186 | CONTRACT_NAME, 187 | CONTRACT_SYMBOL, 188 | METADATA_UPDATABLE, 189 | ROYALTY_SHARE, 190 | ROYALTY_ADDRESS, 191 | MAX_SUPPLY, 192 | MINT_PRICE, 193 | TOKENS_PER_MINT, 194 | PRESALE_MINT_START_DATE, 195 | PUBLIC_MINT_START_DATE, 196 | BASE_URI, 197 | PREREVEAL_TOKEN_URI, 198 | PRESALE_WHITELISTED_ADDRESSES 199 | }; 200 | -------------------------------------------------------------------------------- /backend/utils/nftport/updateContract.js: -------------------------------------------------------------------------------- 1 | (async () => { 2 | const fs = require("fs"); 3 | const basePath = process.cwd(); 4 | const yesno = require("yesno"); 5 | const { program } = require("commander"); 6 | 7 | program.option("-u, --update ", "Field to update"); 8 | 9 | program.parse(process.argv); 10 | 11 | const options = program.opts(); 12 | 13 | const { 14 | fetchNoRetry, 15 | } = require(`${basePath}/utils/functions/fetchWithRetry.js`); 16 | let { 17 | CHAIN, 18 | PUBLIC_MINT_START_DATE, 19 | PRESALE_MINT_START_DATE, 20 | CONTRACT_ADDRESS, 21 | BASE_URI, 22 | PREREVEAL_TOKEN_URI, 23 | ROYALTY_SHARE, 24 | ROYALTY_ADDRESS, 25 | PRESALE_WHITELISTED_ADDRESSES, 26 | } = require(`${basePath}/src/config.js`); 27 | 28 | const contract = { 29 | chain: CHAIN.toLowerCase(), 30 | contract_address: CONTRACT_ADDRESS, 31 | }; 32 | let updateValue = ""; 33 | 34 | const getWhiteList = async () => { 35 | // create promise 36 | return new Promise(async (resolve, reject) => { 37 | const url = `https://api.nftport.xyz/v0/me/contracts/collections?chain=${CHAIN.toLowerCase()}`; 38 | const options = { 39 | method: "GET", 40 | headers: { 41 | "Content-Type": "application/json", 42 | }, 43 | }; 44 | 45 | const response = await fetchNoRetry(url, options); 46 | if (response.response === "OK") { 47 | const contractInfo = response.contracts.filter( 48 | (contract) => 49 | contract.address.toLowerCase() === CONTRACT_ADDRESS.toLowerCase() 50 | ); 51 | const presaleWhitelistedAddresses = 52 | contractInfo[0]?.presale_whitelisted_addresses || []; 53 | resolve(presaleWhitelistedAddresses); 54 | } else { 55 | console.log(`Whitelist fetch failed!`); 56 | reject(response); 57 | } 58 | }); 59 | }; 60 | 61 | switch (options.update) { 62 | case "public_mint_start_date": 63 | contract.public_mint_start_date = PUBLIC_MINT_START_DATE; 64 | updateValue = PUBLIC_MINT_START_DATE; 65 | break; 66 | case "presale_mint_start_date": 67 | contract.presale_mint_start_date = PRESALE_MINT_START_DATE; 68 | updateValue = PRESALE_MINT_START_DATE; 69 | break; 70 | case "presale_whitelisted_addresses": 71 | const addresses_add = PRESALE_WHITELISTED_ADDRESSES.map((address) => 72 | address.toLowerCase() 73 | ); 74 | const existingAddresses = await getWhiteList(); 75 | contract.presale_whitelisted_addresses = [ 76 | ...existingAddresses, 77 | ...addresses_add, 78 | ]; 79 | updateValue = `include ${PRESALE_WHITELISTED_ADDRESSES}`; 80 | break; 81 | case "presale_whitelisted_addresses_remove": 82 | const addresses_remove = PRESALE_WHITELISTED_ADDRESSES.map((address) => 83 | address.toLowerCase() 84 | ); 85 | const existingAddressesList = await getWhiteList(); 86 | const updatedAddresses = existingAddressesList.filter((address) => !addresses_remove.includes(address)); 87 | contract.presale_whitelisted_addresses = updatedAddresses; 88 | updateValue = `remove ${PRESALE_WHITELISTED_ADDRESSES}`; 89 | break; 90 | case "royalty_share": 91 | contract.royalty_share = ROYALTY_SHARE; 92 | updateValue = ROYALTY_SHARE; 93 | break; 94 | case "royalty_address": 95 | contract.royalty_address = ROYALTY_ADDRESS; 96 | updateValue = ROYALTY_ADDRESS; 97 | break; 98 | case "base_uri": 99 | if (!BASE_URI) { 100 | try { 101 | let jsonFile = fs.readFileSync( 102 | `${basePath}/build/ipfsMetas/_ipfsMetasResponse.json` 103 | ); 104 | let metaData = JSON.parse(jsonFile); 105 | if (metaData.response === "OK") { 106 | BASE_URI = metaData.metadata_directory_ipfs_uri; 107 | } else { 108 | console.log( 109 | 'There is an issue with the metadata upload. Please check the /build/_ipfsMetas/_ipfsMetasResponse.json file for more information. Running "npm run upload_metadata" may fix this issue.' 110 | ); 111 | } 112 | } catch (err) { 113 | console.log( 114 | `/build/_ipfsMetasGeneric/_ipfsMetasResponse.json file not found. Run "npm run upload_metadata" first.` 115 | ); 116 | process.exit(0); 117 | } 118 | } 119 | contract.base_uri = BASE_URI; 120 | updateValue = BASE_URI; 121 | break; 122 | case "prereveal_token_uri": 123 | if (!PREREVEAL_TOKEN_URI) { 124 | try { 125 | let jsonFile = fs.readFileSync( 126 | `${basePath}/build/ipfsMetasGeneric/_ipfsMetasResponse.json` 127 | ); 128 | let metaData = JSON.parse(jsonFile); 129 | if (metaData.response === "OK") { 130 | PREREVEAL_TOKEN_URI = metaData.metadata_uri; 131 | } else { 132 | console.log( 133 | 'There is an issue with the metadata upload. Please check the /build/_ipfsMetasGeneric/_ipfsMetasResponse.json file for more information. Running "npm run upload_metadata" may fix this issue.' 134 | ); 135 | } 136 | } catch (err) { 137 | console.log( 138 | `/build/_ipfsMetasGeneric/_ipfsMetasResponse.json file not found. Run "npm run upload_metadata" first.` 139 | ); 140 | console.log(`Catch: ${err}`); 141 | process.exit(0); 142 | } 143 | } 144 | contract.prereveal_token_uri = PREREVEAL_TOKEN_URI; 145 | updateValue = PREREVEAL_TOKEN_URI; 146 | break; 147 | default: 148 | console.log("Invalid update statement. Exiting..."); 149 | process.exit(0); 150 | } 151 | 152 | const updateContract = async () => { 153 | const ok = await yesno({ 154 | question: `Updating ${options.update} to ${updateValue}, correct? (y/n):`, 155 | default: null, 156 | }); 157 | 158 | if (!ok) { 159 | console.log("Exiting..."); 160 | process.exit(0); 161 | } 162 | 163 | try { 164 | const url = `https://api.nftport.xyz/v0/contracts/collections`; 165 | const options = { 166 | method: "PUT", 167 | headers: { 168 | "Content-Type": "application/json", 169 | }, 170 | body: JSON.stringify(contract), 171 | }; 172 | const response = await fetchNoRetry(url, options); 173 | if (response.response === "OK") { 174 | console.log(`Contract Updated!`); 175 | } else { 176 | console.log(`Contract update failed!`); 177 | } 178 | } catch (error) { 179 | console.log(`CATCH: Contract update failed!`, `ERROR: ${error}`); 180 | } 181 | }; 182 | 183 | updateContract(); 184 | })(); 185 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CodeCats 5 | 6 | 7 | 8 | 9 | 10 | 15 | 21 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 |
35 | 40 | 66 |
67 |
68 | 69 |
70 |
71 |

Connect to MetaMask to Get Started

72 |

Welcome to the CodeCats NFT Project!

73 |

74 | The CodeCats NFT Project is a decentralized, open-source project that aims to demonstrate how to develope and launch your own NFT Collection. Follow step by step on the codeSTACKr YouTube channel. 75 |

76 |
77 |
78 | 79 |
80 |
81 |
82 |
    83 |
  • 84 | 20 85 |

    Days

    86 |
  • 87 | 88 |
  • 89 | 20 90 |

    Hour

    91 |
  • 92 | 93 |
  • 94 | 20 95 |

    Min

    96 |
  • 97 | 98 |
  • 99 | 20 100 |

    Sec

    101 |
  • 102 |
103 |

Pre-Sale Countdown

104 |
105 |

NFT Drop Coming Soon!!

106 |

107 | A new batch of cute cats will be available very soon! 108 |

109 | Join the Catmmunity 116 | 117 | 153 |
154 | 171 |
172 | 173 |
174 | 175 |
176 |
177 |
    178 |
  • 179 | 180 |
  • 181 |
  • 182 | 183 |
  • 184 |
  • 185 | 186 |
  • 187 |
  • 188 | 189 |
  • 190 |
  • 191 | 192 |
  • 193 |
  • 194 | 195 |
  • 196 |
  • 197 | 198 |
  • 199 |
  • 200 | 201 |
  • 202 |
  • 203 | 204 |
  • 205 |
206 |
207 |
208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | -------------------------------------------------------------------------------- /frontend/css/style.css: -------------------------------------------------------------------------------- 1 | /* FONTS */ 2 | 3 | @import url("https://fonts.googleapis.com/css2?family=Arvo:ital,wght@0,400;0,700;1,400;1,700&family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap"); 4 | 5 | /* GLOBAL STYLES */ 6 | 7 | :root { 8 | --font-primary: "Poppins", sans-serif; 9 | --font-secondary: "Arvo", serif; 10 | --font-code: "Roboto", monospace; 11 | --bg-color: #00163f; 12 | --bg-color-transparent: #00163f80; 13 | --font-color: #ffffff; 14 | --btn-bg-color: #ffffff; 15 | --accent-color: #be7efe; 16 | --header-shadow: var(--accent-color) 0px 0px 5px; 17 | --card-shadow: var(--accent-color) 0px 0px 20px; 18 | } 19 | 20 | * { 21 | padding: 0; 22 | margin: 0; 23 | box-sizing: border-box; 24 | } 25 | 26 | body { 27 | background-color: var(--bg-color); 28 | display: flex; 29 | flex-flow: column; 30 | height: 100vh; 31 | color: var(--font-color); 32 | font-family: var(--font-primary); 33 | } 34 | 35 | .container { 36 | display: flex; 37 | flex-direction: column; 38 | max-width: 960px; 39 | margin: 0 auto; 40 | padding: 1rem 2rem; 41 | justify-content: center; 42 | overflow: hidden; 43 | } 44 | 45 | /* HEADER */ 46 | 47 | header { 48 | background-color: var(--bg-color); 49 | box-shadow: var(--header-shadow); 50 | } 51 | 52 | header .container { 53 | flex-direction: row; 54 | justify-content: space-between; 55 | align-items: center; 56 | } 57 | 58 | .menu { 59 | display: flex; 60 | gap: 2rem; 61 | align-items: center; 62 | } 63 | 64 | /* NOT CONNECTED */ 65 | .not-connected { 66 | align-items: center; 67 | height: 100%; 68 | text-align: center; 69 | margin: 0 auto 0 35vw; 70 | opacity: 0; 71 | transform: translateY(10vh); 72 | transition: all 0.5s ease-in-out; 73 | } 74 | 75 | .show-not-connected { 76 | opacity: 1; 77 | transform: translateY(0); 78 | } 79 | 80 | .not-connected h1 { 81 | text-shadow: 2px 2px 0 var(--accent-color); 82 | font-style: italic; 83 | margin: 2rem 0; 84 | font-size: 3.5rem; 85 | } 86 | 87 | .not-connected p { 88 | font-size: 1.1rem; 89 | font-family: var(--font-secondary); 90 | line-height: 1.8; 91 | } 92 | 93 | .not-connected a { 94 | text-decoration: none; 95 | color: var(--accent-color); 96 | font-size: 1.5rem; 97 | font-weight: bold; 98 | } 99 | 100 | /* BUTTONS */ 101 | 102 | .btn { 103 | display: inline-block; 104 | padding: 1rem 2rem; 105 | background: var(--btn-bg-color); 106 | color: var(--accent-color); 107 | font-family: var(--font-secondary); 108 | font-style: italic; 109 | font-weight: bold; 110 | font-size: 1rem; 111 | text-align: center; 112 | text-transform: capitalize; 113 | transition: all 0.3s ease; 114 | box-shadow: var(--header-shadow); 115 | cursor: pointer; 116 | position: relative; 117 | text-decoration: none; 118 | outline: none; 119 | } 120 | .btn:hover { 121 | box-shadow: none; 122 | } 123 | .btn:disabled { 124 | cursor: default; 125 | } 126 | 127 | .wallet-btn { 128 | border-radius: 5rem; 129 | margin-left: 2rem; 130 | border-color: transparent; 131 | } 132 | 133 | .hero-btn { 134 | border-radius: 4px; 135 | font-size: 1.2rem; 136 | padding: 1.5rem 3rem; 137 | } 138 | 139 | /* CARD */ 140 | 141 | .card { 142 | position: relative; 143 | background-color: var(--bg-color); 144 | border-radius: 5px; 145 | border: 3px solid var(--accent-color); 146 | box-shadow: var(--card-shadow); 147 | padding: 5rem; 148 | margin-bottom: 2rem; 149 | opacity: 0; 150 | transform: translateX(100vw); 151 | transition: all 0.5s ease-in-out; 152 | } 153 | 154 | .show-card { 155 | opacity: 1; 156 | transform: translateX(0); 157 | } 158 | 159 | /* COUNTDOWN SECTION */ 160 | 161 | section { 162 | flex: 1 1 auto; 163 | } 164 | 165 | #countdown { 166 | display: none; 167 | text-align: center; 168 | } 169 | 170 | .countdown { 171 | display: flex; 172 | flex-direction: column; 173 | justify-content: center; 174 | align-items: center; 175 | } 176 | 177 | .countdown ul { 178 | display: flex; 179 | gap: 2rem; 180 | } 181 | .countdown .clock-item { 182 | list-style: none; 183 | width: 70px; 184 | height: auto; 185 | } 186 | .countdown .clock-item .count-number { 187 | background: var(--accent-color); 188 | color: var(--font-color); 189 | font-size: 2rem; 190 | font-weight: 700; 191 | font-family: var(--font-secondary); 192 | padding: 1rem; 193 | display: inline-block; 194 | width: 100%; 195 | text-align: center; 196 | line-height: 1; 197 | border-radius: 4px 4px 0 0; 198 | } 199 | .countdown .clock-item .count-text { 200 | background: var(--btn-bg-color); 201 | font-family: var(--font-secondary); 202 | font-size: 1.1rem; 203 | color: var(--bg-color); 204 | padding: 0.5rem 0.7rem; 205 | display: inline-block; 206 | width: 100%; 207 | text-align: center; 208 | line-height: 1; 209 | border-radius: 0 0 4px 4px; 210 | } 211 | 212 | .countdown h1 { 213 | text-shadow: 2px 2px 0 var(--accent-color); 214 | font-style: italic; 215 | margin: 2rem 0; 216 | font-size: 3.5rem; 217 | text-align: center; 218 | } 219 | 220 | .countdown h2 { 221 | margin-top: 1rem; 222 | } 223 | 224 | .countdown > p { 225 | font-size: 1.1rem; 226 | font-family: var(--font-secondary); 227 | margin-bottom: 2rem; 228 | text-align: center; 229 | } 230 | 231 | /* MINT CONTAINER */ 232 | 233 | .mint-container { 234 | width: 100%; 235 | display: flex; 236 | flex-direction: column; 237 | justify-content: center; 238 | } 239 | 240 | .info-container { 241 | display: flex; 242 | justify-content: space-between; 243 | width: 100%; 244 | text-align: center; 245 | margin-bottom: 2rem; 246 | } 247 | 248 | .mint-qty { 249 | display: flex; 250 | justify-content: space-between; 251 | align-items: center; 252 | padding: 1rem; 253 | background-color: var(--accent-color); 254 | border-radius: 5px; 255 | } 256 | 257 | .mint-qty > span { 258 | display: flex; 259 | } 260 | 261 | .mint-btn { 262 | display: flex; 263 | align-items: center; 264 | gap: 3rem; 265 | margin: auto; 266 | } 267 | 268 | .input-number { 269 | height: 100%; 270 | padding: 1rem; 271 | vertical-align: top; 272 | text-align: center; 273 | outline: none; 274 | } 275 | 276 | .input-number, 277 | .input-number-decrement, 278 | .input-number-increment { 279 | user-select: none; 280 | } 281 | 282 | .total-price-container { 283 | display: flex; 284 | justify-content: space-between; 285 | width: 100%; 286 | padding: 1rem 0 2rem; 287 | } 288 | 289 | .total-price-container p { 290 | color: var(--accent-color); 291 | } 292 | 293 | /* MINTED CONTAINER */ 294 | 295 | .minted-container { 296 | display: flex; 297 | flex-direction: column; 298 | justify-content: center; 299 | align-items: center; 300 | width: 100%; 301 | } 302 | 303 | .minted-container h1 { 304 | font-size: 5rem; 305 | margin-bottom: 2rem; 306 | } 307 | 308 | .minted-container h2 { 309 | font-size: 2rem; 310 | margin-bottom: 1rem; 311 | } 312 | 313 | .minted-container p { 314 | margin: 1rem 0; 315 | } 316 | 317 | /* SLIDDER */ 318 | 319 | .splide { 320 | position: absolute; 321 | top: 90px; 322 | left: 5rem; 323 | z-index: -1; 324 | } 325 | 326 | .splide__slide { 327 | border: var(--accent-color) solid; 328 | border-width: 5px 10px; 329 | } 330 | 331 | .splide__slide img { 332 | width: 100%; 333 | } 334 | 335 | /* SPINNER */ 336 | .dot-spin { 337 | position: absolute; 338 | top: 50%; 339 | left: 50%; 340 | transform: translate(-50%, -50%) scale(3); 341 | } 342 | 343 | /* MEDIA QUERIES */ 344 | 345 | @media screen and (max-width: 768px) { 346 | .menu { 347 | gap: 1rem; 348 | } 349 | .wallet-btn { 350 | margin-left: 0; 351 | padding: 0.5rem 1rem; 352 | max-width: 8rem; 353 | } 354 | .not-connected { 355 | background-color: var(--bg-color-transparent); 356 | border-radius: 5px; 357 | padding: 2rem; 358 | margin: 0 auto; 359 | } 360 | .countdown ul { 361 | display: grid; 362 | grid-template-columns: repeat(2, 1fr); 363 | } 364 | .countdown .clock-item { 365 | width: 100%; 366 | } 367 | .countdown .clock-item .count-number { 368 | font-size: 1.5rem; 369 | } 370 | .countdown .clock-item .count-text { 371 | font-size: 1rem; 372 | } 373 | .countdown h1 { 374 | font-size: 2rem; 375 | margin: 1rem 0; 376 | } 377 | .countdown h2 { 378 | padding-top: 0.5rem; 379 | } 380 | .countdown p { 381 | font-size: 1rem; 382 | } 383 | .card { 384 | padding: 1rem; 385 | } 386 | .mint-qty .btn { 387 | padding: 1rem; 388 | font-size: 13px; 389 | } 390 | } 391 | 392 | /* UTILITIES */ 393 | 394 | .hidden { 395 | display: none; 396 | } 397 | -------------------------------------------------------------------------------- /backend/src/main.js: -------------------------------------------------------------------------------- 1 | const basePath = process.cwd(); 2 | const { NETWORK } = require(`${basePath}/constants/network.js`); 3 | const fs = require("fs"); 4 | const sha1 = require(`${basePath}/node_modules/sha1`); 5 | const { createCanvas, loadImage } = require(`${basePath}/node_modules/canvas`); 6 | const buildDir = `${basePath}/build`; 7 | const layersDir = `${basePath}/layers`; 8 | const { 9 | format, 10 | baseUri, 11 | description, 12 | background, 13 | uniqueDnaTorrance, 14 | layerConfigurations, 15 | rarityDelimiter, 16 | shuffleLayerConfigurations, 17 | debugLogs, 18 | extraMetadata, 19 | text, 20 | namePrefix, 21 | network, 22 | solanaMetadata, 23 | gif, 24 | } = require(`${basePath}/src/config.js`); 25 | const canvas = createCanvas(format.width, format.height); 26 | const ctx = canvas.getContext("2d"); 27 | ctx.imageSmoothingEnabled = format.smoothing; 28 | var metadataList = []; 29 | var attributesList = []; 30 | var dnaList = new Set(); 31 | const DNA_DELIMITER = "-"; 32 | const HashlipsGiffer = require(`${basePath}/modules/HashlipsGiffer.js`); 33 | 34 | let hashlipsGiffer = null; 35 | 36 | const buildSetup = () => { 37 | if (fs.existsSync(buildDir)) { 38 | fs.rmdirSync(buildDir, { recursive: true }); 39 | } 40 | fs.mkdirSync(buildDir); 41 | fs.mkdirSync(`${buildDir}/json`); 42 | fs.mkdirSync(`${buildDir}/images`); 43 | if (gif.export) { 44 | fs.mkdirSync(`${buildDir}/gifs`); 45 | } 46 | }; 47 | 48 | const getRarityWeight = (_str) => { 49 | let nameWithoutExtension = _str.slice(0, -4); 50 | var nameWithoutWeight = Number( 51 | nameWithoutExtension.split(rarityDelimiter).pop() 52 | ); 53 | if (isNaN(nameWithoutWeight)) { 54 | nameWithoutWeight = 1; 55 | } 56 | return nameWithoutWeight; 57 | }; 58 | 59 | const cleanDna = (_str) => { 60 | const withoutOptions = removeQueryStrings(_str); 61 | var dna = Number(withoutOptions.split(":").shift()); 62 | return dna; 63 | }; 64 | 65 | const cleanName = (_str) => { 66 | let nameWithoutExtension = _str.slice(0, -4); 67 | var nameWithoutWeight = nameWithoutExtension.split(rarityDelimiter).shift(); 68 | return nameWithoutWeight; 69 | }; 70 | 71 | const getElements = (path) => { 72 | return fs 73 | .readdirSync(path) 74 | .filter((item) => !/(^|\/)\.[^\/\.]/g.test(item)) 75 | .map((i, index) => { 76 | return { 77 | id: index, 78 | name: cleanName(i), 79 | filename: i, 80 | path: `${path}${i}`, 81 | weight: getRarityWeight(i), 82 | }; 83 | }); 84 | }; 85 | 86 | const layersSetup = (layersOrder) => { 87 | const layers = layersOrder.map((layerObj, index) => ({ 88 | id: index, 89 | elements: getElements(`${layersDir}/${layerObj.name}/`), 90 | name: 91 | layerObj.options?.["displayName"] != undefined 92 | ? layerObj.options?.["displayName"] 93 | : layerObj.name, 94 | blend: 95 | layerObj.options?.["blend"] != undefined 96 | ? layerObj.options?.["blend"] 97 | : "source-over", 98 | opacity: 99 | layerObj.options?.["opacity"] != undefined 100 | ? layerObj.options?.["opacity"] 101 | : 1, 102 | bypassDNA: 103 | layerObj.options?.["bypassDNA"] !== undefined 104 | ? layerObj.options?.["bypassDNA"] 105 | : false, 106 | })); 107 | return layers; 108 | }; 109 | 110 | const saveImage = (_editionCount) => { 111 | fs.writeFileSync( 112 | `${buildDir}/images/${_editionCount}.png`, 113 | canvas.toBuffer("image/png") 114 | ); 115 | }; 116 | 117 | const genColor = () => { 118 | let hue = Math.floor(Math.random() * 360); 119 | let pastel = `hsl(${hue}, 100%, ${background.brightness})`; 120 | return pastel; 121 | }; 122 | 123 | const drawBackground = () => { 124 | ctx.fillStyle = background.static ? background.default : genColor(); 125 | ctx.fillRect(0, 0, format.width, format.height); 126 | }; 127 | 128 | const addMetadata = (_dna, _edition) => { 129 | let dateTime = Date.now(); 130 | let tempMetadata = { 131 | name: `${namePrefix} #${_edition}`, 132 | description: description, 133 | image: `${baseUri}/${_edition}.png`, 134 | attributes: attributesList, 135 | dna: sha1(_dna), 136 | edition: _edition, 137 | ...extraMetadata, 138 | date: dateTime, 139 | compiler: "HashLips Art Engine - codeSTACKr Modified", 140 | }; 141 | if (network == NETWORK.sol) { 142 | tempMetadata = { 143 | //Added metadata for solana 144 | name: tempMetadata.name, 145 | symbol: solanaMetadata.symbol, 146 | description: tempMetadata.description, 147 | //Added metadata for solana 148 | seller_fee_basis_points: solanaMetadata.seller_fee_basis_points, 149 | image: `image.png`, 150 | //Added metadata for solana 151 | external_url: solanaMetadata.external_url, 152 | edition: _edition, 153 | ...extraMetadata, 154 | attributes: tempMetadata.attributes, 155 | properties: { 156 | files: [ 157 | { 158 | uri: "image.png", 159 | type: "image/png", 160 | }, 161 | ], 162 | category: "image", 163 | creators: solanaMetadata.creators, 164 | }, 165 | }; 166 | } 167 | metadataList.push(tempMetadata); 168 | attributesList = []; 169 | }; 170 | 171 | const addAttributes = (_element) => { 172 | let selectedElement = _element.layer.selectedElement; 173 | attributesList.push({ 174 | trait_type: _element.layer.name, 175 | value: selectedElement.name, 176 | }); 177 | }; 178 | 179 | const loadLayerImg = async (_layer) => { 180 | return new Promise(async (resolve) => { 181 | const image = await loadImage(`${_layer.selectedElement.path}`); 182 | resolve({ layer: _layer, loadedImage: image }); 183 | }); 184 | }; 185 | 186 | const addText = (_sig, x, y, size) => { 187 | ctx.fillStyle = text.color; 188 | ctx.font = `${text.weight} ${size}pt ${text.family}`; 189 | ctx.textBaseline = text.baseline; 190 | ctx.textAlign = text.align; 191 | ctx.fillText(_sig, x, y); 192 | }; 193 | 194 | const drawElement = (_renderObject, _index, _layersLen) => { 195 | ctx.globalAlpha = _renderObject.layer.opacity; 196 | ctx.globalCompositeOperation = _renderObject.layer.blend; 197 | text.only 198 | ? addText( 199 | `${_renderObject.layer.name}${text.spacer}${_renderObject.layer.selectedElement.name}`, 200 | text.xGap, 201 | text.yGap * (_index + 1), 202 | text.size 203 | ) 204 | : ctx.drawImage( 205 | _renderObject.loadedImage, 206 | 0, 207 | 0, 208 | format.width, 209 | format.height 210 | ); 211 | 212 | addAttributes(_renderObject); 213 | }; 214 | 215 | const constructLayerToDna = (_dna = "", _layers = []) => { 216 | let mappedDnaToLayers = _layers.map((layer, index) => { 217 | let selectedElement = layer.elements.find( 218 | (e) => e.id == cleanDna(_dna.split(DNA_DELIMITER)[index]) 219 | ); 220 | return { 221 | name: layer.name, 222 | blend: layer.blend, 223 | opacity: layer.opacity, 224 | selectedElement: selectedElement, 225 | }; 226 | }); 227 | return mappedDnaToLayers; 228 | }; 229 | 230 | /** 231 | * In some cases a DNA string may contain optional query parameters for options 232 | * such as bypassing the DNA isUnique check, this function filters out those 233 | * items without modifying the stored DNA. 234 | * 235 | * @param {String} _dna New DNA string 236 | * @returns new DNA string with any items that should be filtered, removed. 237 | */ 238 | const filterDNAOptions = (_dna) => { 239 | const dnaItems = _dna.split(DNA_DELIMITER); 240 | const filteredDNA = dnaItems.filter((element) => { 241 | const query = /(\?.*$)/; 242 | const querystring = query.exec(element); 243 | if (!querystring) { 244 | return true; 245 | } 246 | const options = querystring[1].split("&").reduce((r, setting) => { 247 | const keyPairs = setting.split("="); 248 | return { ...r, [keyPairs[0]]: keyPairs[1] }; 249 | }, []); 250 | 251 | return options.bypassDNA; 252 | }); 253 | 254 | return filteredDNA.join(DNA_DELIMITER); 255 | }; 256 | 257 | /** 258 | * Cleaning function for DNA strings. When DNA strings include an option, it 259 | * is added to the filename with a ?setting=value query string. It needs to be 260 | * removed to properly access the file name before Drawing. 261 | * 262 | * @param {String} _dna The entire newDNA string 263 | * @returns Cleaned DNA string without querystring parameters. 264 | */ 265 | const removeQueryStrings = (_dna) => { 266 | const query = /(\?.*$)/; 267 | return _dna.replace(query, ""); 268 | }; 269 | 270 | const isDnaUnique = (_DnaList = new Set(), _dna = "") => { 271 | const _filteredDNA = filterDNAOptions(_dna); 272 | return !_DnaList.has(_filteredDNA); 273 | }; 274 | 275 | const createDna = (_layers) => { 276 | let randNum = []; 277 | _layers.forEach((layer) => { 278 | var totalWeight = 0; 279 | layer.elements.forEach((element) => { 280 | totalWeight += element.weight; 281 | }); 282 | // number between 0 - totalWeight 283 | let random = Math.floor(Math.random() * totalWeight); 284 | for (var i = 0; i < layer.elements.length; i++) { 285 | // subtract the current weight from the random weight until we reach a sub zero value. 286 | random -= layer.elements[i].weight; 287 | if (random < 0) { 288 | return randNum.push( 289 | `${layer.elements[i].id}:${layer.elements[i].filename}${ 290 | layer.bypassDNA ? "?bypassDNA=true" : "" 291 | }` 292 | ); 293 | } 294 | } 295 | }); 296 | return randNum.join(DNA_DELIMITER); 297 | }; 298 | 299 | const writeMetaData = (_data) => { 300 | fs.writeFileSync(`${buildDir}/json/_metadata.json`, _data); 301 | }; 302 | 303 | const saveMetaDataSingleFile = (_editionCount) => { 304 | let metadata = metadataList.find((meta) => meta.edition == _editionCount); 305 | debugLogs 306 | ? console.log( 307 | `Writing metadata for ${_editionCount}: ${JSON.stringify(metadata)}` 308 | ) 309 | : null; 310 | fs.writeFileSync( 311 | `${buildDir}/json/${_editionCount}.json`, 312 | JSON.stringify(metadata, null, 2) 313 | ); 314 | }; 315 | 316 | function shuffle(array) { 317 | let currentIndex = array.length, 318 | randomIndex; 319 | while (currentIndex != 0) { 320 | randomIndex = Math.floor(Math.random() * currentIndex); 321 | currentIndex--; 322 | [array[currentIndex], array[randomIndex]] = [ 323 | array[randomIndex], 324 | array[currentIndex], 325 | ]; 326 | } 327 | return array; 328 | } 329 | 330 | const startCreating = async () => { 331 | let layerConfigIndex = 0; 332 | let editionCount = 1; 333 | let failedCount = 0; 334 | let abstractedIndexes = []; 335 | for ( 336 | let i = network == NETWORK.sol ? 0 : 0; 337 | i <= layerConfigurations[layerConfigurations.length - 1].growEditionSizeTo; 338 | i++ 339 | ) { 340 | abstractedIndexes.push(i); 341 | } 342 | if (shuffleLayerConfigurations) { 343 | abstractedIndexes = shuffle(abstractedIndexes); 344 | } 345 | debugLogs 346 | ? console.log("Editions left to create: ", abstractedIndexes) 347 | : null; 348 | while (layerConfigIndex < layerConfigurations.length) { 349 | const layers = layersSetup( 350 | layerConfigurations[layerConfigIndex].layersOrder 351 | ); 352 | while ( 353 | editionCount <= layerConfigurations[layerConfigIndex].growEditionSizeTo 354 | ) { 355 | let newDna = createDna(layers); 356 | if (isDnaUnique(dnaList, newDna)) { 357 | let results = constructLayerToDna(newDna, layers); 358 | let loadedElements = []; 359 | 360 | results.forEach((layer) => { 361 | loadedElements.push(loadLayerImg(layer)); 362 | }); 363 | 364 | await Promise.all(loadedElements).then((renderObjectArray) => { 365 | debugLogs ? console.log("Clearing canvas") : null; 366 | ctx.clearRect(0, 0, format.width, format.height); 367 | if (gif.export) { 368 | hashlipsGiffer = new HashlipsGiffer( 369 | canvas, 370 | ctx, 371 | `${buildDir}/gifs/${abstractedIndexes[0]}.gif`, 372 | gif.repeat, 373 | gif.quality, 374 | gif.delay 375 | ); 376 | hashlipsGiffer.start(); 377 | } 378 | if (background.generate) { 379 | drawBackground(); 380 | } 381 | renderObjectArray.forEach((renderObject, index) => { 382 | drawElement( 383 | renderObject, 384 | index, 385 | layerConfigurations[layerConfigIndex].layersOrder.length 386 | ); 387 | if (gif.export) { 388 | hashlipsGiffer.add(); 389 | } 390 | }); 391 | if (gif.export) { 392 | hashlipsGiffer.stop(); 393 | } 394 | debugLogs 395 | ? console.log("Editions left to create: ", abstractedIndexes) 396 | : null; 397 | saveImage(abstractedIndexes[0]); 398 | addMetadata(newDna, abstractedIndexes[0]); 399 | saveMetaDataSingleFile(abstractedIndexes[0]); 400 | console.log( 401 | `Created edition: ${abstractedIndexes[0]}, with DNA: ${sha1( 402 | newDna 403 | )}` 404 | ); 405 | }); 406 | dnaList.add(filterDNAOptions(newDna)); 407 | editionCount++; 408 | abstractedIndexes.shift(); 409 | } else { 410 | console.log("DNA exists!"); 411 | failedCount++; 412 | if (failedCount >= uniqueDnaTorrance) { 413 | console.log( 414 | `You need more layers or elements to grow your edition to ${layerConfigurations[layerConfigIndex].growEditionSizeTo} artworks!` 415 | ); 416 | process.exit(); 417 | } 418 | } 419 | } 420 | layerConfigIndex++; 421 | } 422 | writeMetaData(JSON.stringify(metadataList, null, 2)); 423 | }; 424 | 425 | module.exports = { startCreating, buildSetup, getElements }; 426 | -------------------------------------------------------------------------------- /frontend/js/app.js: -------------------------------------------------------------------------------- 1 | let accounts; 2 | 3 | // METAMASK CONNECTION 4 | window.addEventListener("DOMContentLoaded", async () => { 5 | const welcomeH1 = document.getElementById("welcomeH1"); 6 | const welcomeH2 = document.getElementById("welcomeH2"); 7 | const welcomeP = document.getElementById("welcomeP"); 8 | 9 | welcomeH1.innerText = welcome_h1; 10 | welcomeH2.innerText = welcome_h2; 11 | welcomeP.innerHTML = welcome_p; 12 | 13 | if (window.ethereum) { 14 | window.web3 = new Web3(window.ethereum); 15 | checkChain(); 16 | } else if (window.web3) { 17 | window.web3 = new Web3(window.web3.currentProvider); 18 | } 19 | 20 | if (window.web3) { 21 | // Check if User is already connected by retrieving the accounts 22 | await window.web3.eth.getAccounts().then(async (addr) => { 23 | accounts = addr; 24 | }); 25 | } 26 | 27 | const splide = new Splide(".splide", { 28 | type: "loop", 29 | arrows: false, 30 | perMove: 3, 31 | pagination: false, 32 | autoplay: true, 33 | direction: 'ttb', 34 | height: "calc(100vh - 90px)", 35 | width: '30vw', 36 | autoHeight: true, 37 | }); 38 | splide.mount(); 39 | 40 | updateConnectStatus(); 41 | if (MetaMaskOnboarding.isMetaMaskInstalled()) { 42 | window.ethereum.on("accountsChanged", (newAccounts) => { 43 | accounts = newAccounts; 44 | updateConnectStatus(); 45 | }); 46 | } 47 | }); 48 | 49 | const updateConnectStatus = async () => { 50 | const onboarding = new MetaMaskOnboarding(); 51 | const onboardButton = document.getElementById("connectWallet"); 52 | const notConnected = document.querySelector('.not-connected'); 53 | const spinner = document.getElementById("spinner"); 54 | if (!MetaMaskOnboarding.isMetaMaskInstalled()) { 55 | onboardButton.innerText = "Install MetaMask!"; 56 | onboardButton.onclick = () => { 57 | onboardButton.innerText = "Connecting..."; 58 | onboardButton.disabled = true; 59 | onboarding.startOnboarding(); 60 | // HIDE SPINNER 61 | spinner.classList.add('hidden'); 62 | notConnected.classList.remove('hidden'); 63 | notConnected.classList.add('show-not-connected'); 64 | }; 65 | } else if (accounts && accounts.length > 0) { 66 | onboardButton.innerText = `✔ ...${accounts[0].slice(-4)}`; 67 | window.address = accounts[0]; 68 | onboardButton.disabled = true; 69 | onboarding.stopOnboarding(); 70 | notConnected.classList.remove('show-not-connected'); 71 | notConnected.classList.add('hidden'); 72 | // SHOW SPINNER 73 | spinner.classList.remove('hidden'); 74 | window.contract = new web3.eth.Contract(abi, contractAddress); 75 | loadInfo(); 76 | } else { 77 | onboardButton.innerText = "Connect MetaMask!"; 78 | // HIDE SPINNER 79 | spinner.classList.add('hidden'); 80 | notConnected.classList.remove('hidden'); 81 | notConnected.classList.add('show-not-connected'); 82 | onboardButton.onclick = async () => { 83 | await window.ethereum 84 | .request({ 85 | method: "eth_requestAccounts", 86 | }) 87 | .then(function (accts) { 88 | onboardButton.innerText = `✔ ...${accts[0].slice(-4)}`; 89 | notConnected.classList.remove('show-not-connected'); 90 | notConnected.classList.add('hidden'); 91 | // SHOW SPINNER 92 | spinner.classList.remove('hidden'); 93 | onboardButton.disabled = true; 94 | window.address = accts[0]; 95 | accounts = accts; 96 | window.contract = new web3.eth.Contract(abi, contractAddress); 97 | loadInfo(); 98 | }); 99 | }; 100 | } 101 | }; 102 | 103 | async function checkChain() { 104 | let chainId = 0; 105 | if(chain === 'goerli') { 106 | chainId = 5; 107 | } else if(chain === 'polygon') { 108 | chainId = 137; 109 | } else if(chain === 'ethereum') { 110 | chainId = 1; 111 | } 112 | if (window.ethereum.networkVersion !== chainId) { 113 | try { 114 | await window.ethereum.request({ 115 | method: 'wallet_switchEthereumChain', 116 | params: [{ chainId: web3.utils.toHex(chainId) }], 117 | }); 118 | updateConnectStatus(); 119 | } catch (err) { 120 | // This error code indicates that the chain has not been added to MetaMask. 121 | if (err.code === 4902) { 122 | try { 123 | if(chain === 'goerli') { 124 | await window.ethereum.request({ 125 | method: 'wallet_addEthereumChain', 126 | params: [ 127 | { 128 | chainName: 'Goerli Test Network', 129 | chainId: web3.utils.toHex(chainId), 130 | nativeCurrency: { name: 'ETH', decimals: 18, symbol: 'ETH' }, 131 | rpcUrls: ['https://goerli.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161'], 132 | }, 133 | ], 134 | }); 135 | } else if(chain === 'polygon') { 136 | await window.ethereum.request({ 137 | method: 'wallet_addEthereumChain', 138 | params: [ 139 | { 140 | chainName: 'Polygon Mainnet', 141 | chainId: web3.utils.toHex(chainId), 142 | nativeCurrency: { name: 'MATIC', decimals: 18, symbol: 'MATIC' }, 143 | rpcUrls: ['https://polygon-rpc.com/'], 144 | }, 145 | ], 146 | }); 147 | } 148 | updateConnectStatus(); 149 | } catch (err) { 150 | console.log(err); 151 | } 152 | } 153 | } 154 | } 155 | } 156 | 157 | async function loadInfo() { 158 | window.info = await window.contract.methods.getInfo().call(); 159 | const publicMintActive = await contract.methods.mintingActive().call(); 160 | const presaleMintActive = await contract.methods.presaleActive().call(); 161 | const mainHeading = document.getElementById("mainHeading"); 162 | const subHeading = document.getElementById("subHeading"); 163 | const mainText = document.getElementById("mainText"); 164 | const actionButton = document.getElementById("actionButton"); 165 | const mintContainer = document.getElementById("mintContainer"); 166 | const mintButton = document.getElementById("mintButton"); 167 | const spinner = document.getElementById("spinner"); 168 | 169 | let startTime = ""; 170 | if (publicMintActive) { 171 | mainHeading.innerText = h1_public_mint; 172 | mainText.innerText = p_public_mint; 173 | actionButton.classList.add('hidden'); 174 | mintButton.innerText = button_public_mint; 175 | mintContainer.classList.remove('hidden'); 176 | setTotalPrice(); 177 | } else if (presaleMintActive) { 178 | startTime = window.info.runtimeConfig.publicMintStart; 179 | mainHeading.innerText = h1_presale_mint; 180 | subHeading.innerText = h2_presale_mint; 181 | 182 | try { 183 | // CHECK IF WHITELISTED 184 | const merkleData = await fetch( 185 | `/.netlify/functions/merkleProof/?wallet=${window.address}&chain=${chain}&contract=${contractAddress}` 186 | ); 187 | const merkleJson = await merkleData.json(); 188 | const whitelisted = await contract.methods.isWhitelisted(window.address, merkleJson).call(); 189 | if(!whitelisted) { 190 | mainText.innerText = p_presale_mint_not_whitelisted; 191 | actionButton.innerText = button_presale_mint_not_whitelisted; 192 | } else { 193 | mainText.innerText = p_presale_mint_whitelisted; 194 | actionButton.classList.add('hidden'); 195 | mintButton.innerText = button_presale_mint_whitelisted; 196 | mintContainer.classList.remove('hidden'); 197 | } 198 | } catch(e) { 199 | // console.log(e); 200 | mainText.innerText = p_presale_mint_already_minted; 201 | actionButton.innerText = button_presale_already_minted; 202 | } 203 | setTotalPrice(); 204 | } else { 205 | startTime = window.info.runtimeConfig.presaleMintStart; 206 | mainHeading.innerText = h1_presale_coming_soon; 207 | subHeading.innerText = h2_presale_coming_soon; 208 | mainText.innerText = p_presale_coming_soon; 209 | actionButton.innerText = button_presale_coming_soon; 210 | } 211 | 212 | const clockdiv = document.getElementById("countdown"); 213 | clockdiv.setAttribute("data-date", startTime); 214 | countdown(); 215 | 216 | // HIDE SPINNER 217 | spinner.classList.add('hidden'); 218 | 219 | // SHOW CARD 220 | setTimeout(() => { 221 | const countdownCard = document.querySelector('.countdown'); 222 | countdownCard.classList.add('show-card'); 223 | }, 1000); 224 | 225 | let priceType = ''; 226 | if(chain === 'goerli' || chain === 'ethereum') { 227 | priceType = 'ETH'; 228 | } else if (chain === 'polygon') { 229 | priceType = 'MATIC'; 230 | } 231 | const price = web3.utils.fromWei(info.deploymentConfig.mintPrice, 'ether'); 232 | const pricePerMint = document.getElementById("pricePerMint"); 233 | const maxPerMint = document.getElementById("maxPerMint"); 234 | const totalSupply = document.getElementById("totalSupply"); 235 | const mintInput = document.getElementById("mintInput"); 236 | 237 | pricePerMint.innerText = `${price} ${priceType}`; 238 | maxPerMint.innerText = `${info.deploymentConfig.tokensPerMint}`; 239 | totalSupply.innerText = `${info.deploymentConfig.maxSupply}`; 240 | mintInput.setAttribute("max", info.deploymentConfig.tokensPerMint); 241 | 242 | // MINT INPUT 243 | const mintIncrement = document.getElementById("mintIncrement"); 244 | const mintDecrement = document.getElementById("mintDecrement"); 245 | const setQtyMax = document.getElementById("setQtyMax"); 246 | const min = mintInput.attributes.min.value || false; 247 | const max = mintInput.attributes.max.value || false; 248 | mintDecrement.onclick = () => { 249 | let value = parseInt(mintInput.value) - 1 || 1; 250 | if(!min || value >= min) { 251 | mintInput.value = value; 252 | setTotalPrice() 253 | } 254 | }; 255 | mintIncrement.onclick = () => { 256 | let value = parseInt(mintInput.value) + 1 || 1; 257 | if(!max || value <= max) { 258 | mintInput.value = value; 259 | setTotalPrice() 260 | } 261 | }; 262 | setQtyMax.onclick = () => { 263 | mintInput.value = max; 264 | setTotalPrice() 265 | }; 266 | mintInput.onchange = () => { 267 | setTotalPrice() 268 | }; 269 | mintInput.onkeyup = async (e) => { 270 | if (e.keyCode === 13) { 271 | mint(); 272 | } 273 | }; 274 | mintButton.onclick = mint; 275 | } 276 | 277 | function setTotalPrice() { 278 | const mintInput = document.getElementById("mintInput"); 279 | const mintInputValue = parseInt(mintInput.value); 280 | const totalPrice = document.getElementById("totalPrice"); 281 | const mintButton = document.getElementById("mintButton"); 282 | if(mintInputValue < 1 || mintInputValue > info.deploymentConfig.tokensPerMint) { 283 | totalPrice.innerText = 'INVALID QUANTITY'; 284 | mintButton.disabled = true; 285 | mintInput.disabled = true; 286 | return; 287 | } 288 | const totalPriceWei = BigInt(info.deploymentConfig.mintPrice) * BigInt(mintInputValue); 289 | 290 | let priceType = ''; 291 | if(chain === 'goerli' || chain === 'ethereum') { 292 | priceType = 'ETH'; 293 | } else if (chain === 'polygon') { 294 | priceType = 'MATIC'; 295 | } 296 | const price = web3.utils.fromWei(totalPriceWei.toString(), 'ether'); 297 | totalPrice.innerText = `${price} ${priceType}`; 298 | mintButton.disabled = false; 299 | mintInput.disabled = false; 300 | } 301 | 302 | async function mint() { 303 | const mintButton = document.getElementById("mintButton"); 304 | mintButton.disabled = true; 305 | const spinner = '
Waiting for transaction...'; 306 | mintButton.innerHTML = spinner; 307 | 308 | const amount = parseInt(document.getElementById("mintInput").value); 309 | const value = BigInt(info.deploymentConfig.mintPrice) * BigInt(amount); 310 | const publicMintActive = await contract.methods.mintingActive().call(); 311 | const presaleMintActive = await contract.methods.presaleActive().call(); 312 | 313 | if (publicMintActive) { 314 | // PUBLIC MINT 315 | try { 316 | const mintTransaction = await contract.methods 317 | .mint(amount) 318 | .send({ from: window.address, value: value.toString() }); 319 | if(mintTransaction) { 320 | if(chain === 'goerli') { 321 | const url = `https://goerli.etherscan.io/tx/${mintTransaction.transactionHash}`; 322 | const mintedContainer = document.querySelector('.minted-container'); 323 | const countdownContainer = document.querySelector('.countdown'); 324 | const mintedTxnBtn = document.getElementById("mintedTxnBtn"); 325 | mintedTxnBtn.href = url; 326 | countdownContainer.classList.add('hidden'); 327 | mintedContainer.classList.remove('hidden'); 328 | } 329 | console.log("Minted successfully!", `Transaction Hash: ${mintTransaction.transactionHash}`); 330 | } else { 331 | const mainText = document.getElementById("mainText"); 332 | mainText.innerText = mint_failed; 333 | mintButton.innerText = button_public_mint; 334 | mintButton.disabled = false; 335 | 336 | console.log("Failed to mint!"); 337 | } 338 | } catch(e) { 339 | const mainText = document.getElementById("mainText"); 340 | mainText.innerText = mint_failed; 341 | mintButton.innerText = button_public_mint; 342 | mintButton.disabled = false; 343 | 344 | console.log(e); 345 | } 346 | } else if (presaleMintActive) { 347 | // PRE-SALE MINTING 348 | try { 349 | const merkleData = await fetch( 350 | `/.netlify/functions/merkleProof/?wallet=${window.address}&chain=${chain}&contract=${contractAddress}` 351 | ); 352 | const merkleJson = await merkleData.json(); 353 | const presaleMintTransaction = await contract.methods 354 | .presaleMint(amount, merkleJson) 355 | .send({ from: window.address, value: value.toString() }); 356 | if(presaleMintTransaction) { 357 | if(chain === 'goerli') { 358 | const url = `https://goerli.etherscan.io/tx/${presaleMintTransaction.transactionHash}`; 359 | const mintedContainer = document.querySelector('.minted-container'); 360 | const countdownContainer = document.querySelector('.countdown'); 361 | const mintedTxnBtn = document.getElementById("mintedTxnBtn"); 362 | mintedTxnBtn.href = url; 363 | countdownContainer.classList.add('hidden'); 364 | mintedContainer.classList.remove('hidden'); 365 | } 366 | console.log("Minted successfully!", `Transaction Hash: ${presaleMintTransaction.transactionHash}`); 367 | } else { 368 | const mainText = document.getElementById("mainText"); 369 | mainText.innerText = mint_failed; 370 | mintButton.innerText = button_presale_mint_whitelisted; 371 | mintButton.disabled = false; 372 | 373 | console.log("Failed to mint!"); 374 | } 375 | } catch(e) { 376 | const mainText = document.getElementById("mainText"); 377 | mainText.innerText = mint_failed; 378 | mintButton.innerText = button_presale_mint_whitelisted; 379 | mintButton.disabled = false; 380 | 381 | // console.log(e); 382 | } 383 | } 384 | } 385 | -------------------------------------------------------------------------------- /frontend/js/splide.min.js: -------------------------------------------------------------------------------- 1 | function At(n,t){for(var i=0;i=u))return l();v(s)}}function l(){f=!0}function d(){cancelAnimationFrame(n),f=!(n=c=0)}return{start:function(n){n||d(),o=e()-(n?c*t:0),f=!1,v(s)},rewind:function(){o=e(),c=0,r&&r(c)},pause:l,cancel:d,set:function(n){t=n},isPaused:function(){return f}}}function d(n){var t=n;return{set:function(n){t=n},is:function(n){return x(w(n),t)}}}function Dn(i,r){var u;return function(){var n=arguments,t=this;u||(u=Rn(r||0,function(){i.apply(t,n),u=null},null,1)).start()}}var h={marginRight:["marginBottom","marginLeft"],autoWidth:["autoHeight"],fixedWidth:["fixedHeight"],paddingLeft:["paddingTop","paddingRight"],paddingRight:["paddingBottom","paddingLeft"],width:["height"],left:["top","right"],right:["bottom","left"],x:["y"],X:["Y"],Y:["X"],ArrowLeft:["ArrowUp","ArrowRight"],ArrowRight:["ArrowDown","ArrowLeft"]};var On=m,Mn=m+"__slider",Tn=m+"__track",jn=m+"__list",Fn=m+"__slide",In=Fn+"--clone",Wn=Fn+"__container",Cn=m+"__arrows",t=m+"__arrow",Nn=t+"--prev",Xn=t+"--next",i=m+"__pagination",Bn=m+"__progress",Gn=Bn+"__bar",Hn=m+"__autoplay",Yn=m+"__play",Un=m+"__pause",qn="is-active",Jn="is-prev",Kn="is-next",Vn="is-visible",Qn="is-loading",Zn=[qn,Vn,Jn,Kn,Qn];var $n="role",nt="aria-controls",tt="aria-current",it="aria-label",rt="aria-hidden",ut="tabindex",p="aria-orientation",ot=[$n,nt,tt,it,rt,p,ut,"disabled"],et="slide",ct="loop",ft="fade";function at(u,i,r,o){var t,n=Pn(u),e=n.on,c=n.emit,f=n.bind,a=n.destroy,s=u.Components,l=u.root,d=u.options,v=d.isNavigation,h=d.updateOnMove,p=s.Direction.resolve,g=X(o,"style"),m=-1o.perPage}}},Layout:function(n,t,i){var r,u,o=Pn(n),e=o.on,c=o.bind,f=o.emit,a=t.Slides,s=t.Direction.resolve,l=(t=t.Elements).root,d=t.track,v=t.list,h=a.getAt;function p(){u=null,r="ttb"===i.direction,C(l,"maxWidth",V(i.width)),C(d,s("paddingLeft"),m(!1)),C(d,s("paddingRight"),m(!0)),g()}function g(){var n=G(l);u&&u.width===n.width&&u.height===n.height||(C(d,"height",function(){var n="";r&&(Q(n=y(),"height or heightRatio is missing."),n="calc("+n+" - "+m(!1)+" - "+m(!0)+")");return n}()),a.style(s("marginRight"),V(i.gap)),a.style("width",(i.autoWidth?"":V(i.fixedWidth)||(r?"":w()))||null),a.style("height",V(i.fixedHeight)||(r?i.autoHeight?"":w():y())||null,!0),u=n,f(An))}function m(n){var t=i.padding,n=s(n?"right":"left");return t&&V(t[n]||(I(t)?0:t))||"0px"}function y(){return V(i.height||G(v).width*i.heightRatio)}function w(){var n=V(i.gap);return"calc((100%"+(n&&" + "+n)+")/"+(i.perPage||1)+(n&&" - "+n)+")"}function _(n,t){var i=h(n);if(i){n=G(i.slide)[s("right")],i=G(v)[s("left")];return un(n-i)+(t?0:b())}return 0}function b(){var n=h(0);return n&&parseFloat(C(n.slide,s("marginRight")))||0}return{mount:function(){p(),c(window,"resize load",Dn(f.bind(this,kn))),e([xn,bn],p),e(kn,g)},listSize:function(){return G(v)[s("width")]},slideSize:function(n,t){return(n=h(n||0))?G(n.slide)[s("width")]+(t?0:b()):0},sliderSize:function(){return _(n.length-1,!0)-_(-1,!0)},totalSize:_,getPadding:function(n){return parseFloat(C(d,s("padding"+(n?"Right":"Left"))))||0}}},Clones:function(c,n,f){var t,i=Pn(c),r=i.on,u=i.emit,a=n.Elements,s=n.Slides,o=n.Direction.resolve,l=[];function e(){(t=p())&&(function(u){var o=s.get().slice(),e=o.length;if(e){for(;o.lengthv(x(!0));return i||t}return{mount:function(){a=c.Transition,t([ln,An,xn,bn],g)},destroy:function(){T(h,"style")},move:function(n,t,i,r){var u,o;k()||(u=e.state.set,o=b(),n!==t&&(a.cancel(),y(w(o,t":"<",!1,r):r&&r()}))},jump:m,translate:y,shift:w,cancel:function(){y(b()),a.cancel()},toIndex:function(n){for(var t=c.Slides.get(),i=0,r=1/0,u=0;u])(\d+)?/)||[],i=r[1],r=r[2],"+"===i||"-"===i?t=x(p+ +(""+i+(+r||1)),p,!0):">"===i?t=r?L(+r):w(!0):"<"===i&&(t=_(!0))):t=v?n:en(n,0,k())}return t}(n);u.useScroll?y(r,!0,!0,u.speed,i):-1<(n=A(r))&&!a.isBusy()&&(t||n!==p)&&(z(n),a.move(r,n,g,i))},scroll:y,getNext:w,getPrev:_,getAdjacent:b,getEnd:k,setIndex:z,getIndex:function(n){return n?g:p},toIndex:L,toPage:E,toDest:S,hasFocus:P}},Arrows:function(u,n,i){var r,t=Pn(u),o=t.on,e=t.bind,c=t.emit,f=i.classes,a=i.i18n,s=n.Elements,l=n.Controller,d=s.arrows,v=s.prev,h=s.next,p={};function g(){var n,t;i.arrows&&(v&&h||(d=F("div",f.arrows),v=m(!0),h=m(!1),r=!0,L(d,[v,h]),S(d,P("slider"===i.arrows&&s.slider||u.root)))),v&&h&&(p.prev?N(d,!1===i.arrows?"none":""):(n=s.track.id,j(v,nt,n),j(h,nt,n),p.prev=v,p.next=h,t=l.go,o([ln,vn,xn,bn,En],y),e(h,"click",function(){t(">",!0)}),e(v,"click",function(){t("<",!0)}),c("arrows:mounted",v,h)))}function m(n){return Y('