-
-
Save tolicodes/2981264987c7ce0be5ccc065e2506c81 to your computer and use it in GitHub Desktop.
Offchain Whitelist
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//SPDX-License-Identifier: Unlicense | |
pragma solidity >=0.7.0 <0.9.0; | |
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; | |
import "@openzeppelin/contracts/access/Ownable.sol"; | |
import "@openzeppelin/contracts/utils/Strings.sol"; | |
import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; | |
// for whitelist and yellowlist server signatures | |
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; | |
contract YellowDuckCollection is ERC721Enumerable, Ownable { | |
using Strings for uint256; | |
// for whitelist and yellowlist server signatures | |
using ECDSA for bytes32; | |
// metadata | |
string public baseURI; | |
string public placeholderURI; | |
string private baseExtension = ".json"; | |
// cost of token | |
// change using setCost | |
uint256 public cost = 0.12 ether; | |
// max total ducks | |
uint256 public maxSupply = 999; | |
// max yellowlist supply | |
// uint256 public maxYellowlistSupply = 111; | |
// running supply | |
uint256 public supply = 0; | |
// max mints per whitelisted address | |
uint256 public maxWhitelistMintAmount = 3; | |
// max public sale mints per address | |
uint256 public maxPublicSaleMintAmount = 5; | |
// number of mints per address for whitelist | |
mapping(address => uint8) whitelistAddressMintedAmount; | |
// number of mints per address for public sale | |
mapping(address => uint8) publicSaleAddressMintedAmount; | |
// 0: yellowlist | |
// 1: whitelist | |
// 2: public | |
// 3: ended | |
uint8 public saleState = 1; | |
// yellowlist token IDs that have already been minted | |
mapping(uint256 => bool) private mintedTokenIds; | |
// yellowlist server signatures already used to mint a token (whitelist or yellow list) | |
mapping(bytes => bool) private usedYellowlistBackendSignatures; | |
// adddress for server signing for yellowlist or whitelist | |
address private adminWalletAddress = | |
0x4b9F8c0c724d231401Fc5709a956a4159f48754e; | |
// keeps track of next sequential token ID | |
uint256 private _nextTokenId = 114; | |
bool private isRevealed = false; | |
constructor( | |
string memory _name, | |
string memory _symbol | |
) ERC721(_name, _symbol) { | |
} | |
// UTILITY FUNCTIONS | |
// use this function to get the hash of any string | |
function _getHash(string memory str) internal pure returns (bytes32) { | |
string memory prefix = "\x19Ethereum Signed Message:\n"; | |
string memory stringLength = Strings.toString(bytes(str).length); | |
bytes memory abiPackedConcattedBytes = bytes.concat( | |
bytes(prefix), | |
bytes(stringLength), | |
bytes(str) | |
); | |
bytes32 ret = keccak256(abiPackedConcattedBytes); | |
return ret; | |
} | |
// used to verify signature from backend for yellowlist and whitelist | |
function _verifyBackendSignature( | |
string memory message, | |
bytes memory signature | |
) public view returns (bool) { | |
require( | |
!usedYellowlistBackendSignatures[signature], | |
"Signature has already been used to mint" | |
); | |
//we hash it on our side to verify the message is the one signed for | |
bytes32 messageHash = _getHash(message); | |
//check the signing address of the message did in fact sign that message message | |
address signingAddress = ECDSA.recover(messageHash, signature); | |
//verify that the message hash was signed by the admin wallet addreess | |
bool valid = signingAddress == adminWalletAddress; | |
require(valid, "Message not signed"); | |
return true; | |
} | |
// gets the next token id to mint | |
function _getNextTokenId() private returns (uint256) { | |
// we start at the current nextTokenId | |
for (uint256 i = _nextTokenId; i <= maxSupply; i++) { | |
// this begins token IDs at 1 | |
_nextTokenId++; | |
// inject solidity | |
// if the token has already been minted (via yellowlist), we keep going | |
if (!mintedTokenIds[_nextTokenId]) { | |
// otherwise, that's the ID we are gonna mint! | |
return _nextTokenId; | |
} | |
} | |
// there are no more tokens! | |
require(false, "All tokens have been minted"); | |
return 0; | |
} | |
// ADMIN FUNCTIONS | |
// requires yellowlist state | |
modifier requireYellowlistState() { | |
require(saleState == 0, "Not in yellowlist sale"); | |
_; | |
} | |
// requires yellowlist state | |
modifier requireWhitelistState() { | |
require(saleState == 1, "Not in whitelist sale"); | |
_; | |
} | |
// requires yellowlist state | |
modifier _requirePublicSaleState() { | |
require(saleState == 2, "Not in public sale"); | |
_; | |
} | |
// transition to yellowlist state | |
function reveal() public onlyOwner { | |
isRevealed = true; | |
} | |
// end sale | |
function endSale() public onlyOwner { | |
saleState = 3; | |
} | |
// transition to yellowlist state | |
function enableYellowlistState() public onlyOwner { | |
saleState = 0; | |
cost = 0.12 ether; | |
} | |
// transition to whitelist state | |
function enableWhitelistState() public onlyOwner { | |
saleState = 1; | |
cost = 0.11 ether; | |
} | |
// transition to public sale state | |
function enablePublicSaleState() public onlyOwner { | |
saleState = 2; | |
cost = 0.12 ether; | |
} | |
// change the cost of the token | |
function setCost(uint256 _newCost) public onlyOwner { | |
cost = _newCost; | |
} | |
// set new base URI for metadata | |
function setBaseURI(string memory _uri) public onlyOwner { | |
baseURI = _uri; | |
} | |
// set new base URI for metadata | |
function setPlaceholderURI(string memory _uri) public onlyOwner { | |
placeholderURI = _uri; | |
} | |
// BUYER FUNCTIONS | |
// used by buyer to mint a specific token if they are on yellowlist | |
// must provide a signature from the backend | |
function yellowlistMint(uint256 tokenId, bytes memory signature) | |
public | |
payable | |
requireYellowlistState | |
{ | |
// fails if signature is invalid | |
_verifyBackendSignature(Strings.toString(tokenId), signature); | |
require(supply + 1 <= maxSupply, "Purchase would exceed max supply."); | |
require(msg.value == cost, "You must pay for your duck"); | |
// mint the specified tokenId | |
_safeMint(msg.sender, tokenId); | |
supply++; | |
mintedTokenIds[tokenId] = true; | |
// use up the signature so that it can no longer be used to mint | |
usedYellowlistBackendSignatures[signature] = true; | |
} | |
function toAsciiString(address x) internal pure returns (string memory) { | |
bytes memory s = new bytes(40); | |
for (uint i = 0; i < 20; i++) { | |
bytes1 b = bytes1(uint8(uint(uint160(x)) / (2**(8*(19 - i))))); | |
bytes1 hi = bytes1(uint8(b) / 16); | |
bytes1 lo = bytes1(uint8(b) - 16 * uint8(hi)); | |
s[2*i] = char(hi); | |
s[2*i+1] = char(lo); | |
} | |
return string(bytes.concat(bytes("0x"), s)); | |
} | |
function char(bytes1 b) internal pure returns (bytes1 c) { | |
if (uint8(b) < 10) return bytes1(uint8(b) + 0x30); | |
else return bytes1(uint8(b) + 0x57); | |
} | |
// used by buyer to mint if they're on the whitelist | |
function whitelistMint(bytes memory signature, uint8 numberOfTokens) | |
public | |
payable | |
requireWhitelistState | |
{ | |
require(numberOfTokens > 0, "You must mint at least one duck."); | |
// 111 is max yellowlist supply, so we'll remove these from the supply | |
require(supply + numberOfTokens <= (maxSupply - 114), "Purchase would exceed max supply."); | |
require( | |
whitelistAddressMintedAmount[msg.sender] + numberOfTokens <= maxWhitelistMintAmount, | |
"You may mint up to 3 ducks." | |
); | |
require(msg.value == cost * numberOfTokens, "You must pay for your duck"); | |
string memory sender = toAsciiString(msg.sender); | |
// fails if signature is invalid (message is the sender wallet address) | |
_verifyBackendSignature(sender, signature); | |
for (uint i = 0; i < numberOfTokens; i++) { | |
// mint the next avaialble token id | |
_safeMint(msg.sender, _getNextTokenId()); | |
supply++; | |
// track that the wallet minted another token | |
whitelistAddressMintedAmount[msg.sender]++; | |
} | |
} | |
// public | |
function publicMint(uint8 numberOfTokens) public payable _requirePublicSaleState { | |
require(numberOfTokens > 0, "You must mint at least one duck for public sale."); | |
// 114 is max yellowlist supply, so we'll remove these from the supply | |
require(supply + numberOfTokens <= (maxSupply - 114), "Purchase would exceed max supply."); | |
require( | |
publicSaleAddressMintedAmount[msg.sender] + numberOfTokens <= maxPublicSaleMintAmount, | |
"You may mint up to 5 ducks on the public list sale." | |
); | |
require(msg.value == cost * numberOfTokens, "You must pay for your duck"); | |
for (uint i = 0; i < numberOfTokens; i++) { | |
// mint the next avaialble token id | |
_safeMint(msg.sender, _getNextTokenId()); | |
supply++; | |
// track that the wallet minted another token! | |
publicSaleAddressMintedAmount[msg.sender]++; | |
} | |
} | |
// for OpenSea | |
function tokenURI(uint256 tokenId) | |
public | |
view | |
virtual | |
override | |
returns (string memory) | |
{ | |
require( | |
_exists(tokenId), | |
"ERC721Metadata: URI query for nonexistent token" | |
); | |
return string( | |
abi.encodePacked( | |
isRevealed ? baseURI : placeholderURI, | |
"/", | |
tokenId.toString(), | |
baseExtension | |
) | |
); | |
} | |
function withdraw() onlyOwner public { | |
payable(msg.sender).transfer(address(this).balance); | |
} | |
function whitelistMintedByAddress() public view returns (uint8) { | |
return whitelistAddressMintedAmount[msg.sender]; | |
} | |
function publicMintedByAddress() public view returns (uint8) { | |
return publicSaleAddressMintedAmount[msg.sender]; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { ethers } from 'ethers'; | |
import { useEffect } from 'react'; | |
import { useState } from 'react'; | |
export function Connect({ setWalletAddress }) { | |
const [walletAddress, setLocalWalletAddress] = useState(); | |
if (window.ethereum) { | |
window.ethereum.on('accountsChanged', function (accounts) { | |
window.location.reload() | |
}) | |
window.ethereum.on('networkChanged', function (networkId) { | |
// console.log(networkId) | |
// Time to reload your interface with the new networkId | |
console.log(networkId) | |
}) | |
} | |
const requestWalletAddress = async () => { | |
const [walletAddress] = await window.ethereum.request({ method: 'eth_requestAccounts' }); | |
const provider = new ethers.providers.Web3Provider(window.ethereum); | |
setLocalWalletAddress(walletAddress); | |
setWalletAddress(walletAddress); | |
} | |
useEffect(() => { | |
// only if ethereum is already connected, we fetch the address | |
if (window.ethereum.isConnected()) { | |
requestWalletAddress(); | |
} | |
}) | |
// when button is pressed ask to connect | |
const connectMetamask = async () => { | |
requestWalletAddress(); | |
}; | |
let view = (<div></div>); | |
const shortWalletAddress = walletAddress && `${walletAddress.substring(0, 5)}...${walletAddress.substring(walletAddress.length - 4, walletAddress.length)}` | |
if (walletAddress) { | |
view = ( | |
<div style={{color: 'white', style: 'inline', textAlign: 'center'}}> | |
<a onClick={connectMetamask} class="button-icon is-wallet w-inline-block"> | |
<div style={{margin: '0 auto'}}> | |
<p> | |
<svg width="20" height="18" viewBox="0 0 20 18" fill="none" xmlns="http://www.w3.org/2000/svg"> | |
<path d="M17 4H16V3C16 2.20435 15.6839 1.44129 15.1213 0.87868C14.5587 0.316071 13.7956 0 13 0H3C2.20435 0 1.44129 0.316071 0.87868 0.87868C0.316071 1.44129 0 2.20435 0 3V3V15C0 15.7956 0.316071 16.5587 0.87868 17.1213C1.44129 17.6839 2.20435 18 3 18H17C17.7956 18 18.5587 17.6839 19.1213 17.1213C19.6839 16.5587 20 15.7956 20 15V7C20 6.20435 19.6839 5.44129 19.1213 4.87868C18.5587 4.31607 17.7956 4 17 4ZM3 2H13C13.2652 2 13.5196 2.10536 13.7071 2.29289C13.8946 2.48043 14 2.73478 14 3V4H3C2.73478 4 2.48043 3.89464 2.29289 3.70711C2.10536 3.51957 2 3.26522 2 3C2 2.73478 2.10536 2.48043 2.29289 2.29289C2.48043 2.10536 2.73478 2 3 2V2ZM18 12H17C16.7348 12 16.4804 11.8946 16.2929 11.7071C16.1054 11.5196 16 11.2652 16 11C16 10.7348 16.1054 10.4804 16.2929 10.2929C16.4804 10.1054 16.7348 10 17 10H18V12ZM18 8H17C16.2044 8 15.4413 8.31607 14.8787 8.87868C14.3161 9.44129 14 10.2044 14 11C14 11.7956 14.3161 12.5587 14.8787 13.1213C15.4413 13.6839 16.2044 14 17 14H18V15C18 15.2652 17.8946 15.5196 17.7071 15.7071C17.5196 15.8946 17.2652 16 17 16H3C2.73478 16 2.48043 15.8946 2.29289 15.7071C2.10536 15.5196 2 15.2652 2 15V5.83C2.32127 5.94302 2.65943 6.00051 3 6H17C17.2652 6 17.5196 6.10536 17.7071 6.29289C17.8946 6.48043 18 6.73478 18 7V8Z" fill="currentColor"></path> | |
</svg> | |
{" " +shortWalletAddress} | |
</p> | |
</div> | |
</a> | |
</div> | |
) | |
} else { | |
view = ( | |
<a onClick={() => {connectMetamask()}} class="button-icon is-wallet w-inline-block"><div class="margin-right margin-small"><div class="embed w-embed"><svg width="20" height="18" viewBox="0 0 20 18" fill="none" xmlns="http://www.w3.org/2000/svg"> | |
<path d="M17 4H16V3C16 2.20435 15.6839 1.44129 15.1213 0.87868C14.5587 0.316071 13.7956 0 13 0H3C2.20435 0 1.44129 0.316071 0.87868 0.87868C0.316071 1.44129 0 2.20435 0 3V3V15C0 15.7956 0.316071 16.5587 0.87868 17.1213C1.44129 17.6839 2.20435 18 3 18H17C17.7956 18 18.5587 17.6839 19.1213 17.1213C19.6839 16.5587 20 15.7956 20 15V7C20 6.20435 19.6839 5.44129 19.1213 4.87868C18.5587 4.31607 17.7956 4 17 4ZM3 2H13C13.2652 2 13.5196 2.10536 13.7071 2.29289C13.8946 2.48043 14 2.73478 14 3V4H3C2.73478 4 2.48043 3.89464 2.29289 3.70711C2.10536 3.51957 2 3.26522 2 3C2 2.73478 2.10536 2.48043 2.29289 2.29289C2.48043 2.10536 2.73478 2 3 2V2ZM18 12H17C16.7348 12 16.4804 11.8946 16.2929 11.7071C16.1054 11.5196 16 11.2652 16 11C16 10.7348 16.1054 10.4804 16.2929 10.2929C16.4804 10.1054 16.7348 10 17 10H18V12ZM18 8H17C16.2044 8 15.4413 8.31607 14.8787 8.87868C14.3161 9.44129 14 10.2044 14 11C14 11.7956 14.3161 12.5587 14.8787 13.1213C15.4413 13.6839 16.2044 14 17 14H18V15C18 15.2652 17.8946 15.5196 17.7071 15.7071C17.5196 15.8946 17.2652 16 17 16H3C2.73478 16 2.48043 15.8946 2.29289 15.7071C2.10536 15.5196 2 15.2652 2 15V5.83C2.32127 5.94302 2.65943 6.00051 3 6H17C17.2652 6 17.5196 6.10536 17.7071 6.29289C17.8946 6.48043 18 6.73478 18 7V8Z" fill="currentColor"></path> | |
</svg></div></div><div class="padButton">Connect Wallet</div></a> | |
) | |
} | |
return view; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'animate.css'; | |
import { useEffect, useState } from 'react'; | |
import { Connect } from './Connect'; | |
import { DuckCounter } from './DuckCounter'; | |
import YellowDuckCollection from '../Assets/Contracts/YellowDuckCollection.json'; | |
import axios from 'axios'; | |
import { ethers } from 'ethers'; | |
import usePortal from 'react-useportal'; | |
function Home() { | |
const contractAddress = '0x088C09a1FcC6B4EeA127341EF292386320D26aD3'; | |
const SIGNING_SERVER_ROOT = "https://us-central1-yellowduck-337518.cloudfunctions.net" | |
const provider = new ethers.providers.Web3Provider(window.ethereum); | |
// get the end user | |
const signer = provider.getSigner(); | |
// get the smart contract | |
const contract = new ethers.Contract(contractAddress, YellowDuckCollection.abi, signer); | |
const PHASE_YELLOWLIST = 0; | |
const PHASE_WHITELIST = 1; | |
const PHASE_PUBLICSALE = 2; | |
const PHASE_CLOSED = 3; | |
// MetaMask Wallet Address | |
const [walletAddress, setWalletAddress] = useState(""); | |
// Yellowlist/Whitelist/Public State | |
const [phase, setPhase] = useState(-1); | |
// Counter | |
const [counter, setCounter] = useState(1); | |
// Status, leave blank | |
const [status, setStatus] = useState('') | |
// Cost | |
const [cost, setCost] = useState('') | |
// Ducks Minted | |
const [ducksMinted, setDucksMinted] = useState(0); | |
// total user can mint | |
const [canMintAmount, setCanMintAmount] = useState(0); | |
// Minted | |
const [mintedAmount, setMintedAmount] = useState(0) | |
useEffect(() => { | |
web3GetSaleState(); | |
}, [walletAddress]); | |
const [costOfMint, setCostOfMint] = useState(0); | |
useEffect(() => { | |
setCostOfMint(ethers.utils.parseUnits('' + (cost * counter), 'ether')); | |
}, [cost, counter]); | |
useEffect(() => { | |
if (phase === PHASE_YELLOWLIST) { | |
if (mintedAmount) { | |
setCanMintAmount(0); | |
} else { | |
setCanMintAmount(1); | |
} | |
} else if (phase === PHASE_WHITELIST) { | |
setCanMintAmount(3 - mintedAmount); | |
} else if (phase === PHASE_PUBLICSALE) { | |
setCanMintAmount(5 - mintedAmount); | |
} | |
}, [mintedAmount, phase]); | |
useEffect(() => { | |
if (phase === PHASE_CLOSED) { | |
setStatus("Public Sale has been closed"); | |
} | |
else if (canMintAmount) { | |
setStatus( | |
phase === PHASE_YELLOWLIST | |
? "Welcome to the #YellowList." | |
: "You can mint up to " + canMintAmount + " duck(s)." | |
); | |
} else { | |
if (phase === PHASE_YELLOWLIST) { | |
setStatus("You have minted your #YellowList!") | |
} else if (phase === PHASE_WHITELIST) { | |
setStatus("You have hit the mint limit for our whitelist. Check in later for the public sale!"); | |
} else if (phase === PHASE_PUBLICSALE) { | |
setStatus("You have minted all of your YellowDucks."); | |
} | |
} | |
}, [canMintAmount, phase]); | |
const web3GetSaleState = async () => { | |
try { | |
const phase = await contract.saleState() | |
const costOfOneMint = await contract.cost() | |
const ducksMinted = await contract.supply() | |
const mintSinglePrice = ethers.utils.formatEther(costOfOneMint) | |
const ducksMintedFriendly = ethers.utils.formatEther(''+ducksMinted) * 1000000000000000000 | |
setDucksMinted(ducksMintedFriendly); | |
setCost(mintSinglePrice) | |
setPhase(phase); | |
if(!walletAddress) return; | |
let localStorageMinted; | |
let contractMinted; | |
switch (phase) { | |
case PHASE_YELLOWLIST: | |
setMintedAmount(parseInt(localStorage.getItem(`${walletAddress}-yellowlistMinted`)) || 0); | |
break; | |
case PHASE_WHITELIST: | |
localStorageMinted = parseInt(localStorage.getItem(`${walletAddress}-whiteMinted`)) || 0; | |
setMintedAmount(localStorageMinted); | |
contractMinted = await contract.whitelistMintedByAddress() || 0; | |
// the contract is sometimes behind. If the number is bigger on contract they probably | |
// used another device to mint, so we use that | |
if (contractMinted > localStorageMinted) { | |
setMintedAmount(contractMinted); | |
} | |
break; | |
case PHASE_PUBLICSALE: | |
localStorageMinted = parseInt(localStorage.getItem(`${walletAddress}-publicMinted`)) || 0; | |
setMintedAmount(localStorageMinted); | |
contractMinted = await contract.publicMintedByAddress() || 0; | |
// the contract is sometimes behind. If the number is bigger on contract they probably | |
// used another device to mint, so we use that | |
if (contractMinted > localStorageMinted) { | |
setMintedAmount(contractMinted); | |
} | |
break; | |
case PHASE_CLOSED: | |
setStatus(""); | |
break; | |
default: | |
setStatus("The minting is in an unknown state."); | |
break; | |
} | |
} catch (e) { | |
console.log(e) | |
} | |
} | |
const catchErrors = (e) => { | |
if (e['message']) { | |
if (e['message'].includes('Signature has already')) { | |
setStatus("You have already minted your duck(s)! Please check your OpenSea collection.") | |
setMintedAmount(1) | |
} | |
else if (e['message'].toString().includes('insufficient')) { | |
setStatus("You must have at least " + (+ethers.utils.formatEther(costOfMint)).toFixed(2) + " ETH to mint " + counter + " YellowDuck(s)!") | |
} | |
else { | |
setStatus(e['message']) | |
} | |
} else { | |
setStatus("An unknown error has occurred.") | |
} | |
} | |
const web3PerformYellowlistMint = async () => { | |
try { | |
const { | |
tokenId, | |
signature | |
} = (await axios.get(`${SIGNING_SERVER_ROOT}/getYellowlistSignature`, { | |
params: { | |
walletAddress | |
} | |
})).data; | |
try { | |
console.log(costOfMint); | |
const mint = await contract.yellowlistMint(tokenId, signature, { value: costOfMint }); | |
setMintedAmount(1); | |
setDucksMinted(ducksMinted + 1); | |
localStorage.setItem(`${walletAddress}-yellowlistMinted`, 1) | |
alert("Congratulations! You have minted your #YellowList! Please wait for up to 60 seconds for your YellowDuck to arrive in your wallet."); | |
await web3GetSaleState(); | |
} catch (e) { | |
catchErrors(e); | |
} | |
} catch (e) { | |
alert("You need to be listed for a 1-to-1 mint by completing a duck challenge.") | |
} | |
} | |
const web3PerformWhitelistMint = async () => { | |
try { | |
const { | |
signature | |
} = (await axios.get(`${SIGNING_SERVER_ROOT}/getWhitelistSignature`, { | |
params: { | |
walletAddress | |
} | |
})).data; | |
//this value must match the smart contract! | |
try { | |
const mint = await contract.whitelistMint(signature, counter, { value: costOfMint }); | |
setDucksMinted(ducksMinted + counter); | |
alert("Congratulations! You have successfully minted! Please wait for up to 60 seconds for your YellowDuck(s) to arrive in your wallet."); | |
const newMintedAmount = counter + mintedAmount; | |
setMintedAmount(newMintedAmount); | |
localStorage.setItem(`${walletAddress}-whiteMinted`, newMintedAmount); | |
} catch (e) { | |
catchErrors(e); | |
} | |
} catch (e) { | |
alert("You are not currently whitelisted!"); | |
} | |
} | |
const web3PerformPublicMint = async () => { | |
try { | |
//this value must match the smart contract! | |
const mint = await contract.publicMint(counter, { value: costOfMint }); | |
setDucksMinted(ducksMinted + counter); | |
alert("Congratulations! You have minted your YellowDuck(s)! Please wait for up to 60 seconds for your YellowDuck(s) to arrive in your wallet."); | |
const newMintedAmount = counter + mintedAmount; | |
setMintedAmount(newMintedAmount); | |
localStorage.setItem(`${walletAddress}-publicMinted`, newMintedAmount); | |
} catch (e) { | |
catchErrors(e); | |
} | |
} | |
// <h3 className="heading-status">Welcome to YellowDuck NFT Minting!</h3> | |
let statusInfo = ( | |
<h3 className="heading-status"></h3> | |
) | |
let mintButton = null; | |
if (walletAddress) { | |
switch (phase) { | |
case PHASE_YELLOWLIST: | |
statusInfo = ( | |
<h3 className="heading-status">Yellowlist Sale</h3> | |
) | |
if (!mintedAmount) { | |
mintButton = ( | |
<div key={1}> | |
<button className="minting-button" onClick={web3PerformYellowlistMint}>Mint!</button> | |
</div> | |
) | |
} | |
break; | |
case PHASE_WHITELIST: | |
statusInfo = ( | |
<h3 className="heading-status">Whitelist Sale</h3> | |
) | |
mintButton = ( | |
<div key={1}> | |
<button className="minting-button" onClick={web3PerformWhitelistMint}>Mint!</button> | |
</div> | |
) | |
break; | |
case PHASE_PUBLICSALE: | |
statusInfo = ( | |
<h3 className="heading-status">Public Sale</h3> | |
) | |
mintButton = ( | |
<div key={1}> | |
<button className="minting-button" onClick={web3PerformPublicMint}>Mint!</button> | |
</div> | |
) | |
break; | |
case PHASE_CLOSED: | |
statusInfo = ( | |
<h3 className="heading-status">Sold Out</h3> | |
) | |
break; | |
default: | |
break; | |
} | |
} | |
const incrementCounter = () => { | |
setCounter(Math.min(counter + 1, canMintAmount)); | |
}; | |
const decrementCounter = () => { | |
setCounter(Math.max(1, counter - 1)); | |
} | |
const boxCounter = ( | |
<div className="box-items"> | |
<div className="box"> | |
<ButtonDecrement onClickFunc={() => { decrementCounter(); } } /> | |
<Display message={counter} /> | |
<ButtonIncrement onClickFunc={() => { incrementCounter(); }} /> | |
</div> | |
</div> | |
); | |
const { Portal } = usePortal({ | |
bindTo: document.querySelector('.connect-button') | |
}); | |
const { Portal:DuckCounterPortal } = usePortal({ | |
bindTo: document.querySelector('#duck-counter') | |
}); | |
return ( | |
<> | |
<Portal> | |
<Connect setWalletAddress={setWalletAddress} /> | |
</Portal> | |
<DuckCounterPortal> | |
<DuckCounter counter={ducksMinted} /> | |
</DuckCounterPortal> | |
<div>{statusInfo}</div> | |
{(canMintAmount > 0) && ( | |
<div className="box"> | |
{[PHASE_PUBLICSALE, PHASE_WHITELIST].includes(phase) && ( | |
boxCounter | |
)} | |
{mintButton && ( | |
<div className="box-items"> | |
{mintButton} | |
</div> | |
)} | |
{/* <div className="box-items"> | |
<div className="heading-status"> | |
<a className="heading-status" target="_blank" href="https://testnets.opensea.io/account">Link to OpenSea Collection</a> | |
</div> | |
</div> */} | |
</div>)} | |
<p className="mint-status" style={{ textAlign: 'center' }}>{status}</p> | |
</> | |
); | |
} | |
function ButtonIncrement(props) { | |
return ( | |
<button className="mint-button-inc" onClick={props.onClickFunc}> | |
+ | |
</button> | |
) | |
} | |
function ButtonDecrement(props) { | |
return ( | |
<button className="mint-button-dec" onClick={props.onClickFunc}> | |
- | |
</button> | |
) | |
} | |
function Display(props) { | |
return ( | |
<label className="mint-num-val" >{props.message}</label> | |
) | |
} | |
export default Home; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as functions from "firebase-functions"; | |
const ethers = require('ethers'); | |
const dotenv = require('dotenv'); | |
dotenv.config(); | |
const Firestore = require('@google-cloud/firestore') | |
const cors = require('cors'); | |
const db = new Firestore(); | |
// for signatures | |
const wallet = new ethers.Wallet(process.env.ETH_PRIVATE_KEY); | |
export const getYellowlistSignature = functions.https.onRequest(async (request, response) => { | |
cors()(request, response, async () => { | |
const walletAddress = request.query.walletAddress as string; | |
if (!walletAddress) { | |
response.status(400).send('walletAddress Required'); | |
return; | |
} | |
const yellowListDoc = await db.collection('yellowlist').doc(walletAddress.toLowerCase()).get(); | |
if (yellowListDoc.exists) { | |
const tokenId = yellowListDoc.data().tokenId; | |
const msg = '' + tokenId; | |
const signature = await wallet.signMessage(msg); | |
response.send({ | |
signature, | |
tokenId | |
}); | |
} else { | |
response.status(401).send('You are not allowed to mint yellowlist tokens. Shame....') | |
}; | |
}) | |
}); | |
export const getWhitelistSignature = functions.https.onRequest(async (request, response) => { | |
cors()(request, response, async () => { | |
let walletAddress = request.query.walletAddress as string; | |
if (!walletAddress) { | |
response.status(400).send('walletAddress Required'); | |
return; | |
} | |
const whiteListDoc = await db.collection('whitelist').doc(walletAddress.toLowerCase()).get(); | |
if (whiteListDoc.exists) { | |
// address of the wallet who is allowed to mint (must be lowercase) | |
const msg = walletAddress.toLowerCase(); | |
const signature = await wallet.signMessage(msg); | |
response.json({ | |
signature | |
}); | |
} else { | |
response.status(401).send('You are not allowed to mint whitelist tokens. Shame....') | |
} | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require("@nomiclabs/hardhat-waffle"); | |
require("@nomiclabs/hardhat-etherscan"); | |
require("@nomiclabs/hardhat-ethers"); | |
const dotenv = require('dotenv'); | |
dotenv.config(); | |
require('./scripts/deploy'); | |
/** | |
* @type import('hardhat/config').HardhatUserConfig | |
*/ | |
module.exports = { | |
solidity: "0.8.7", | |
paths: { | |
artifacts: './src/artifacts', | |
}, | |
networks: { | |
rinkeby: { | |
url: "https://rinkeby-light.eth.linkpool.io/", | |
accounts: [process.env.ETH_PRIVATE_KEY]// add the account that will deploy the contract (private key) | |
}, | |
}, | |
// support for verifying my contract | |
etherscan: { | |
apiKey: process.env.ETHERSCAN_API_KEY, | |
}, | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// We require the Hardhat Runtime Environment explicitly here. This is optional | |
// but useful for running the script in a standalone fashion through `node <script>`. | |
// | |
// When running the script with `npx hardhat run <script>` you'll find the Hardhat | |
// Runtime Environment's members available in the global scope. | |
const dotenv = require("dotenv"); | |
dotenv.config(); | |
task("deploy", "Deploys the collection") | |
.setAction(async (a, hre) => { | |
const constructorArguments = [ | |
"YellowDuckCollection", | |
"YDV09B" | |
]; | |
const YellowDuck = await hre.ethers.getContractFactory("YellowDuckCollection"); | |
const yellowDuck = await YellowDuck.deploy(...constructorArguments); | |
await yellowDuck.deployed(); | |
console.log("YellowDuck deployed to:", yellowDuck.address); | |
console.log('Waiting 60s to verify') | |
await new Promise ((resolve) => { | |
setTimeout(async () => { | |
await hre.run('verify:verify', { | |
address: yellowDuck.address, | |
constructorArguments, | |
}); | |
resolve(); | |
}, 60 * 1000); | |
}); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment