Skip to content

Instantly share code, notes, and snippets.

@ChristianOConnor
Last active May 31, 2023 00:54
Show Gist options
  • Save ChristianOConnor/76e4556b4c7b22d7494d2d9b814e9915 to your computer and use it in GitHub Desktop.
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.
// 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