Created SkinZ NFT Contract.
//SPDX-License-Identifier: Unlicense | ||
pragma solidity ^0.8.0; | ||
import "hardhat/console.sol"; | ||
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; | ||
import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; | ||
import "@openzeppelin/contracts/access/Ownable.sol"; | ||
import "@openzeppelin/contracts/utils/Strings.sol"; | ||
/// @title Base64 | ||
/// @author Brecht Devos - <[email protected]> | ||
/// @notice Provides a function for encoding some bytes in base64 | ||
library Base64 { | ||
string internal constant TABLE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; | ||
function encode(bytes memory data) internal pure returns (string memory) { | ||
if (data.length == 0) return ''; | ||
// load the table into memory | ||
string memory table = TABLE; | ||
// multiply by 4/3 rounded up | ||
uint256 encodedLen = 4 * ((data.length + 2) / 3); | ||
// add some extra buffer at the end required for the writing | ||
string memory result = new string(encodedLen + 32); | ||
assembly { | ||
// set the actual output length | ||
mstore(result, encodedLen) | ||
// prepare the lookup table | ||
let tablePtr := add(table, 1) | ||
// input ptr | ||
let dataPtr := data | ||
let endPtr := add(dataPtr, mload(data)) | ||
// result ptr, jump over length | ||
let resultPtr := add(result, 32) | ||
// run over the input, 3 bytes at a time | ||
for {} lt(dataPtr, endPtr) {} | ||
{ | ||
dataPtr := add(dataPtr, 3) | ||
// read 3 bytes | ||
let input := mload(dataPtr) | ||
// write 4 characters | ||
mstore(resultPtr, shl(248, mload(add(tablePtr, and(shr(18, input), 0x3F))))) | ||
resultPtr := add(resultPtr, 1) | ||
mstore(resultPtr, shl(248, mload(add(tablePtr, and(shr(12, input), 0x3F))))) | ||
resultPtr := add(resultPtr, 1) | ||
mstore(resultPtr, shl(248, mload(add(tablePtr, and(shr( 6, input), 0x3F))))) | ||
resultPtr := add(resultPtr, 1) | ||
mstore(resultPtr, shl(248, mload(add(tablePtr, and( input, 0x3F))))) | ||
resultPtr := add(resultPtr, 1) | ||
} | ||
// padding with '=' | ||
switch mod(mload(data), 3) | ||
case 1 { mstore(sub(resultPtr, 2), shl(240, 0x3d3d)) } | ||
case 2 { mstore(sub(resultPtr, 1), shl(248, 0x3d)) } | ||
} | ||
return result; | ||
} | ||
} | ||
/** | ||
* @title Precompiled contract that exists in every Arbitrum chain at address(100), 0x0000000000000000000000000000000000000064. Exposes a variety of system-level functionality. | ||
*/ | ||
interface ArbSys { | ||
/** | ||
* @notice Get internal version number identifying an ArbOS build | ||
* @return version number as int | ||
*/ | ||
function arbOSVersion() external pure returns (uint); | ||
/** | ||
* @notice Get Arbitrum block number (distinct from L1 block number; Arbitrum genesis block has block number 0) | ||
* @return block number as int | ||
*/ | ||
function arbBlockNumber() external view returns (uint); | ||
/** | ||
* @notice Send given amount of Eth to dest from sender. | ||
* This is a convenience function, which is equivalent to calling sendTxToL1 with empty calldataForL1. | ||
* @param destination recipient address on L1 | ||
* @return unique identifier for this L2-to-L1 transaction. | ||
*/ | ||
function withdrawEth(address destination) external payable returns(uint); | ||
/** | ||
* @notice Send a transaction to L1 | ||
* @param destination recipient address on L1 | ||
* @param calldataForL1 (optional) calldata for L1 contract call | ||
* @return a unique identifier for this L2-to-L1 transaction. | ||
*/ | ||
function sendTxToL1(address destination, bytes calldata calldataForL1) external payable returns(uint); | ||
/** | ||
* @notice get the number of transactions issued by the given external account or the account sequence number of the given contract | ||
* @param account target account | ||
* @return the number of transactions issued by the given external account or the account sequence number of the given contract | ||
*/ | ||
function getTransactionCount(address account) external view returns(uint256); | ||
/** | ||
* @notice get the value of target L2 storage slot | ||
* This function is only callable from address 0 to prevent contracts from being able to call it | ||
* @param account target account | ||
* @param index target index of storage slot | ||
* @return stotage value for the given account at the given index | ||
*/ | ||
function getStorageAt(address account, uint256 index) external view returns (uint256); | ||
/** | ||
* @notice check if current call is coming from l1 | ||
* @return true if the caller of this was called directly from L1 | ||
*/ | ||
function isTopLevelCall() external view returns (bool); | ||
event EthWithdrawal(address indexed destAddr, uint amount); | ||
event L2ToL1Transaction(address caller, address indexed destination, uint indexed uniqueId, | ||
uint indexed batchNumber, uint indexInBatch, | ||
uint arbBlockNum, uint ethBlockNum, uint timestamp, | ||
uint callvalue, bytes data); | ||
} | ||
contract SkinZNFT is ERC721Enumerable, ReentrancyGuard, Ownable { | ||
using Address for address; | ||
struct SkinDefinition { | ||
string image; //ipfs CID for skin artwork | ||
string name; | ||
} | ||
mapping(uint256 => SkinDefinition) internal _nftData; // on NFT claim (mint) we generate random NFT data | ||
struct SkinData { | ||
string image; | ||
string name; | ||
uint256 count; | ||
} | ||
SkinData[] internal _unmintedSkins; | ||
uint256 internal max_token_id; | ||
constructor() ERC721("Skin-Z", "SKZ") Ownable() { | ||
max_token_id = 0; | ||
//NOTE: if we move away from arbitrum, this needs removed! | ||
/*address arbsys_addr = address(0x64); | ||
require(arbsys_addr.isContract(), "could not find arbsys contract at 0x64?! not deployed on arbitrum?");*/ | ||
//generate unminted skins | ||
pushSkin(SkinData("QmVWU23fNY5RNp3mpAhGE6ZtZGcs2EnrKkN4Lmf5w96joC","Example NFT", 1)); // push a certain # of these skins to be mintable | ||
pushSkin(SkinData("QmVWU23fNY5RNp3mpAhGE6ZtZGcs2EnrKkN4Lmf5w96joC","Rare NFT", 1)); // push a certain # of these skins to be mintable | ||
pushSkin(SkinData("QmVWU23fNY5RNp3mpAhGE6ZtZGcs2EnrKkN4Lmf5w96joC","Common NFT", 1)); // push a certain # of these skins to be mintable | ||
} | ||
// owner can push new skins for minting | ||
function pushSkin(SkinData memory data) public onlyOwner { | ||
_unmintedSkins.push(data); | ||
max_token_id += data.count; | ||
} | ||
// batch push new mints (for when we deploy a new generation!) | ||
function pushMultiple(SkinData[] memory data) public onlyOwner { | ||
for(uint256 i = 0; i < data.length; i++) | ||
{ | ||
_unmintedSkins.push(data[i]); | ||
max_token_id += data[i].count; | ||
} | ||
} | ||
// NOTE: if moving from arbitrum to another blockchain, this source of randomness would need uodated! | ||
// generate a random number (changes between arbitrum and ethereum mainnet!) | ||
function random() internal view returns(uint256) | ||
{ | ||
/*ArbSys arbos = ArbSys(address(0x64)); | ||
return uint256(keccak256(abi.encodePacked("created by kegan hollern",block.number,arbos.arbBlockNumber(), arbos.arbOSVersion())));*/ | ||
return uint256(keccak256(abi.encodePacked("created by kegan hollern",block.number))); //code for non-arbitrum randomization | ||
} | ||
function randomSkin() internal returns (SkinDefinition memory) | ||
{ | ||
require(_unmintedSkins.length > 0, "no skins are available to be randomly generated"); | ||
uint256 index = 0; | ||
if(_unmintedSkins.length > 1) //if only one skin type remains, no reason to run RNG and waste gas | ||
{ | ||
uint256 rng = random(); | ||
index = rng % _unmintedSkins.length; | ||
} | ||
string memory skin_cid = _unmintedSkins[index].image; | ||
string memory skin_name = _unmintedSkins[index].name; | ||
if((_unmintedSkins[index].count - 1) == 0) { // no reason to decrement count to 0 (and spend gas) when we just need to remove this item | ||
//remove skin | ||
_unmintedSkins[index] = _unmintedSkins[_unmintedSkins.length-1]; //move last element into the position of our item we want to remove | ||
_unmintedSkins.pop(); //delete last item which is now a duplicate | ||
} else { | ||
_unmintedSkins[index].count--; //more skins of this type remain, just decrement count | ||
} | ||
SkinDefinition memory output = SkinDefinition(skin_cid, skin_name); | ||
return output; | ||
} | ||
function tokenURI(uint256 tokenId) override public view returns (string memory) { | ||
require(_exists(tokenId), "token not claimed"); | ||
SkinDefinition memory _data = _nftData[tokenId]; | ||
assert(bytes(_data.image).length > 0); //should NEVER happen | ||
//nft metadata is stored on chain instead of on IPFS, this allows each NFT to have a unique name. | ||
string memory json = Base64.encode(bytes(string(abi.encodePacked('{"name":"',_data.name,' #',Strings.toString(tokenId), '", "description": "Skin-Z are NFTs for DayZ Standalone. While not unique, some skins are rarer than others. Owning one allows skins to be applied onto gear in DayZ.", "image": "ipfs://', _data.image, '"}')))); | ||
string memory output = string(abi.encodePacked('data:application/json;base64,', json)); | ||
return output; | ||
} | ||
function claim(uint256 tokenId) public nonReentrant { | ||
require(_unmintedSkins.length > 0, "no more skins to claim"); | ||
require(tokenId <= max_token_id && tokenId > 0, "Invalid Token ID"); //tokenIds start from 1! | ||
require(!_exists(tokenId), "token already claimed"); | ||
_nftData[tokenId] = randomSkin(); | ||
assert(bytes(_nftData[tokenId].image).length > 0); | ||
assert(bytes(_nftData[tokenId].name).length > 0); | ||
_safeMint(_msgSender(), tokenId); | ||
} | ||
} | ||
\ No newline at end of file |
Please register or sign in to comment