Skip to content

Commit 72bc819

Browse files
authored
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
1 parent db43f33 commit 72bc819

22 files changed

+600
-0
lines changed

random-svg-nft/.env.example

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
ETHERSCAN_API_KEY=<YOUR ETHERSCAN API>
2+
RINKEBY_URL=https://eth-rinkeby.alchemyapi.io/v2/<YOUR ALCHEMY KEY>
3+
PRIVATE_KEY=<YOUR PRIVATE KEY>
4+
SUBSCRIPTION_ID=<ID>

random-svg-nft/.eslintignore

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules
2+
artifacts
3+
cache
4+
coverage

random-svg-nft/.eslintrc.js

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
module.exports = {
2+
env: {
3+
browser: false,
4+
es2021: true,
5+
mocha: true,
6+
node: true,
7+
},
8+
plugins: ["@typescript-eslint"],
9+
extends: [
10+
"standard",
11+
"plugin:prettier/recommended",
12+
"plugin:node/recommended",
13+
],
14+
parser: "@typescript-eslint/parser",
15+
parserOptions: {
16+
ecmaVersion: 12,
17+
},
18+
rules: {
19+
"node/no-unsupported-features/es-syntax": [
20+
"error",
21+
{ ignores: ["modules"] },
22+
],
23+
},
24+
};

random-svg-nft/.gitignore

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
node_modules
2+
.env
3+
coverage
4+
coverage.json
5+
typechain
6+
7+
#Hardhat files
8+
cache
9+
artifacts
10+
11+
yarn.lock
12+
package-lock.json

random-svg-nft/.npmignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
hardhat.config.ts
2+
scripts
3+
test

random-svg-nft/.prettierignore

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules
2+
artifacts
3+
cache
4+
coverage*
5+
gasReporterOutput.json

random-svg-nft/.solhint.json

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"extends": "solhint:recommended",
3+
"rules": {
4+
"compiler-version": ["error", "^0.8.0"],
5+
"func-visibility": ["warn", { "ignoreConstructors": true }]
6+
}
7+
}

random-svg-nft/.solhintignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules

