Solidity Smart Contract
JosieNftBox.sol
See below the code for JosieNftBox (opens in a new tab) that makes it all happen.
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "./JosieNftBoxMetadata.sol";
import "./WithOperatorFilter.sol";
contract JosieNftBox is ERC721, Ownable, WithOperatorFilter {
// Metadata image
JosieNftBoxMetadata public metadataContract;
uint256 public currentTokenSupply;
uint256 public START_TIMESTAMP;
uint256 constant public ROLLS_PER_DAY = 5;
uint256 constant SECONDS_BETWEEN_ROLLS = 86400;
uint256 public tokensRolled;
// Revealed token information
mapping(uint256 => uint256) public revealedTokenVariant;
uint256 constant NUM_REVEALED_VARIANTS = 12;
// Random reveal variables
uint256 internal _leftToRoll;
mapping(uint256 => uint256) internal _idSwaps;
mapping(uint256 => uint256) internal _prizeDrawnSwaps;
uint256 internal _currentPrng;
// Royalty configuration
address public royaltyRecipient;
uint256 public royaltyBps = 1000;
// Prizes
string[15] PRIZES = [
"Filter - #17/100",
"Looks like you've had a bit too much to think - #17/21",
"Looks like you've had a bit too much to think - #15/21",
"This is America - #57/70",
"This is America - #60/70",
"CyberBrokers - Phillip Whitlock #8128",
"CyberBrokers - Sonia of the Twilight #8018",
"CyberBrokers - Enya the Sickly #5513",
"CyberBrokers - Soppy Vader #3141",
"CyberBrokers - Everly of Uncouth #7612",
"CyberBrokers - Tainted Stoyer #6123",
"CyberBrokers - Zion Lackadaisical #423",
"CyberBrokers - Nolan from Pretty #2274",
"CyberBrokers - Macy Slurry #8905",
"CyberBrokers - Deja of Monroe #5876"
];
event RevealToken(uint256 tokenId, string variant, string prize);
constructor(
address _metadataContractAddress
)
ERC721("Don't Feed the Pigeons", 'PIGEONS')
{
setMetadataContract(_metadataContractAddress);
START_TIMESTAMP = block.timestamp;
// Default royalty recipient
royaltyRecipient = msg.sender;
}
/**
* Owner-only functions
**/
function setIsOperatorFilterEnabled(bool _setting) external onlyOwner {
isOperatorFilterEnabled = _setting;
}
function setMetadataContract(address _metadataContractAddress) public onlyOwner {
metadataContract = JosieNftBoxMetadata(_metadataContractAddress);
}
function setRoyaltyRecipient(address _recipient) external onlyOwner {
royaltyRecipient = _recipient;
}
function setRoyaltyBps(uint256 _bps) external onlyOwner {
require(_bps < 10000, "Royalty basis points must be under 10,000 (100%)");
royaltyBps = _bps;
}
function airdrop(
address[] calldata addresses
)
external
onlyOwner
{
require(addresses.length > 0, "Invalid addresses lengths");
uint256 currentToken = totalSupply();
for (uint256 idx; idx < addresses.length; idx++) {
require(currentToken < 500, "At max supply.");
_safeMint(
addresses[idx],
currentToken + idx + 1
);
}
currentTokenSupply = currentToken + addresses.length;
}
function rollReveals()
external
onlyOwner
{
// If we've rolled all tokens, bail
require(tokensRolled < totalSupply(), "Rolled all tokens");
// Determine how many days passed, for how many days of rolls we need to perform
uint256 tokensToRoll = numToRoll();
// If we have no tokens to roll, bail
require(tokensToRoll > 0, "No tokens to roll yet");
// Cap the number of tokens to roll at 5 for gas concerns
tokensToRoll = tokensToRoll > 5 ? 5 : tokensToRoll;
// Copy the current data
uint256 leftToRoll = totalSupply() - tokensRolled;
uint256 currentPrng = _currentPrng;
// Roll the tokens
uint256 _tokenId;
uint256 _prizeIndex;
for (uint256 idx; idx < tokensToRoll; idx++) {
// Generate the next random number
currentPrng = _prng(currentPrng, leftToRoll, 1);
// Pull the next token ID
_tokenId = _pullRandomUniqueIndex(currentPrng, leftToRoll, _idSwaps);
// Generate the revealed token variant
revealedTokenVariant[_tokenId] = 1 + (_prng(currentPrng, leftToRoll, 42) % (NUM_REVEALED_VARIANTS));
// Pull the prize index -- start at 0th index to map with the prize array
_prizeIndex = (leftToRoll == 1) ? 500 : (_pullRandomUniqueIndex(_prng(currentPrng, leftToRoll, 69), leftToRoll - 1, _prizeDrawnSwaps) - 1);
// Decrement the local mint counter
leftToRoll--;
// Reveal the token
_revealToken(_tokenId, _prizeIndex);
}
// Store the latest values
_currentPrng = currentPrng;
// Update the count of tokens rolled
tokensRolled += tokensToRoll;
}
function numToRoll()
public
view
returns (
uint256
)
{
uint256 tokensToRoll = ((((block.timestamp - START_TIMESTAMP) / SECONDS_BETWEEN_ROLLS) + 1) * ROLLS_PER_DAY) - tokensRolled;
// Do not exceed the number left to roll
if (tokensToRoll > totalSupply() - tokensRolled) {
tokensToRoll = totalSupply() - tokensRolled;
}
return tokensToRoll;
}
function _revealToken(
uint256 _tokenId,
uint256 _prizeIndex
)
private
{
string memory prize = "N/A";
if (_prizeIndex < PRIZES.length) {
prize = PRIZES[_prizeIndex];
} else if (_prizeIndex == 500) {
prize = 'CryptoPunk #9964';
}
emit RevealToken(_tokenId, metadataContract.VARIANT_NAMES(revealedTokenVariant[_tokenId]), prize);
}
/**
* Public functions
**/
function totalSupply() public view returns (uint256) {
return currentTokenSupply;
}
function tokenURI(
uint256 _tokenId
)
public
view
virtual
override
returns (
string memory
) {
require(address(metadataContract).code.length > 0, "Metadata contract not set");
return metadataContract.constructTokenURI(
_tokenId,
revealedTokenVariant[_tokenId]
);
}
/**
* Credit: created by dievardump (Simon Fremaux)
**/
function _pullRandomUniqueIndex(
uint256 currentPrng,
uint256 leftToRoll,
mapping(uint256 => uint256) storage _swaps
)
internal
returns (uint256)
{
require(leftToRoll > 0, "No more unique indexes to pull");
// get a random id
uint256 index = 1 + (currentPrng % leftToRoll);
uint256 chosenIndex = _swaps[index];
if (chosenIndex == 0) {
chosenIndex = index;
}
uint256 temp = _swaps[leftToRoll];
// "swap" indexes so we don't lose any unminted ids
// either it's id _leftToRoll or the id that was swapped with it
if (temp == 0) {
_swaps[index] = leftToRoll;
} else {
// get some refund
_swaps[index] = temp;
delete _swaps[leftToRoll];
}
return chosenIndex;
}
function _prng(
uint256 currentPrng,
uint256 leftToRoll,
uint256 blockOffset
)
internal
view
returns (uint256)
{
return uint256(
keccak256(
abi.encodePacked(
blockhash(block.number - blockOffset),
block.difficulty,
currentPrng,
leftToRoll
)
)
);
}
/**
* OpenSea
**/
function setApprovalForAll(address operator, bool approved) public override onlyAllowedOperatorApproval(operator) {
super.setApprovalForAll(operator, approved);
}
function approve(address operator, uint256 tokenId) public override onlyAllowedOperatorApproval(operator) {
super.approve(operator, tokenId);
}
function transferFrom(address from, address to, uint256 tokenId) public override onlyAllowedOperator(from) {
super.transferFrom(from, to, tokenId);
}
function safeTransferFrom(address from, address to, uint256 tokenId) public override onlyAllowedOperator(from) {
super.safeTransferFrom(from, to, tokenId);
}
function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data)
public
override
onlyAllowedOperator(from)
{
super.safeTransferFrom(from, to, tokenId, data);
}
/**
* On-Chain Royalties & Interface
**/
function supportsInterface(bytes4 interfaceId)
public
view
override
returns (bool)
{
return interfaceId == this.royaltyInfo.selector || super.supportsInterface(interfaceId);
}
function royaltyInfo(uint256, uint256 amount)
public
view
returns (address, uint256)
{
return (royaltyRecipient, (amount * royaltyBps) / 10000);
}
}