Skip to content

Commit

Permalink
Add Random Emoji on-chain NFT on OpenSea example (#3)
Browse files Browse the repository at this point in the history
* 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
andrejrakic authored Apr 5, 2022
1 parent 9a87d17 commit 2070c6c
Show file tree
Hide file tree
Showing 22 changed files with 600 additions and 0 deletions.
4 changes: 4 additions & 0 deletions random-svg-nft/.env.example
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>
4 changes: 4 additions & 0 deletions random-svg-nft/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
artifacts
cache
coverage
24 changes: 24 additions & 0 deletions random-svg-nft/.eslintrc.js
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"] },
],
},
};
12 changes: 12 additions & 0 deletions random-svg-nft/.gitignore
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
3 changes: 3 additions & 0 deletions random-svg-nft/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
hardhat.config.ts
scripts
test
5 changes: 5 additions & 0 deletions random-svg-nft/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules
artifacts
cache
coverage*
gasReporterOutput.json
7 changes: 7 additions & 0 deletions random-svg-nft/.solhint.json
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 }]
}
}
1 change: 1 addition & 0 deletions random-svg-nft/.solhintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
98 changes: 98 additions & 0 deletions random-svg-nft/README.md
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

![demo](demo.png)

## 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/)
135 changes: 135 additions & 0 deletions random-svg-nft/contracts/EmojiNFT.sol
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();
}
}
4 changes: 4 additions & 0 deletions random-svg-nft/contracts/test/LinkToken.sol
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";
4 changes: 4 additions & 0 deletions random-svg-nft/contracts/test/VRFCoordinatorV2Mock.sol
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";
Binary file added random-svg-nft/demo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
41 changes: 41 additions & 0 deletions random-svg-nft/hardhat.config.ts
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;
Loading

0 comments on commit 2070c6c

Please sign in to comment.