random-svg-nft/README.md

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Random Emoji NFT collection on OpenSea
2+
3+
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.
4+
5+
## What we are building
6+
7+
![demo](demo.png)
8+
9+
## Getting started
10+
11+
### Prerequisites
12+
13+
Be sure to have installed the following
14+
15+
- [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
16+
- [Node.js](https://nodejs.org/en/download/)
17+
- [Yarn](https://yarnpkg.com/getting-started/install)
18+
19+
### Installation
20+
21+
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.
22+
2. Clone the repo
23+
24+
```
25+
git clone https://github.com/smartcontractkit/smart-contract-examples.git
26+
```
27+
28+
3. Enter the direcory
29+
30+
```
31+
cd smart-contract-examples/random-svg-nft
32+
```
33+
34+
### Build and Deploy
35+
36+
1. Install packages
37+
38+
```shell
39+
yarn
40+
```
41+
42+
2. Compile contracts
43+
44+
```shell
45+
yarn compile
46+
```
47+
48+
3. Run tests
49+
50+
```shell
51+
yarn test
52+
```
53+
54+
or
55+
56+
```shell
57+
REPORT_GAS=true yarn test
58+
```
59+
60+
4. Run test coverage
61+
62+
```shell
63+
yarn coverage
64+
```
65+
66+
5. Deploy contract to Rinkeby
67+
68+
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)
69+
70+
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
71+
72+
```shell
73+
ETHERSCAN_API_KEY=<YOUR ETHERSCAN API>
74+
RINKEBY_URL=https://eth-rinkeby.alchemyapi.io/v2/<YOUR ALCHEMY KEY>
75+
PRIVATE_KEY=<YOUR PRIVATE KEY>
76+
SUBSCRIPTION_ID=<ID>
77+
```
78+
79+
After that run the deployment script which will
80+
81+
- deploy your smart contract to the Rinkeby
82+
- verify it on Etherscan
83+
84+
```shell
85+
yarn deploy
86+
```
87+
88+
Lastly, navigate back to the Chainlink VRF Subscription Managment Page and add the address of deployed smart contract as subscription consumer.
89+
90+
If the verification process fails because the new contract hasn't been indexed yet on Etherscan, run the next command from your terminal
91+
92+
```shell
93+
npx hardhat verify --network rinkeby <CONTRACT_ADDRESS> 0xd89b2bf150e3b9e13446986e571fb9cab24b13cea0a43ea20a6049a85cc807cc <SUBSCRIPTION_ID> 1000000 4 3
94+
```
95+
96+
### Minting
97+
98+
Call `mint()` function of your contract. Soon after the successful transaction, your NFT will be available on [OpenSea](https://testnets.opensea.io/)

random-svg-nft/contracts/EmojiNFT.sol

+135
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
//SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.7;
3+
4+
import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
5+
import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";
6+
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
7+
import "@openzeppelin/contracts/utils/Counters.sol";
8+
import "@openzeppelin/contracts/utils/Strings.sol";
9+
import "@openzeppelin/contracts/utils/Base64.sol";
10+
11+
contract EmojiNFT is ERC721URIStorage, VRFConsumerBaseV2 {
12+
using Counters for Counters.Counter;
13+
Counters.Counter private tokenIds;
14+
15+
string[] private emojis = [
16+
unicode"😁",
17+
unicode"😂",
18+
unicode"😍",
19+
unicode"😭",
20+
unicode"😴",
21+
unicode"😎",
22+
unicode"🤑",
23+
unicode"🥳",
24+
unicode"😱",
25+
unicode"🙄"
26+
];
27+
28+
VRFCoordinatorV2Interface internal immutable vrfCoordinator;
29+
bytes32 internal immutable keyHash;
30+
uint64 internal immutable subscriptionId;
31+
uint32 internal immutable callbackGasLimit;
32+
uint32 internal immutable numWords;
33+
uint16 internal immutable requestConfirmations;
34+
35+
mapping(uint256 => address) requestToSender;
36+
37+
event RandomnessRequested(uint256 indexed requestId);
38+
39+
constructor(
40+
address _vrfCoordinator,
41+
bytes32 _keyHash,
42+
uint64 _subscriptionId,
43+
uint32 _callbackGasLimit,
44+
uint16 _requestConfirmations
45+
) VRFConsumerBaseV2(_vrfCoordinator) ERC721("EmojiNFT", "EMOJI") {
46+
vrfCoordinator = VRFCoordinatorV2Interface(_vrfCoordinator);
47+
keyHash = _keyHash;
48+
subscriptionId = _subscriptionId;
49+
callbackGasLimit = _callbackGasLimit;
50+
numWords = 4;
51+
requestConfirmations = _requestConfirmations;
52+
}
53+
54+
function mint() public returns (uint256 requestId) {
55+
requestId = vrfCoordinator.requestRandomWords(
56+
keyHash,
57+
subscriptionId,
58+
requestConfirmations,
59+
callbackGasLimit,
60+
numWords
61+
);
62+
63+
requestToSender[requestId] = msg.sender;
64+
65+
emit RandomnessRequested(requestId);
66+
}
67+
68+
function pickRandomColor(uint256 firstRandomNumber, uint256 secondRandomNumber, uint256 thirdRandomNumber)
69+
internal
70+
pure
71+
returns (string memory)
72+
{
73+
uint256 r = firstRandomNumber % 256;
74+
uint256 g = secondRandomNumber % 256;
75+
uint256 b = thirdRandomNumber % 256;
76+
77+
return
78+
string(
79+
abi.encodePacked(
80+
"rgb(",
81+
Strings.toString(r),
82+
", ",
83+
Strings.toString(g),
84+
", ",
85+
Strings.toString(b),
86+
");"
87+
)
88+
);
89+
}
90+
91+
function createOnChainSvg(string memory emoji, string memory color) internal pure returns(string memory svg) {
92+
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:";
93+
string memory afterColorSvg = "' /><text x='50%' y='50%' class='base' dominant-baseline='middle' text-anchor='middle'>";
94+
95+
svg = string(abi.encodePacked(baseSvg, color, afterColorSvg, emoji, "</text></svg>"));
96+
}
97+
98+
function createTokenUri(string memory emoji, string memory svg) internal pure returns(string memory tokenUri) {
99+
string memory json = Base64.encode(
100+
bytes(
101+
string(
102+
abi.encodePacked(
103+
'{"name": "',
104+
emoji,
105+
'", "description": "Random Emoji NFT Collection Powered by Chainlink VRF", "image": "data:image/svg+xml;base64,',
106+
Base64.encode(bytes(svg)),
107+
'"}'
108+
)
109+
)
110+
)
111+
);
112+
113+
tokenUri = string(
114+
abi.encodePacked("data:application/json;base64,", json)
115+
);
116+
}
117+
118+
function fulfillRandomWords(uint256 requestId, uint256[] memory randomNumbers)
119+
internal
120+
override
121+
{
122+
uint256 tokenId = tokenIds.current();
123+
124+
uint256 emojiIndex = (randomNumbers[0] % emojis.length) + 1;
125+
string memory emoji = emojis[emojiIndex];
126+
string memory color = pickRandomColor(randomNumbers[1], randomNumbers[2], randomNumbers[3]);
127+
string memory svg = createOnChainSvg(emoji, color);
128+
string memory tokenUri = createTokenUri(emoji, svg);
129+
130+
_safeMint(requestToSender[requestId], tokenId);
131+
_setTokenURI(tokenId, tokenUri);
132+
133+
tokenIds.increment();
134+
}
135+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.4.24;
3+
4+
import "@chainlink/contracts/src/v0.4/LinkToken.sol";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
//SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.0;
3+
4+
import "@chainlink/contracts/src/v0.8/mocks/VRFCoordinatorV2Mock.sol";

random-svg-nft/demo.png

345 KB
Loading

random-svg-nft/hardhat.config.ts

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import * as dotenv from "dotenv";
2+
3+
import { HardhatUserConfig } from "hardhat/config";
4+
import "@nomiclabs/hardhat-etherscan";
5+
import "@nomiclabs/hardhat-waffle";
6+
import "@typechain/hardhat";
7+
import "hardhat-gas-reporter";
8+
import "solidity-coverage";
9+
10+
dotenv.config();
11+
12+
const config: HardhatUserConfig = {
13+
solidity: {
14+
compilers: [
15+
{
16+
version: "0.8.7",
17+
},
18+
{
19+
version: "0.4.24",
20+
},
21+
],
22+
},
23+
networks: {
24+
rinkeby: {
25+
url: process.env.RINKEBY_URL || "",
26+
accounts:
27+
process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [],
28+
},
29+
},
30+
gasReporter: {
31+
enabled: process.env.REPORT_GAS !== undefined,
32+
currency: "USD",
33+
},
34+
etherscan: {
35+
apiKey: {
36+
rinkeby: process.env.ETHERSCAN_API_KEY,
37+
},
38+
},
39+
};
40+
41+
export default config;

0 commit comments

Comments
 (0)