-
Notifications
You must be signed in to change notification settings - Fork 92
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Random Emoji on-chain NFT on OpenSea example (#3)
* Add Random Emoji on-chain NFT on OpenSea example * Update README.md * Migrate example to VRF v2 * Adjust Readme file * Remove numWords constructor parameter because the contract needs exactly four random values from VRF
- Loading branch information
1 parent
9a87d17
commit 2070c6c
Showing
22 changed files
with
600 additions
and
0 deletions.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
ETHERSCAN_API_KEY=<YOUR ETHERSCAN API> | ||
RINKEBY_URL=https://eth-rinkeby.alchemyapi.io/v2/<YOUR ALCHEMY KEY> | ||
PRIVATE_KEY=<YOUR PRIVATE KEY> | ||
SUBSCRIPTION_ID=<ID> |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
node_modules | ||
artifacts | ||
cache | ||
coverage |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
module.exports = { | ||
env: { | ||
browser: false, | ||
es2021: true, | ||
mocha: true, | ||
node: true, | ||
}, | ||
plugins: ["@typescript-eslint"], | ||
extends: [ | ||
"standard", | ||
"plugin:prettier/recommended", | ||
"plugin:node/recommended", | ||
], | ||
parser: "@typescript-eslint/parser", | ||
parserOptions: { | ||
ecmaVersion: 12, | ||
}, | ||
rules: { | ||
"node/no-unsupported-features/es-syntax": [ | ||
"error", | ||
{ ignores: ["modules"] }, | ||
], | ||
}, | ||
}; |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
node_modules | ||
.env | ||
coverage | ||
coverage.json | ||
typechain | ||
|
||
#Hardhat files | ||
cache | ||
artifacts | ||
|
||
yarn.lock | ||
package-lock.json |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
hardhat.config.ts | ||
scripts | ||
test |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
node_modules | ||
artifacts | ||
cache | ||
coverage* | ||
gasReporterOutput.json |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"extends": "solhint:recommended", | ||
"rules": { | ||
"compiler-version": ["error", "^0.8.0"], | ||
"func-visibility": ["warn", { "ignoreConstructors": true }] | ||
} | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
node_modules |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
# Random Emoji NFT collection on OpenSea | ||
|
||
This demo project demonstrates how to create randomly generated on-chain Emoji NFTs using Chainlink VRF and how to host that collection on OpenSea on Rinkeby Testnet. | ||
|
||
## What we are building | ||
|
||
 | ||
|
||
## Getting started | ||
|
||
### Prerequisites | ||
|
||
Be sure to have installed the following | ||
|
||
- [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) | ||
- [Node.js](https://nodejs.org/en/download/) | ||
- [Yarn](https://yarnpkg.com/getting-started/install) | ||
|
||
### Installation | ||
|
||
1. Get a RPC API Key from a node provider such as [Alchemy](https://www.alchemy.com/), [Infura](https://infura.io/), [Moralis](https://moralis.io/), or [QuickNode](https://www.quicknode.com/). This example uses the RINKEBY Ethereum test network. | ||
2. Clone the repo | ||
|
||
``` | ||
git clone https://github.com/smartcontractkit/smart-contract-examples.git | ||
``` | ||
|
||
3. Enter the direcory | ||
|
||
``` | ||
cd smart-contract-examples/random-svg-nft | ||
``` | ||
|
||
### Build and Deploy | ||
|
||
1. Install packages | ||
|
||
```shell | ||
yarn | ||
``` | ||
|
||
2. Compile contracts | ||
|
||
```shell | ||
yarn compile | ||
``` | ||
|
||
3. Run tests | ||
|
||
```shell | ||
yarn test | ||
``` | ||
|
||
or | ||
|
||
```shell | ||
REPORT_GAS=true yarn test | ||
``` | ||
|
||
4. Run test coverage | ||
|
||
```shell | ||
yarn coverage | ||
``` | ||
|
||
5. Deploy contract to Rinkeby | ||
|
||
Go to [Chainlink VRF Subscription Managment Page](https://vrf.chain.link/), connect your wallet, create new subscription and fund it. Make sure to have at least 1 Rinkeby LINK in your wallet, you can obtain it from the [Chainlink Faucet](https://faucets.chain.link/arbitrum-rinkeby) | ||
|
||
Copy the `.env.example` file to a file named `.env`, and put your Private Key, RPC API Key, [Etherscan API Key](https://etherscan.io/apis), and Subscription ID like this | ||
|
||
```shell | ||
ETHERSCAN_API_KEY=<YOUR ETHERSCAN API> | ||
RINKEBY_URL=https://eth-rinkeby.alchemyapi.io/v2/<YOUR ALCHEMY KEY> | ||
PRIVATE_KEY=<YOUR PRIVATE KEY> | ||
SUBSCRIPTION_ID=<ID> | ||
``` | ||
|
||
After that run the deployment script which will | ||
|
||
- deploy your smart contract to the Rinkeby | ||
- verify it on Etherscan | ||
|
||
```shell | ||
yarn deploy | ||
``` | ||
|
||
Lastly, navigate back to the Chainlink VRF Subscription Managment Page and add the address of deployed smart contract as subscription consumer. | ||
|
||
If the verification process fails because the new contract hasn't been indexed yet on Etherscan, run the next command from your terminal | ||
|
||
```shell | ||
npx hardhat verify --network rinkeby <CONTRACT_ADDRESS> 0xd89b2bf150e3b9e13446986e571fb9cab24b13cea0a43ea20a6049a85cc807cc <SUBSCRIPTION_ID> 1000000 4 3 | ||
``` | ||
|
||
### Minting | ||
|
||
Call `mint()` function of your contract. Soon after the successful transaction, your NFT will be available on [OpenSea](https://testnets.opensea.io/) |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
//SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.7; | ||
|
||
import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol"; | ||
import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol"; | ||
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; | ||
import "@openzeppelin/contracts/utils/Counters.sol"; | ||
import "@openzeppelin/contracts/utils/Strings.sol"; | ||
import "@openzeppelin/contracts/utils/Base64.sol"; | ||
|
||
contract EmojiNFT is ERC721URIStorage, VRFConsumerBaseV2 { | ||
using Counters for Counters.Counter; | ||
Counters.Counter private tokenIds; | ||
|
||
string[] private emojis = [ | ||
unicode"😁", | ||
unicode"😂", | ||
unicode"😍", | ||
unicode"😭", | ||
unicode"😴", | ||
unicode"😎", | ||
unicode"🤑", | ||
unicode"🥳", | ||
unicode"😱", | ||
unicode"🙄" | ||
]; | ||
|
||
VRFCoordinatorV2Interface internal immutable vrfCoordinator; | ||
bytes32 internal immutable keyHash; | ||
uint64 internal immutable subscriptionId; | ||
uint32 internal immutable callbackGasLimit; | ||
uint32 internal immutable numWords; | ||
uint16 internal immutable requestConfirmations; | ||
|
||
mapping(uint256 => address) requestToSender; | ||
|
||
event RandomnessRequested(uint256 indexed requestId); | ||
|
||
constructor( | ||
address _vrfCoordinator, | ||
bytes32 _keyHash, | ||
uint64 _subscriptionId, | ||
uint32 _callbackGasLimit, | ||
uint16 _requestConfirmations | ||
) VRFConsumerBaseV2(_vrfCoordinator) ERC721("EmojiNFT", "EMOJI") { | ||
vrfCoordinator = VRFCoordinatorV2Interface(_vrfCoordinator); | ||
keyHash = _keyHash; | ||
subscriptionId = _subscriptionId; | ||
callbackGasLimit = _callbackGasLimit; | ||
numWords = 4; | ||
requestConfirmations = _requestConfirmations; | ||
} | ||
|
||
function mint() public returns (uint256 requestId) { | ||
requestId = vrfCoordinator.requestRandomWords( | ||
keyHash, | ||
subscriptionId, | ||
requestConfirmations, | ||
callbackGasLimit, | ||
numWords | ||
); | ||
|
||
requestToSender[requestId] = msg.sender; | ||
|
||
emit RandomnessRequested(requestId); | ||
} | ||
|
||
function pickRandomColor(uint256 firstRandomNumber, uint256 secondRandomNumber, uint256 thirdRandomNumber) | ||
internal | ||
pure | ||
returns (string memory) | ||
{ | ||
uint256 r = firstRandomNumber % 256; | ||
uint256 g = secondRandomNumber % 256; | ||
uint256 b = thirdRandomNumber % 256; | ||
|
||
return | ||
string( | ||
abi.encodePacked( | ||
"rgb(", | ||
Strings.toString(r), | ||
", ", | ||
Strings.toString(g), | ||
", ", | ||
Strings.toString(b), | ||
");" | ||
) | ||
); | ||
} | ||
|
||
function createOnChainSvg(string memory emoji, string memory color) internal pure returns(string memory svg) { | ||
string memory baseSvg = "<svg xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='xMinYMin meet' viewBox='0 0 350 350'><style>.base { font-size: 100px; }</style><rect width='100%' height='100%' style='fill:"; | ||
string memory afterColorSvg = "' /><text x='50%' y='50%' class='base' dominant-baseline='middle' text-anchor='middle'>"; | ||
|
||
svg = string(abi.encodePacked(baseSvg, color, afterColorSvg, emoji, "</text></svg>")); | ||
} | ||
|
||
function createTokenUri(string memory emoji, string memory svg) internal pure returns(string memory tokenUri) { | ||
string memory json = Base64.encode( | ||
bytes( | ||
string( | ||
abi.encodePacked( | ||
'{"name": "', | ||
emoji, | ||
'", "description": "Random Emoji NFT Collection Powered by Chainlink VRF", "image": "data:image/svg+xml;base64,', | ||
Base64.encode(bytes(svg)), | ||
'"}' | ||
) | ||
) | ||
) | ||
); | ||
|
||
tokenUri = string( | ||
abi.encodePacked("data:application/json;base64,", json) | ||
); | ||
} | ||
|
||
function fulfillRandomWords(uint256 requestId, uint256[] memory randomNumbers) | ||
internal | ||
override | ||
{ | ||
uint256 tokenId = tokenIds.current(); | ||
|
||
uint256 emojiIndex = (randomNumbers[0] % emojis.length) + 1; | ||
string memory emoji = emojis[emojiIndex]; | ||
string memory color = pickRandomColor(randomNumbers[1], randomNumbers[2], randomNumbers[3]); | ||
string memory svg = createOnChainSvg(emoji, color); | ||
string memory tokenUri = createTokenUri(emoji, svg); | ||
|
||
_safeMint(requestToSender[requestId], tokenId); | ||
_setTokenURI(tokenId, tokenUri); | ||
|
||
tokenIds.increment(); | ||
} | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.4.24; | ||
|
||
import "@chainlink/contracts/src/v0.4/LinkToken.sol"; |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
//SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.0; | ||
|
||
import "@chainlink/contracts/src/v0.8/mocks/VRFCoordinatorV2Mock.sol"; |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import * as dotenv from "dotenv"; | ||
|
||
import { HardhatUserConfig } from "hardhat/config"; | ||
import "@nomiclabs/hardhat-etherscan"; | ||
import "@nomiclabs/hardhat-waffle"; | ||
import "@typechain/hardhat"; | ||
import "hardhat-gas-reporter"; | ||
import "solidity-coverage"; | ||
|
||
dotenv.config(); | ||
|
||
const config: HardhatUserConfig = { | ||
solidity: { | ||
compilers: [ | ||
{ | ||
version: "0.8.7", | ||
}, | ||
{ | ||
version: "0.4.24", | ||
}, | ||
], | ||
}, | ||
networks: { | ||
rinkeby: { | ||
url: process.env.RINKEBY_URL || "", | ||
accounts: | ||
process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [], | ||
}, | ||
}, | ||
gasReporter: { | ||
enabled: process.env.REPORT_GAS !== undefined, | ||
currency: "USD", | ||
}, | ||
etherscan: { | ||
apiKey: { | ||
rinkeby: process.env.ETHERSCAN_API_KEY, | ||
}, | ||
}, | ||
}; | ||
|
||
export default config; |
Oops, something went wrong.