Last active
May 31, 2023 00:54
-
-
Save ChristianOConnor/76e4556b4c7b22d7494d2d9b814e9915 to your computer and use it in GitHub Desktop.
This contract mints NFTs and selects a random Classifier with API3. This contract also requires a signature from a server-bound private key via EIP712.
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: UNLICENSED | |
pragma solidity ^0.8.18; | |
import "@openzeppelin/contracts/access/Ownable.sol"; | |
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; | |
import "@api3/airnode-protocol/contracts/rrp/requesters/RrpRequesterV0.sol"; | |
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; | |
import "@openzeppelin/contracts/utils/cryptography/draft-EIP712.sol"; | |
import "@openzeppelin/contracts/utils/Counters.sol"; | |
import "@openzeppelin/contracts/utils/Strings.sol"; | |
contract RandomReachDebug6p2 is ERC721URIStorage, Ownable, RrpRequesterV0, EIP712 { | |
using Counters for Counters.Counter; | |
struct Request { | |
address minter; | |
uint256 nonce; | |
uint256 deadline; | |
} | |
// This is the keccak256 hash of the Request schema | |
bytes32 internal constant REQUEST_TYPEHASH = keccak256(bytes("Request(address minter,uint256 nonce,uint256 deadline)")); | |
event RequestedRandom(bytes32 indexed requestId); | |
event MintedRandomNFT(bytes32 indexed requestId, uint256 response); | |
event MintCostChanged(uint256 newCost); | |
event Withdrawn(address indexed to, uint256 amount); | |
event Verified(address indexed signer, uint256 nonce, address from, bytes32 sigR, bytes32 sigS, uint8 sigV, address recovered); | |
address public airnode; | |
bytes32 public endpointIdUint256; | |
address public sponsorWallet; | |
uint256 public chainId; | |
string public version; | |
address public authorizedAccount; | |
uint256 public mintCost = 0.01 ether; | |
uint256 public constant MAX_MINTS_PER_ADDRESS = 3; | |
Counters.Counter private _tokenIdTracker; | |
mapping(address => Counters.Counter) private _nonces; | |
mapping(uint256 => Classifier) public tokenIdToClassifier; | |
mapping(bytes32 => bool) public awaitingFulfillment; | |
mapping(bytes32 => address) public requestToMinter; | |
mapping(address => uint256) public minterToMintCount; | |
enum Classifier {FIRST, SECOND, THIRD} | |
string public firstUri; | |
string public secondUri; | |
string public thirdUri; | |
string private constant ERR_INVALID_SIGNER = "INVALID_SIGNER"; | |
string private constant ERR_REQUEST_ID_UNKNOWN = "Request ID not known"; | |
string private constant ERR_MINT_COST_NOT_MET = "Minting cost not met"; | |
string private constant ERR_MINT_LIMIT_REACHED = "Mint limit reached"; | |
string private constant ERR_VERIFICATION_FAILED = "Verification failed"; | |
constructor(string memory name, string memory symbol, address _airnodeRrp, string memory _version, uint256 _chainId) | |
RrpRequesterV0(_airnodeRrp) | |
ERC721(name, symbol) | |
EIP712(name, _version) { | |
version = _version; | |
chainId = _chainId; | |
} | |
// This function is being overridden to provide your custom logic | |
function _domainSeparator() internal view returns (bytes32) { | |
return keccak256( | |
abi.encode( | |
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), | |
keccak256(bytes(name())), | |
keccak256(bytes(version)), | |
chainId, | |
address(this) | |
) | |
); | |
} | |
function setParameters( | |
address _airnode, | |
bytes32 _endpointIdUint256, | |
address _sponsorWallet, | |
address _authorizedAccount | |
) external onlyOwner() { | |
airnode = _airnode; | |
endpointIdUint256 = _endpointIdUint256; | |
sponsorWallet = _sponsorWallet; | |
authorizedAccount = _authorizedAccount; | |
} | |
function setURIs( | |
string calldata _firstUri, | |
string calldata _secondUri, | |
string calldata _thirdUri | |
) external onlyOwner() { | |
firstUri = _firstUri; | |
secondUri = _secondUri; | |
thirdUri = _thirdUri; | |
} | |
function updateMintCost(uint256 newCost) external onlyOwner() { | |
mintCost = newCost; | |
emit MintCostChanged(newCost); | |
} | |
function requestRandomNFT( | |
address minter, | |
uint256 nonce, | |
uint256 deadline, | |
uint8 v, | |
bytes32 r, | |
bytes32 s | |
) external payable { | |
require(msg.value >= mintCost, ERR_MINT_COST_NOT_MET); | |
require(minterToMintCount[minter] < MAX_MINTS_PER_ADDRESS, ERR_MINT_LIMIT_REACHED); | |
bytes32 digest = keccak256( | |
abi.encodePacked( | |
"\x19\x01", | |
_domainSeparator(), | |
keccak256( | |
abi.encode( | |
REQUEST_TYPEHASH, | |
minter, | |
nonce, | |
deadline | |
) | |
) | |
) | |
); | |
// Recover the address of the signer | |
address signer = ECDSA.recover(digest, v, r, s); | |
require(signer == authorizedAccount, string(abi.encodePacked("Signer address: ", Strings.toHexString(uint160(signer))))); | |
// Create the request | |
bytes32 requestId = airnodeRrp.makeFullRequest( | |
airnode, | |
endpointIdUint256, | |
address(this), | |
sponsorWallet, | |
address(this), | |
this.fulfillRandom.selector, | |
"" | |
); | |
awaitingFulfillment[requestId] = true; | |
requestToMinter[requestId] = minter; | |
minterToMintCount[minter]++; | |
emit RequestedRandom(requestId); | |
} | |
function fulfillRandom(bytes32 requestId, bytes calldata data) external onlyAirnodeRrp { | |
require(awaitingFulfillment[requestId], ERR_REQUEST_ID_UNKNOWN); | |
address minter = requestToMinter[requestId]; | |
delete awaitingFulfillment[requestId]; | |
uint256 tokenId = _tokenIdTracker.current(); | |
_tokenIdTracker.increment(); | |
uint256 randomNumber = abi.decode(data, (uint256)); | |
if (randomNumber % 3 == 0) { | |
_mint(minter, tokenId, firstUri, Classifier.FIRST); | |
} else if (randomNumber % 3 == 1) { | |
_mint(minter, tokenId, secondUri, Classifier.SECOND); | |
} else { | |
_mint(minter, tokenId, thirdUri, Classifier.THIRD); | |
} | |
emit MintedRandomNFT(requestId, randomNumber); | |
} | |
function _mint(address minter, uint256 tokenId, string memory tokenUri, Classifier classifier) private { | |
_mint(minter, tokenId); | |
_setTokenURI(tokenId, tokenUri); | |
tokenIdToClassifier[tokenId] = classifier; | |
} | |
function withdraw(address to, uint256 amount) external onlyOwner() { | |
require(address(this).balance >= amount, "Not enough balance"); | |
payable(to).transfer(amount); | |
emit Withdrawn(to, amount); | |
} | |
function contractBalance() public view returns (uint256) { | |
return address(this).balance; | |
} | |
function getTokenIdTracker() public view returns (uint256) { | |
return _tokenIdTracker.current(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment