├── .gitignore ├── LICENSE ├── README.md ├── brownie-config.yaml ├── contracts ├── AdvancedCollectible.sol ├── SimpleCollectible.sol └── test_contracts │ ├── LinkToken.sol │ ├── MockOracle.sol │ ├── MockV3Aggregator.sol │ └── VRFCoordinatorMock.sol ├── img ├── pug.png ├── shiba-inu.png └── st-bernard.png ├── interfaces └── LinkTokenInterface.sol ├── metadata ├── rinkeby │ ├── 0-PUG.json │ ├── 0-SHIBA_INU.json │ ├── 1-SHIBA_INU.json │ └── 2-ST_BERNARD.json └── sample_metadata.py ├── requirements.txt ├── scripts ├── advanced_collectible │ ├── create_collectible.py │ ├── create_metadata.py │ ├── deploy_advanced.py │ ├── fund_collectible.py │ ├── get_tokens.py │ └── set_tokenuri.py ├── flatten.py ├── helpful_scripts.py ├── simple_collectible │ ├── create_collectible.py │ └── deploy_simple.py └── upload_to_pinata.py └── tests ├── conftest.py ├── test_advanced_collectible.py └── test_simple_collectible.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Brownie 2 | build 3 | .pytest_cache 4 | .env 5 | .DS* 6 | 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | pip-wheel-metadata/ 30 | share/python-wheels/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | MANIFEST 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .nox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | *.py,cover 57 | .hypothesis/ 58 | .pytest_cache/ 59 | 60 | # Translations 61 | *.mo 62 | *.pot 63 | 64 | # Django stuff: 65 | *.log 66 | local_settings.py 67 | db.sqlite3 68 | db.sqlite3-journal 69 | 70 | # Flask stuff: 71 | instance/ 72 | .webassets-cache 73 | 74 | # Scrapy stuff: 75 | .scrapy 76 | 77 | # Sphinx documentation 78 | docs/_build/ 79 | 80 | # PyBuilder 81 | target/ 82 | 83 | # Jupyter Notebook 84 | .ipynb_checkpoints 85 | 86 | # IPython 87 | profile_default/ 88 | ipython_config.py 89 | 90 | # pyenv 91 | .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 101 | __pypackages__/ 102 | 103 | # Celery stuff 104 | celerybeat-schedule 105 | celerybeat.pid 106 | 107 | # SageMath parsed files 108 | *.sage.py 109 | 110 | # Environments 111 | .env 112 | .venv 113 | env/ 114 | venv/ 115 | ENV/ 116 | env.bak/ 117 | venv.bak/ 118 | 119 | # Spyder project settings 120 | .spyderproject 121 | .spyproject 122 | 123 | # Rope project settings 124 | .ropeproject 125 | 126 | # mkdocs documentation 127 | /site 128 | 129 | # mypy 130 | .mypy_cache/ 131 | .dmypy.json 132 | dmypy.json 133 | 134 | # Pyre type checker 135 | .pyre/ 136 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Patrick Collins 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # create-your-own-NFT-and-sell-it 2 | 3 | ## Prerequisities 4 | 5 | Make sure you have the following installed: 6 | 7 | - Python 8 | - Node JS & npm 9 | - MetaMask 10 | - A Facebook or Twitter account 11 | 12 | `MetaMask` is wallet that runs as a self-contained application inside of your browser as an extension. It allows you to interact with Decentralised Applications. MetaMask makes accessing the Ethereum and Testnet blockchains very easy and provides you with wallet addresses. To install it: 13 | 14 | - Go to https://metamask.io and install the browser extension in Chrome or Firefox 15 | - Once downloaded it should redirect you to an onboarding screen, but if not click on the MetaMask icon in your extensions 16 | - Create a new wallet 17 | - During setup you will be presented with a secret phrase, copy and keep it somewhere secure where nobody else can access it 18 | - Once you've created your wallet you'll see that you are based on the Ethereum Mainnet network. Change this so that you're on the `Rinkeby Test Network` 19 | 20 | To complete this tutorial we will need some `Rinkeby ETH` and `Rinkeby LINK`, which are Ethereum and Chainlink respectively. We will use the ETH to use the deploy our smart contracts on the Ethereum blockchain and to get data from off-chain we will use our Chainlink. Chainlink is what's called a `Blockchain oracle`, in short it is the middleware berween the real world and the blockchain world https://dev.to/patrickalphac/off-chain-data-in-ethereum-how-to-access-4395. Let's start by getting some free Rinkeby ETH: 21 | 22 | - Go back to your MetaMask account and copy your Account ID. If you hover above your account name it should open a prompt for you to copy it 23 | - Go to https://faucet.rinkeby.io/ 24 | - Go to your Twitter or Facebook account, past your MetaMask Account ID into a new post and create your post. Follow the guidelines at the bottom of https://faucet.rinkeby.io/ 25 | - Copy the URL of this post on Twitter or Facebook 26 | - Paste it into the input bar on https://faucet.rinkeby.io/ 27 | - Wait for the transaction to complete and you should now have some ETH in your MetaMask account! 28 | 29 | Now we want to add some Rinkeby LINK to our wallet, we do this by: 30 | 31 | - Go to https://rinkeby.chain.link/ 32 | - Paste your MetaMask Account ID into the input box and click on the button to send 100 Test LINK 33 | - When complete, a green notification will appear at the top of screen with a link for you to view this transaction on the blockchain. Click on this link to view the transaction 34 | - Wait for this transaction to be confirmed by a miner on the blockchain, when it has been confirmed the status of this transaction should change to `Success` 35 | - If you check your MetaMask wallet, nothing will have changed. You now need to go back to your MetaMask account and click on `Add Token` 36 | - Complete the form on this page to add your Rinkeby LINK 37 | - The `Token Contract Address` can be found by searching for `LINK Rinkeby address` or by going to https://docs.chain.link/docs/acquire-link/ and looking for the `Rinkeby LINK Token` 38 | - After pasting the `Token Contract Address`, click next and confirm the Chainlink token 39 | - Now we're ready to go with our Ethereum and Chainlink! 40 | 41 | --- 42 | 43 | ## Setup environment 44 | 45 | Now we need to install two things `ganache-cli` and `eth-brownie`. You can do this with these two commands: 46 | 47 | npm install -g ganache-cli 48 | pip install eth-brownie 49 | 50 | For the code, let's make use of some excellent boilerplate code provided an NFT Brownie Mix repo. Use this command to clone all of the files into your current directory: 51 | 52 | git clone https://github.com/PatrickAlphaC/nft-mix . 53 | 54 | Finally we need to create some environment variables, go to the `.env` file in your directory and update the following variables 55 | 56 | ``` 57 | export PRIVATE_KEY= 58 | export WEB3_INFURA_PROJECT_ID= 59 | ``` 60 | 61 | - The private key can be found by accessing your MetaMask account, clicking on the 3 dots to bring up your account details and finally by clicking `Export Private Key` 62 | - The WEB3_INFURA_PROJECT_ID can be accessed by creating a free Infura account. This account will give us a way to send transactions to the blockchain. 63 | - To create an Infura account, to to https://infura.io/ 64 | - Sign up with an email and password 65 | - Confirm email address and then create an Ethereum project 66 | - Once created, you'll be presented with it's `PROJECT_ID`. Use this to create the `WEB3_INFURA_PROJECT_ID` within the `.env` file 67 | - Make sure to also change the Endpoint of the Ethereum project to `Rinkeby` 68 | 69 | As for the other environment variables in this file, keep them commented for now. 70 | 71 | --- 72 | 73 | ## Create our first contract and deploy! 74 | 75 | Now we're ready to run our code to deploy our NFT contract to the Rinkeby blockchain and create our first collectible! We will be deploying it on a platform called `OpenSea`. To do this, source the environment variables by running `source .env` within the root of your project. Now let's create a simple NFT contract on the Rinkeby blockchain by running: 76 | 77 | brownie run scripts/simple_collectible/deploy_simple.py --network rinkeby 78 | 79 | And then let's create our first collectible by running: 80 | 81 | brownie run scripts/simple_collectible/create_collectible.py --network rinkeby 82 | 83 | --- 84 | 85 | ## Diving into the simple contract 86 | 87 | To take a look at the contract that we just deployed, open up the file at this location `contracts/SimpleCollectible.sol`, which is a `solidity` file. It should look something like this: 88 | 89 | ``` 90 | // SPDX-License-Identifier: MIT 91 | pragma solidity 0.6.6; 92 | 93 | import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 94 | 95 | contract SimpleCollectible is ERC721 { 96 | uint256 public tokenCounter; 97 | constructor () public ERC721 ("Dogie", "DOG"){ 98 | tokenCounter = 0; 99 | } 100 | 101 | function createCollectible(string memory tokenURI) public returns (uint256) { 102 | uint256 newItemId = tokenCounter; 103 | _safeMint(msg.sender, newItemId); 104 | _setTokenURI(newItemId, tokenURI); 105 | tokenCounter = tokenCounter + 1; 106 | return newItemId; 107 | } 108 | 109 | } 110 | ``` 111 | 112 | You can see that first we are importing a package for our ERC721 token from OpenZeppelin. ERC721 is a token standard for non-fungible tokens which states that even though someone may be able to make a copy of this token, there will always be only one of this token. On the other hand, fungible tokens follow a different standard such as ERC20s making them replaceable or interchangeable. 113 | 114 | The ERC721 package gives us all the functionality that we will need for our tokens and we inherit all of these when we instantiate the `SimpleCollectible`. Within the constructor we give our NFT a name and a symbol, in this case `Dogie` and `DOG`. This means that every NFT we create using this contract will be of type `Dogie/DOG`, just like how every Pokemon on a Pokemon card is still a Pokemon, each Pokemon is unique but they are all Pokemon. In this case, we are just using a `DOG` as an example. 115 | 116 | Also within the constructor we have a `tokenCounter` which counts how many NFT's we've created of this type. This token counter is then used to create a `token_id` within our Python script. 117 | 118 | After the constructor, we have our function that created the collectible. This is what gets called from our `create_collectible.py` script. The `safeMint` function creates the new NFT and assigns it to whoever called the `createCollectible`, in this case `msg.sender`, along with a `newItemId` which based on the `tokenCounter`. This is how you can keep track of who owns what, by checking the owner of the `tokenId`. 119 | 120 | Finally, we called `setTokenURI`, the `tokenURI` for an NFT is a unique identifier of what the token looks like. The URI could be an API call over HTTPS, an IPFS hash or anything else unique. If you look in our `create_collectible.py` script, we define the `sample_token_uri` as a HTTPS resource. If you open this up in a web browser, you'll see this: 121 | 122 | ``` 123 | { 124 | "name": "PUG", 125 | "description": "An adorable PUG pup!", 126 | "image": "https://ipfs.io/ipfs/QmSsYRx3LpDAb1GZQm7zZ1AuHZjfbPkD6J7s9r41xu1mf8?filename=pug.png", 127 | "attributes": [ 128 | { 129 | "trait_type": "cuteness", 130 | "value": 100 131 | } 132 | ] 133 | } 134 | ``` 135 | 136 | This is all of the metadata that was used to build our collectible! It tells us what the NFT looks, its image, its attributes, its name and a description. This metadata follows a particular standard, reflected by the above JSON, which makes it easy for platforms such as OpenSea, Rarible, Mintable etc... to render NFT's because they all follow this standard! 137 | 138 | ## On-chain vs Off-chain metadata 139 | 140 | When smart contracts and NFT's came about it was super expensive to deploy a lot of data to the blockchain. Images as small as 1KB can easily cost over $1M to store! https://ethereum.stackexchange.com/questions/872/what-is-the-cost-to-store-1kb-10kb-100kb-worth-of-data-into-the-ethereum-block/896#896. This was a huge issue, especially when wanting to store creative or digital arts! To get around this, data is stored off-chain and on-chain. We store metadata on-chain so that we can program NFT's to interact with each other and we store some parts of the data off-chain, such as the image, because there is still not a great way to store large images on-chain. This is where `Chainlink` comes in, as mentioned earlier this is essentially the middleware that connects the blockchain world to the physical world. 141 | 142 | Remember that a blockchain, a distributed ledger, works by each node in the network being able to find the same end result given the same input, i.e. validate each block. When transactions are very simple, such as Bob and Alice exchanging £5, this can be easily validated and reproduced by every node in the network. 143 | 144 | But in the real world transactions aren't that simple, for example what if each transaction on a blockchain was validated by API's and Bob sends Alice a variable amount of £ based on the price of ETH. When every node on the network attempts to validate this transaction, they may get a different result because the API call that they made may return a different value of £, even if it was a fraction of a second later. Therefore none of the nodes would be able to agree and the system would fail. 145 | 146 | Therefore blockchain are designed to be entirely deterministic, that is if we were to replay every transaction, we would end up in the same end state. So if you start to introduce non-deterministic sources into a blockchain, such as API's which may change, be hacked or even deprecated, we can't validate the transactions. 147 | 148 | ### So how does a blockchain oracle solve this problem? 149 | 150 | A blockchain oracle is any device or entity that connects a deterministic blockchain with off-chain data. They enter every data input through an external transaction. This way we can be sure that the blockchain contains all of the information required to verify itself. 151 | 152 | However, one of the main reasons why we use a blockchain in the first place is for this idea of a decentralised network, with no single point of failure. But if we start storing data externally in a centralised fashion, this bring us back to square one right? 153 | 154 | `Chainlink` solves this problem and is the standard for decentralised oracles. A decentralised oracle or decentralised oracle network is a group of independent blockchain oracles that provide data to a blockchain. Every independent node or oracle in the decentralised oracle network independently retrieves data from an off-chain source and brings it on-chain. This data is then aggregated so the system can come to a deterministic value of truth for that data point. 155 | 156 | We these oracles, we are leveraging the same reliable decentralised infrastructure behind blockchain, but for blockchain oracles in order to connect real world data to the blockchain and to enable smart contracts. If nodes on the oracle network are hacked, Chainlink will leverage the decentralised network to carry on. 157 | 158 | --- 159 | 160 | ## Creating an advanced dynamic NFT 161 | 162 | A dynamic NFT is one that can change over time or have on-chain features that we can use to interact with each other. Use cases for these include unlimited customisation for video games, game characters or interactive art. 163 | 164 | To get things started with our advanced NFT, run the following two commands: 165 | 166 | brownie run scripts/advanced_collectible/deploy_advanced.py --network rinkeby 167 | brownie run scripts/advanced_collectible/create_collectible.py --network rinkeby 168 | 169 | Out collectible here is a random dog breed from `Chainlink VRF`, whuch provides random numbers specifically for smart contracts. The key here is that these random numbers are provably-fair and have a verifiable source of randomness which is ideal for smart contracts that rely on a unpredictable outcomes, e.g. in video games. 170 | 171 | Next, we want to create it's metadata by running: 172 | 173 | brownie run scripts/advanced_collectible/create_metadata.py --network rinkeby 174 | 175 | Then we need to set the token URI by running: 176 | 177 | brownie run scripts/advanced_collectible/set_tokenuri.py --network rinkeby 178 | 179 | And there we have it! Our advanced collectible should now be deploying to OpenSea! 180 | 181 | But what exactly did we do here, well let's look at the `AdvancedCollectible.sol` code and take a look: 182 | 183 | ``` 184 | pragma solidity 0.6.6; 185 | 186 | import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 187 | import "@chainlink/contracts/src/v0.6/VRFConsumerBase.sol"; 188 | 189 | contract AdvancedCollectible is ERC721, VRFConsumerBase { 190 | uint256 public tokenCounter; 191 | enum Breed{PUG, SHIBA_INU, ST_BERNARD} 192 | // add other things 193 | mapping(bytes32 => address) public requestIdToSender; 194 | mapping(bytes32 => string) public requestIdToTokenURI; 195 | mapping(uint256 => Breed) public tokenIdToBreed; 196 | mapping(bytes32 => uint256) public requestIdToTokenId; 197 | event requestedCollectible(bytes32 indexed requestId); 198 | 199 | 200 | bytes32 internal keyHash; 201 | uint256 internal fee; 202 | 203 | constructor(address _VRFCoordinator, address _LinkToken, bytes32 _keyhash) 204 | public 205 | VRFConsumerBase(_VRFCoordinator, _LinkToken) 206 | ERC721("Dogie", "DOG") 207 | { 208 | tokenCounter = 0; 209 | keyHash = _keyhash; 210 | fee = 0.1 * 10 ** 18; 211 | } 212 | 213 | function createCollectible(string memory tokenURI, uint256 userProvidedSeed) 214 | public returns (bytes32){ 215 | bytes32 requestId = requestRandomness(keyHash, fee, userProvidedSeed); 216 | requestIdToSender[requestId] = msg.sender; 217 | requestIdToTokenURI[requestId] = tokenURI; 218 | emit requestedCollectible(requestId); 219 | } 220 | 221 | function fulfillRandomness(bytes32 requestId, uint256 randomNumber) internal override { 222 | address dogOwner = requestIdToSender[requestId]; 223 | string memory tokenURI = requestIdToTokenURI[requestId]; 224 | uint256 newItemId = tokenCounter; 225 | _safeMint(dogOwner, newItemId); 226 | _setTokenURI(newItemId, tokenURI); 227 | Breed breed = Breed(randomNumber % 3); 228 | tokenIdToBreed[newItemId] = breed; 229 | requestIdToTokenId[requestId] = newItemId; 230 | tokenCounter = tokenCounter + 1; 231 | } 232 | 233 | function setTokenURI(uint256 tokenId, string memory _tokenURI) public { 234 | require( 235 | _isApprovedOrOwner(_msgSender(), tokenId), 236 | "ERC721: transfer caller is not owner nor approved" 237 | ); 238 | _setTokenURI(tokenId, _tokenURI); 239 | } 240 | } 241 | ``` 242 | 243 | As said before, we used `Chainlink VRF` to select a random breed of dog from PUG, SHIBA_INU or BERNARD using `requestRandomness`. The Chainlink node responds by calling the `fulfillRandomness` function and creates the collectible. Finally, we call our `setTokenURI` to give our NFT it's appearance. 244 | 245 | If you take a look inside `scripts/advanced_collectible/create_metadata.py` you can see that opur token URI is already defined for us for each breed of dog: 246 | 247 | ``` 248 | breed_to_image_uri = { 249 | "PUG": "https://ipfs.io/ipfs/QmSsYRx3LpDAb1GZQm7zZ1AuHZjfbPkD6J7s9r41xu1mf8?filename=pug.png", 250 | "SHIBA_INU": "https://ipfs.io/ipfs/QmYx6GsYAKnNzZ9A6NvEKV9nf1VaDzJrqDR23Y8YSkebLU?filename=shiba-inu.png", 251 | "ST_BERNARD": "https://ipfs.io/ipfs/QmUPjADFGEKmfohdTaNcWhp7VGk26h5jXDA7v3VtTnTLcW?filename=st-bernard.png", 252 | } 253 | ``` 254 | 255 | In this advanced example we didn't really get much control over the image we use or the token URI file containing our metadata. However, if we wanted to do this ourselves for complete customisation, we can do this too. We can do this by using `IPFS`, which is a free decentralised platform that will allow us to store: 256 | 257 | - The image of the NFT 258 | - And the token URI file 259 | 260 | To do this, we need to do a few things: 261 | 262 | - Download IPFS desktop: https://github.com/ipfs/ipfs-desktop 263 | - Click on the `Files` tab 264 | - Click on the `Import` button and choose file 265 | - Choose a file on your local machine 266 | - Wait for it to upload and then click on the elipsis (three dots) for this file 267 | - Click `Share link` 268 | - Copy the link 269 | - And finally replace the link(s) within `scripts/advanced_collectible/set_tokenuri.py` with our new token URI! 270 | 271 | There is one other thing to mention, if the tokenURI is only on our node, this means that when our node is down no one else can view it. To get over this issue, we want others to pin our NFT and we can use a service such as `Pinata` https://pinata.cloud/ to keep our data alive even when our IPFS node is down. Another option would be to use something like Filecoin https://docs.filecoin.io/ since Pinata is not as decentralised as it should be. 272 | -------------------------------------------------------------------------------- /brownie-config.yaml: -------------------------------------------------------------------------------- 1 | # exclude SafeMath when calculating test coverage 2 | # https://eth-brownie.readthedocs.io/en/v1.10.3/config.html#exclude_paths 3 | reports: 4 | exclude_contracts: 5 | - SafeMath 6 | dependencies: 7 | - smartcontractkit/chainlink-brownie-contracts@1.0.2 8 | - OpenZeppelin/openzeppelin-contracts@3.4.0 9 | compiler: 10 | solc: 11 | remappings: 12 | - '@chainlink=smartcontractkit/chainlink-brownie-contracts@1.0.2' 13 | - '@openzeppelin=OpenZeppelin/openzeppelin-contracts@3.4.0' 14 | # automatically fetch contract sources from Etherscan 15 | autofetch_sources: True 16 | dotenv: .env 17 | # set a custom mnemonic for the development network 18 | networks: 19 | default: development 20 | kovan: 21 | vrf_coordinator: '0xdD3782915140c8f3b190B5D67eAc6dc5760C46E9' 22 | link_token: '0xa36085F69e2889c224210F603D836748e7dC0088' 23 | keyhash: '0x6c3699283bda56ad74f6b855546325b68d482e983852a7a82979cc4807b641f4' 24 | fee: 100000000000000000 25 | oracle: '0x2f90A6D021db21e1B2A077c5a37B3C7E75D15b7e' 26 | jobId: '29fa9aa13bf1468788b7cc4a500a45b8' 27 | eth_usd_price_feed: '0x9326BFA02ADD2366b30bacB125260Af641031331' 28 | rinkeby: 29 | vrf_coordinator: '0xb3dCcb4Cf7a26f6cf6B120Cf5A73875B7BBc655B' 30 | link_token: '0x01be23585060835e02b77ef475b0cc51aa1e0709' 31 | keyhash: '0x2ed0feb3e7fd2022120aa84fab1945545a9f2ffc9076fd6156fa96eaff4c1311' 32 | fee: 100000000000000000 33 | oracle: '0x7AFe1118Ea78C1eae84ca8feE5C65Bc76CcF879e' 34 | jobId: '6d1bfe27e7034b1d87b5270556b17277' 35 | eth_usd_price_feed: '0x8A753747A1Fa494EC906cE90E9f37563A8AF630e' 36 | mumbai: 37 | eth_usd_price_feed: '0x0715A7794a1dc8e42615F059dD6e406A6594651A' 38 | binance: 39 | # link_token: ?? 40 | eth_usd_price_feed: '0x9ef1B8c0E4F7dc8bF5719Ea496883DC6401d5b2e' 41 | binance-fork: 42 | eth_usd_price_feed: '0x9ef1B8c0E4F7dc8bF5719Ea496883DC6401d5b2e' 43 | mainnet-fork: 44 | eth_usd_price_feed: '0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419' 45 | matic-fork: 46 | eth_usd_price_feed: '0xF9680D99D6C9589e2a93a78A04A279e509205945' 47 | wallets: 48 | from_key: ${PRIVATE_KEY} 49 | from_mnemonic: ${MNEMONIC} 50 | # You'd have to change the accounts.add to accounts.from_mnemonic to use from_mnemonic 51 | -------------------------------------------------------------------------------- /contracts/AdvancedCollectible.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.6.6; 2 | 3 | import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 4 | import "@chainlink/contracts/src/v0.6/VRFConsumerBase.sol"; 5 | 6 | contract AdvancedCollectible is ERC721, VRFConsumerBase { 7 | uint256 public tokenCounter; 8 | enum Breed {PUG, SHIBA_INU, ST_BERNARD} 9 | // add other things 10 | mapping(bytes32 => address) public requestIdToSender; 11 | mapping(bytes32 => string) public requestIdToTokenURI; 12 | mapping(uint256 => Breed) public tokenIdToBreed; 13 | mapping(bytes32 => uint256) public requestIdToTokenId; 14 | event requestedCollectible(bytes32 indexed requestId); 15 | 16 | bytes32 internal keyHash; 17 | uint256 internal fee; 18 | 19 | constructor( 20 | address _VRFCoordinator, 21 | address _LinkToken, 22 | bytes32 _keyhash 23 | ) 24 | public 25 | VRFConsumerBase(_VRFCoordinator, _LinkToken) 26 | ERC721("Dogie", "DOG") 27 | { 28 | tokenCounter = 0; 29 | keyHash = _keyhash; 30 | fee = 0.1 * 10**18; 31 | } 32 | 33 | function createCollectible(string memory tokenURI, uint256 userProvidedSeed) 34 | public 35 | returns (bytes32) 36 | { 37 | bytes32 requestId = requestRandomness(keyHash, fee, userProvidedSeed); 38 | requestIdToSender[requestId] = msg.sender; 39 | requestIdToTokenURI[requestId] = tokenURI; 40 | emit requestedCollectible(requestId); 41 | } 42 | 43 | function fulfillRandomness(bytes32 requestId, uint256 randomNumber) 44 | internal 45 | override 46 | { 47 | address dogOwner = requestIdToSender[requestId]; 48 | string memory tokenURI = requestIdToTokenURI[requestId]; 49 | uint256 newItemId = tokenCounter; 50 | _safeMint(dogOwner, newItemId); 51 | _setTokenURI(newItemId, tokenURI); 52 | Breed breed = Breed(randomNumber % 3); 53 | tokenIdToBreed[newItemId] = breed; 54 | requestIdToTokenId[requestId] = newItemId; 55 | tokenCounter = tokenCounter + 1; 56 | } 57 | 58 | function setTokenURI(uint256 tokenId, string memory _tokenURI) public { 59 | require( 60 | _isApprovedOrOwner(_msgSender(), tokenId), 61 | "ERC721: transfer caller is not owner nor approved" 62 | ); 63 | _setTokenURI(tokenId, _tokenURI); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /contracts/SimpleCollectible.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.6.6; 3 | 4 | import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 5 | 6 | contract SimpleCollectible is ERC721 { 7 | uint256 public tokenCounter; 8 | constructor () public ERC721 ("Dogie", "DOG"){ 9 | tokenCounter = 0; 10 | } 11 | 12 | function createCollectible(string memory tokenURI) public returns (uint256) { 13 | uint256 newItemId = tokenCounter; 14 | _safeMint(msg.sender, newItemId); 15 | _setTokenURI(newItemId, tokenURI); 16 | tokenCounter = tokenCounter + 1; 17 | return newItemId; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /contracts/test_contracts/LinkToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.11; 2 | 3 | 4 | import "@chainlink/contracts/src/v0.4/ERC677Token.sol"; 5 | import { StandardToken as linkStandardToken } from "@chainlink/contracts/src/v0.4/vendor/StandardToken.sol"; 6 | 7 | 8 | contract LinkToken is linkStandardToken, ERC677Token { 9 | 10 | uint public constant totalSupply = 10**27; 11 | string public constant name = "ChainLink Token"; 12 | uint8 public constant decimals = 18; 13 | string public constant symbol = "LINK"; 14 | 15 | function LinkToken() 16 | public 17 | { 18 | balances[msg.sender] = totalSupply; 19 | } 20 | 21 | /** 22 | * @dev transfer token to a specified address with additional data if the recipient is a contract. 23 | * @param _to The address to transfer to. 24 | * @param _value The amount to be transferred. 25 | * @param _data The extra data to be passed to the receiving contract. 26 | */ 27 | function transferAndCall(address _to, uint _value, bytes _data) 28 | public 29 | validRecipient(_to) 30 | returns (bool success) 31 | { 32 | return super.transferAndCall(_to, _value, _data); 33 | } 34 | 35 | /** 36 | * @dev transfer token to a specified address. 37 | * @param _to The address to transfer to. 38 | * @param _value The amount to be transferred. 39 | */ 40 | function transfer(address _to, uint _value) 41 | public 42 | validRecipient(_to) 43 | returns (bool success) 44 | { 45 | return super.transfer(_to, _value); 46 | } 47 | 48 | /** 49 | * @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender. 50 | * @param _spender The address which will spend the funds. 51 | * @param _value The amount of tokens to be spent. 52 | */ 53 | function approve(address _spender, uint256 _value) 54 | public 55 | validRecipient(_spender) 56 | returns (bool) 57 | { 58 | return super.approve(_spender, _value); 59 | } 60 | 61 | /** 62 | * @dev Transfer tokens from one address to another 63 | * @param _from address The address which you want to send tokens from 64 | * @param _to address The address which you want to transfer to 65 | * @param _value uint256 the amount of tokens to be transferred 66 | */ 67 | function transferFrom(address _from, address _to, uint256 _value) 68 | public 69 | validRecipient(_to) 70 | returns (bool) 71 | { 72 | return super.transferFrom(_from, _to, _value); 73 | } 74 | 75 | 76 | // MODIFIERS 77 | 78 | modifier validRecipient(address _recipient) { 79 | require(_recipient != address(0) && _recipient != address(this)); 80 | _; 81 | } 82 | 83 | } 84 | 85 | -------------------------------------------------------------------------------- /contracts/test_contracts/MockOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.6.0; 3 | 4 | import "@chainlink/contracts/src/v0.6/LinkTokenReceiver.sol"; 5 | import "@chainlink/contracts/src/v0.6/interfaces/ChainlinkRequestInterface.sol"; 6 | import "@chainlink/contracts/src/v0.6/interfaces/LinkTokenInterface.sol"; 7 | import "@chainlink/contracts/src/v0.6/vendor/SafeMathChainlink.sol"; 8 | 9 | /** 10 | * @title The Chainlink Mock Oracle contract 11 | * @notice Chainlink smart contract developers can use this to test their contracts 12 | */ 13 | contract MockOracle is ChainlinkRequestInterface, LinkTokenReceiver { 14 | using SafeMathChainlink for uint256; 15 | 16 | uint256 constant public EXPIRY_TIME = 5 minutes; 17 | uint256 constant private MINIMUM_CONSUMER_GAS_LIMIT = 400000; 18 | 19 | struct Request { 20 | address callbackAddr; 21 | bytes4 callbackFunctionId; 22 | } 23 | 24 | LinkTokenInterface internal LinkToken; 25 | mapping(bytes32 => Request) private commitments; 26 | 27 | event OracleRequest( 28 | bytes32 indexed specId, 29 | address requester, 30 | bytes32 requestId, 31 | uint256 payment, 32 | address callbackAddr, 33 | bytes4 callbackFunctionId, 34 | uint256 cancelExpiration, 35 | uint256 dataVersion, 36 | bytes data 37 | ); 38 | 39 | event CancelOracleRequest( 40 | bytes32 indexed requestId 41 | ); 42 | 43 | /** 44 | * @notice Deploy with the address of the LINK token 45 | * @dev Sets the LinkToken address for the imported LinkTokenInterface 46 | * @param _link The address of the LINK token 47 | */ 48 | constructor(address _link) 49 | public 50 | { 51 | LinkToken = LinkTokenInterface(_link); // external but already deployed and unalterable 52 | } 53 | 54 | /** 55 | * @notice Creates the Chainlink request 56 | * @dev Stores the hash of the params as the on-chain commitment for the request. 57 | * Emits OracleRequest event for the Chainlink node to detect. 58 | * @param _sender The sender of the request 59 | * @param _payment The amount of payment given (specified in wei) 60 | * @param _specId The Job Specification ID 61 | * @param _callbackAddress The callback address for the response 62 | * @param _callbackFunctionId The callback function ID for the response 63 | * @param _nonce The nonce sent by the requester 64 | * @param _dataVersion The specified data version 65 | * @param _data The CBOR payload of the request 66 | */ 67 | function oracleRequest( 68 | address _sender, 69 | uint256 _payment, 70 | bytes32 _specId, 71 | address _callbackAddress, 72 | bytes4 _callbackFunctionId, 73 | uint256 _nonce, 74 | uint256 _dataVersion, 75 | bytes calldata _data 76 | ) 77 | external 78 | override 79 | onlyLINK() 80 | checkCallbackAddress(_callbackAddress) 81 | { 82 | bytes32 requestId = keccak256(abi.encodePacked(_sender, _nonce)); 83 | require(commitments[requestId].callbackAddr == address(0), "Must use a unique ID"); 84 | // solhint-disable-next-line not-rely-on-time 85 | uint256 expiration = now.add(EXPIRY_TIME); 86 | 87 | commitments[requestId] = Request( 88 | _callbackAddress, 89 | _callbackFunctionId 90 | ); 91 | 92 | emit OracleRequest( 93 | _specId, 94 | _sender, 95 | requestId, 96 | _payment, 97 | _callbackAddress, 98 | _callbackFunctionId, 99 | expiration, 100 | _dataVersion, 101 | _data); 102 | } 103 | 104 | /** 105 | * @notice Called by the Chainlink node to fulfill requests 106 | * @dev Given params must hash back to the commitment stored from `oracleRequest`. 107 | * Will call the callback address' callback function without bubbling up error 108 | * checking in a `require` so that the node can get paid. 109 | * @param _requestId The fulfillment request ID that must match the requester's 110 | * @param _data The data to return to the consuming contract 111 | * @return Status if the external call was successful 112 | */ 113 | function fulfillOracleRequest( 114 | bytes32 _requestId, 115 | bytes32 _data 116 | ) 117 | external 118 | isValidRequest(_requestId) 119 | returns (bool) 120 | { 121 | Request memory req = commitments[_requestId]; 122 | delete commitments[_requestId]; 123 | require(gasleft() >= MINIMUM_CONSUMER_GAS_LIMIT, "Must provide consumer enough gas"); 124 | // All updates to the oracle's fulfillment should come before calling the 125 | // callback(addr+functionId) as it is untrusted. 126 | // See: https://solidity.readthedocs.io/en/develop/security-considerations.html#use-the-checks-effects-interactions-pattern 127 | (bool success, ) = req.callbackAddr.call(abi.encodeWithSelector(req.callbackFunctionId, _requestId, _data)); // solhint-disable-line avoid-low-level-calls 128 | return success; 129 | } 130 | 131 | /** 132 | * @notice Allows requesters to cancel requests sent to this oracle contract. Will transfer the LINK 133 | * sent for the request back to the requester's address. 134 | * @dev Given params must hash to a commitment stored on the contract in order for the request to be valid 135 | * Emits CancelOracleRequest event. 136 | * @param _requestId The request ID 137 | * @param _payment The amount of payment given (specified in wei) 138 | * @param _expiration The time of the expiration for the request 139 | */ 140 | function cancelOracleRequest( 141 | bytes32 _requestId, 142 | uint256 _payment, 143 | bytes4, 144 | uint256 _expiration 145 | ) 146 | external 147 | override 148 | { 149 | require(commitments[_requestId].callbackAddr != address(0), "Must use a unique ID"); 150 | // solhint-disable-next-line not-rely-on-time 151 | require(_expiration <= now, "Request is not expired"); 152 | 153 | delete commitments[_requestId]; 154 | emit CancelOracleRequest(_requestId); 155 | 156 | assert(LinkToken.transfer(msg.sender, _payment)); 157 | } 158 | 159 | /** 160 | * @notice Returns the address of the LINK token 161 | * @dev This is the public implementation for chainlinkTokenAddress, which is 162 | * an internal method of the ChainlinkClient contract 163 | */ 164 | function getChainlinkToken() 165 | public 166 | view 167 | override 168 | returns (address) 169 | { 170 | return address(LinkToken); 171 | } 172 | 173 | // MODIFIERS 174 | 175 | /** 176 | * @dev Reverts if request ID does not exist 177 | * @param _requestId The given request ID to check in stored `commitments` 178 | */ 179 | modifier isValidRequest(bytes32 _requestId) { 180 | require(commitments[_requestId].callbackAddr != address(0), "Must have a valid requestId"); 181 | _; 182 | } 183 | 184 | 185 | /** 186 | * @dev Reverts if the callback address is the LINK token 187 | * @param _to The callback address 188 | */ 189 | modifier checkCallbackAddress(address _to) { 190 | require(_to != address(LinkToken), "Cannot callback to LINK"); 191 | _; 192 | } 193 | 194 | } 195 | -------------------------------------------------------------------------------- /contracts/test_contracts/MockV3Aggregator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.6.0; 3 | 4 | import "@chainlink/contracts/src/v0.6/interfaces/AggregatorV2V3Interface.sol"; 5 | 6 | /** 7 | * @title MockV3Aggregator 8 | * @notice Based on the FluxAggregator contract 9 | * @notice Use this contract when you need to test 10 | * other contract's ability to read data from an 11 | * aggregator contract, but how the aggregator got 12 | * its answer is unimportant 13 | */ 14 | contract MockV3Aggregator is AggregatorV2V3Interface { 15 | uint256 constant public override version = 0; 16 | 17 | uint8 public override decimals; 18 | int256 public override latestAnswer; 19 | uint256 public override latestTimestamp; 20 | uint256 public override latestRound; 21 | 22 | mapping(uint256 => int256) public override getAnswer; 23 | mapping(uint256 => uint256) public override getTimestamp; 24 | mapping(uint256 => uint256) private getStartedAt; 25 | 26 | constructor( 27 | uint8 _decimals, 28 | int256 _initialAnswer 29 | ) public { 30 | decimals = _decimals; 31 | updateAnswer(_initialAnswer); 32 | } 33 | 34 | function updateAnswer( 35 | int256 _answer 36 | ) public { 37 | latestAnswer = _answer; 38 | latestTimestamp = block.timestamp; 39 | latestRound++; 40 | getAnswer[latestRound] = _answer; 41 | getTimestamp[latestRound] = block.timestamp; 42 | getStartedAt[latestRound] = block.timestamp; 43 | } 44 | 45 | function updateRoundData( 46 | uint80 _roundId, 47 | int256 _answer, 48 | uint256 _timestamp, 49 | uint256 _startedAt 50 | ) public { 51 | latestRound = _roundId; 52 | latestAnswer = _answer; 53 | latestTimestamp = _timestamp; 54 | getAnswer[latestRound] = _answer; 55 | getTimestamp[latestRound] = _timestamp; 56 | getStartedAt[latestRound] = _startedAt; 57 | } 58 | 59 | function getRoundData(uint80 _roundId) 60 | external 61 | view 62 | override 63 | returns ( 64 | uint80 roundId, 65 | int256 answer, 66 | uint256 startedAt, 67 | uint256 updatedAt, 68 | uint80 answeredInRound 69 | ) 70 | { 71 | return ( 72 | _roundId, 73 | getAnswer[_roundId], 74 | getStartedAt[_roundId], 75 | getTimestamp[_roundId], 76 | _roundId 77 | ); 78 | } 79 | 80 | function latestRoundData() 81 | external 82 | view 83 | override 84 | returns ( 85 | uint80 roundId, 86 | int256 answer, 87 | uint256 startedAt, 88 | uint256 updatedAt, 89 | uint80 answeredInRound 90 | ) 91 | { 92 | return ( 93 | uint80(latestRound), 94 | getAnswer[latestRound], 95 | getStartedAt[latestRound], 96 | getTimestamp[latestRound], 97 | uint80(latestRound) 98 | ); 99 | } 100 | 101 | function description() 102 | external 103 | view 104 | override 105 | returns (string memory) 106 | { 107 | return "v0.6/tests/MockV3Aggregator.sol"; 108 | } 109 | } 110 | 111 | // MockOracle 112 | // Function signatures, event signatures, log topics 113 | -------------------------------------------------------------------------------- /contracts/test_contracts/VRFCoordinatorMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.6.6; 3 | 4 | import "@chainlink/contracts/src/v0.6/interfaces/LinkTokenInterface.sol"; 5 | import "@chainlink/contracts/src/v0.6/VRFConsumerBase.sol"; 6 | 7 | contract VRFCoordinatorMock { 8 | 9 | LinkTokenInterface public LINK; 10 | 11 | event RandomnessRequest(address indexed sender, bytes32 indexed keyHash, uint256 indexed seed); 12 | 13 | constructor(address linkAddress) public { 14 | LINK = LinkTokenInterface(linkAddress); 15 | } 16 | 17 | function onTokenTransfer(address sender, uint256 fee, bytes memory _data) 18 | public 19 | onlyLINK 20 | { 21 | (bytes32 keyHash, uint256 seed) = abi.decode(_data, (bytes32, uint256)); 22 | emit RandomnessRequest(sender, keyHash, seed); 23 | } 24 | 25 | function callBackWithRandomness( 26 | bytes32 requestId, 27 | uint256 randomness, 28 | address consumerContract 29 | ) public { 30 | VRFConsumerBase v; 31 | bytes memory resp = abi.encodeWithSelector(v.rawFulfillRandomness.selector, requestId, randomness); 32 | uint256 b = 206000; 33 | require(gasleft() >= b, "not enough gas for consumer"); 34 | (bool success,) = consumerContract.call(resp); 35 | } 36 | 37 | modifier onlyLINK() { 38 | require(msg.sender == address(LINK), "Must use LINK token"); 39 | _; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /img/pug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code-creations-io/create-your-own-NFT-and-sell-it/ba7d3b33ccc3953cffb879d80af44bc95326cd96/img/pug.png -------------------------------------------------------------------------------- /img/shiba-inu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code-creations-io/create-your-own-NFT-and-sell-it/ba7d3b33ccc3953cffb879d80af44bc95326cd96/img/shiba-inu.png -------------------------------------------------------------------------------- /img/st-bernard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code-creations-io/create-your-own-NFT-and-sell-it/ba7d3b33ccc3953cffb879d80af44bc95326cd96/img/st-bernard.png -------------------------------------------------------------------------------- /interfaces/LinkTokenInterface.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6.6; 2 | 3 | interface LinkTokenInterface { 4 | function allowance(address owner, address spender) external view returns (uint256 remaining); 5 | function approve(address spender, uint256 value) external returns (bool success); 6 | function balanceOf(address owner) external view returns (uint256 balance); 7 | function decimals() external view returns (uint8 decimalPlaces); 8 | function decreaseApproval(address spender, uint256 addedValue) external returns (bool success); 9 | function increaseApproval(address spender, uint256 subtractedValue) external; 10 | function name() external view returns (string memory tokenName); 11 | function symbol() external view returns (string memory tokenSymbol); 12 | function totalSupply() external view returns (uint256 totalTokensIssued); 13 | function transfer(address to, uint256 value) external returns (bool success); 14 | function transferAndCall(address to, uint256 value, bytes calldata data) external returns (bool success); 15 | function transferFrom(address from, address to, uint256 value) external returns (bool success); 16 | } 17 | -------------------------------------------------------------------------------- /metadata/rinkeby/0-PUG.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PUG", 3 | "description": "An adorable PUG pup!", 4 | "image": "https://ipfs.io/ipfs/QmSsYRx3LpDAb1GZQm7zZ1AuHZjfbPkD6J7s9r41xu1mf8?filename=pug.png", 5 | "attributes": [ 6 | { 7 | "trait_type": "cuteness", 8 | "value": 100 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /metadata/rinkeby/0-SHIBA_INU.json: -------------------------------------------------------------------------------- 1 | {"name": "SHIBA_INU", "description": "An adorable SHIBA_INU pup!", "image": "https://ipfs.io/ipfs/QmYx6GsYAKnNzZ9A6NvEKV9nf1VaDzJrqDR23Y8YSkebLU?filename=shiba-inu.png", "attributes": [{"trait_type": "cuteness", "value": 100}]} -------------------------------------------------------------------------------- /metadata/rinkeby/1-SHIBA_INU.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SHIBA_INU", 3 | "description": "An adorable SHIBA_INU pup!", 4 | "image": "https://ipfs.io/ipfs/QmYx6GsYAKnNzZ9A6NvEKV9nf1VaDzJrqDR23Y8YSkebLU?filename=shiba-inu.png", 5 | "attributes": [ 6 | { 7 | "trait_type": "cuteness", 8 | "value": 100 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /metadata/rinkeby/2-ST_BERNARD.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ST_BERNARD", 3 | "description": "An adorable ST_BERNARD pup!", 4 | "image": "https://ipfs.io/ipfs/QmUPjADFGEKmfohdTaNcWhp7VGk26h5jXDA7v3VtTnTLcW?filename=st-bernard.png", 5 | "attributes": [ 6 | { 7 | "trait_type": "cuteness", 8 | "value": 100 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /metadata/sample_metadata.py: -------------------------------------------------------------------------------- 1 | metadata_template = { 2 | "name": "", 3 | "description": "", 4 | "image": "", 5 | "attributes": [{"trait_type": "cuteness", "value": 100}], 6 | } 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | eth-brownie>=1.10.0,<2.0.0 2 | -------------------------------------------------------------------------------- /scripts/advanced_collectible/create_collectible.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from brownie import AdvancedCollectible, accounts, config 3 | from scripts.helpful_scripts import get_breed, fund_advanced_collectible 4 | import time 5 | 6 | 7 | STATIC_SEED = 123 8 | 9 | 10 | def main(): 11 | dev = accounts.add(config["wallets"]["from_key"]) 12 | advanced_collectible = AdvancedCollectible[len(AdvancedCollectible) - 1] 13 | fund_advanced_collectible(advanced_collectible) 14 | transaction = advanced_collectible.createCollectible( 15 | "None", STATIC_SEED, {"from": dev} 16 | ) 17 | print("Waiting on second transaction...") 18 | # wait for the 2nd transaction 19 | transaction.wait(1) 20 | time.sleep(35) 21 | requestId = transaction.events["requestedCollectible"]["requestId"] 22 | token_id = advanced_collectible.requestIdToTokenId(requestId) 23 | breed = get_breed(advanced_collectible.tokenIdToBreed(token_id)) 24 | print("Dog breed of tokenId {} is {}".format(token_id, breed)) 25 | -------------------------------------------------------------------------------- /scripts/advanced_collectible/create_metadata.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import os 3 | import requests 4 | import json 5 | from brownie import AdvancedCollectible, network 6 | from metadata import sample_metadata 7 | from scripts.helpful_scripts import get_breed 8 | from pathlib import Path 9 | from dotenv import load_dotenv 10 | 11 | load_dotenv() 12 | 13 | breed_to_image_uri = { 14 | "PUG": "https://ipfs.io/ipfs/QmSsYRx3LpDAb1GZQm7zZ1AuHZjfbPkD6J7s9r41xu1mf8?filename=pug.png", 15 | "SHIBA_INU": "https://ipfs.io/ipfs/QmYx6GsYAKnNzZ9A6NvEKV9nf1VaDzJrqDR23Y8YSkebLU?filename=shiba-inu.png", 16 | "ST_BERNARD": "https://ipfs.io/ipfs/QmUPjADFGEKmfohdTaNcWhp7VGk26h5jXDA7v3VtTnTLcW?filename=st-bernard.png", 17 | } 18 | 19 | 20 | def main(): 21 | print("Working on " + network.show_active()) 22 | advanced_collectible = AdvancedCollectible[len(AdvancedCollectible) - 1] 23 | number_of_advanced_collectibles = advanced_collectible.tokenCounter() 24 | print( 25 | "The number of tokens you've deployed is: " 26 | + str(number_of_advanced_collectibles) 27 | ) 28 | write_metadata(number_of_advanced_collectibles, advanced_collectible) 29 | 30 | 31 | def write_metadata(token_ids, nft_contract): 32 | for token_id in range(token_ids): 33 | collectible_metadata = sample_metadata.metadata_template 34 | breed = get_breed(nft_contract.tokenIdToBreed(token_id)) 35 | metadata_file_name = ( 36 | "./metadata/{}/".format(network.show_active()) 37 | + str(token_id) 38 | + "-" 39 | + breed 40 | + ".json" 41 | ) 42 | if Path(metadata_file_name).exists(): 43 | print( 44 | "{} already found, delete it to overwrite!".format( 45 | metadata_file_name) 46 | ) 47 | else: 48 | print("Creating Metadata file: " + metadata_file_name) 49 | collectible_metadata["name"] = get_breed( 50 | nft_contract.tokenIdToBreed(token_id) 51 | ) 52 | collectible_metadata["description"] = "An adorable {} pup!".format( 53 | collectible_metadata["name"] 54 | ) 55 | image_to_upload = None 56 | if os.getenv("UPLOAD_IPFS") == "true": 57 | image_path = "./img/{}.png".format( 58 | breed.lower().replace('_', '-')) 59 | image_to_upload = upload_to_ipfs(image_path) 60 | image_to_upload = ( 61 | breed_to_image_uri[breed] if not image_to_upload else image_to_upload 62 | ) 63 | collectible_metadata["image"] = image_to_upload 64 | with open(metadata_file_name, "w") as file: 65 | json.dump(collectible_metadata, file) 66 | if os.getenv("UPLOAD_IPFS") == "true": 67 | upload_to_ipfs(metadata_file_name) 68 | 69 | # curl -X POST -F file=@metadata/rinkeby/0-SHIBA_INU.json http://localhost:5001/api/v0/add 70 | 71 | 72 | def upload_to_ipfs(filepath): 73 | with Path(filepath).open("rb") as fp: 74 | image_binary = fp.read() 75 | ipfs_url = ( 76 | os.getenv("IPFS_URL") 77 | if os.getenv("IPFS_URL") 78 | else "http://localhost:5001" 79 | ) 80 | response = requests.post(ipfs_url + "/api/v0/add", 81 | files={"file": image_binary}) 82 | ipfs_hash = response.json()["Hash"] 83 | filename = filepath.split("/")[-1:][0] 84 | image_uri = "https://ipfs.io/ipfs/{}?filename={}".format( 85 | ipfs_hash, filename) 86 | print(image_uri) 87 | return image_uri 88 | -------------------------------------------------------------------------------- /scripts/advanced_collectible/deploy_advanced.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from brownie import AdvancedCollectible, accounts, network, config 3 | from scripts.helpful_scripts import fund_advanced_collectible 4 | 5 | 6 | def main(): 7 | dev = accounts.add(config["wallets"]["from_key"]) 8 | print(network.show_active()) 9 | # publish_source = True if os.getenv("ETHERSCAN_TOKEN") else False # Currently having an issue with this 10 | publish_source = False 11 | advanced_collectible = AdvancedCollectible.deploy( 12 | config["networks"][network.show_active()]["vrf_coordinator"], 13 | config["networks"][network.show_active()]["link_token"], 14 | config["networks"][network.show_active()]["keyhash"], 15 | {"from": dev}, 16 | publish_source=publish_source, 17 | ) 18 | fund_advanced_collectible(advanced_collectible) 19 | return advanced_collectible 20 | -------------------------------------------------------------------------------- /scripts/advanced_collectible/fund_collectible.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from brownie import AdvancedCollectible 3 | from scripts.helpful_scripts import fund_advanced_collectible 4 | 5 | 6 | def main(): 7 | advanced_collectible = AdvancedCollectible[len(AdvancedCollectible) - 1] 8 | fund_advanced_collectible(advanced_collectible) 9 | -------------------------------------------------------------------------------- /scripts/advanced_collectible/get_tokens.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from brownie import SimpleCollectible, AdvancedCollectible, accounts, network, config 3 | from metadata import sample_metadata 4 | from scripts.helpful_scripts import get_breed 5 | 6 | 7 | def main(): 8 | print("Working on " + network.show_active()) 9 | advanced_collectible = AdvancedCollectible[len(SimpleCollectible) - 1] 10 | breakpoint() 11 | number_of_advanced_collectibles = advanced_collectible.tokenCounter() 12 | print(number_of_advanced_collectibles) 13 | -------------------------------------------------------------------------------- /scripts/advanced_collectible/set_tokenuri.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from brownie import SimpleCollectible, AdvancedCollectible, accounts, network, config 3 | from metadata import sample_metadata 4 | from scripts.helpful_scripts import get_breed, OPENSEA_FORMAT 5 | 6 | 7 | dog_metadata_dic = { 8 | "PUG": "https://ipfs.io/ipfs/Qmd9MCGtdVz2miNumBHDbvj8bigSgTwnr4SbyH6DNnpWdt?filename=0-PUG.json", 9 | "SHIBA_INU": "https://ipfs.io/ipfs/QmdryoExpgEQQQgJPoruwGJyZmz6SqV4FRTX1i73CT3iXn?filename=1-SHIBA_INU.json", 10 | "ST_BERNARD": "https://ipfs.io/ipfs/QmbBnUjyHHN7Ytq9xDsYF9sucZdDJLRkWz7vnZfrjMXMxs?filename=2-ST_BERNARD.json", 11 | } 12 | 13 | def main(): 14 | print("Working on " + network.show_active()) 15 | advanced_collectible = AdvancedCollectible[len(AdvancedCollectible) - 1] 16 | number_of_advanced_collectibles = advanced_collectible.tokenCounter() 17 | print( 18 | "The number of tokens you've deployed is: " 19 | + str(number_of_advanced_collectibles) 20 | ) 21 | for token_id in range(number_of_advanced_collectibles): 22 | breed = get_breed(advanced_collectible.tokenIdToBreed(token_id)) 23 | if not advanced_collectible.tokenURI(token_id).startswith("https://"): 24 | print("Setting tokenURI of {}".format(token_id)) 25 | set_tokenURI(token_id, advanced_collectible, 26 | dog_metadata_dic[breed]) 27 | else: 28 | print("Skipping {}, we already set that tokenURI!".format(token_id)) 29 | 30 | 31 | def set_tokenURI(token_id, nft_contract, tokenURI): 32 | dev = accounts.add(config["wallets"]["from_key"]) 33 | nft_contract.setTokenURI(token_id, tokenURI, {"from": dev}) 34 | print( 35 | "Awesome! You can view your NFT at {}".format( 36 | OPENSEA_FORMAT.format(nft_contract.address, token_id) 37 | ) 38 | ) 39 | print('Please give up to 20 minutes, and hit the "refresh metadata" button') 40 | -------------------------------------------------------------------------------- /scripts/flatten.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from brownie import AdvancedCollectible, accounts, network, config, interface 3 | import json 4 | 5 | 6 | def main(): 7 | flatten() 8 | 9 | 10 | def flatten(): 11 | file = open("./AdvancedCollectible_flattened.json", "w") 12 | json.dump(AdvancedCollectible.get_verification_info(), file) 13 | file.close() 14 | -------------------------------------------------------------------------------- /scripts/helpful_scripts.py: -------------------------------------------------------------------------------- 1 | from brownie import accounts, AdvancedCollectible, config, interface, network 2 | 3 | OPENSEA_FORMAT = "https://testnets.opensea.io/assets/{}/{}" 4 | 5 | def get_breed(breed_number): 6 | switch = {0: "PUG", 1: "SHIBA_INU", 2: "ST_BERNARD"} 7 | return switch[breed_number] 8 | 9 | 10 | def fund_advanced_collectible(nft_contract): 11 | dev = accounts.add(config["wallets"]["from_key"]) 12 | # Get the most recent PriceFeed Object 13 | interface.LinkTokenInterface( 14 | config["networks"][network.show_active()]["link_token"] 15 | ).transfer( 16 | nft_contract, config["networks"][network.show_active()]["fee"], {"from": dev} 17 | ) 18 | -------------------------------------------------------------------------------- /scripts/simple_collectible/create_collectible.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from brownie import SimpleCollectible, accounts, network, config 3 | from scripts.helpful_scripts import OPENSEA_FORMAT 4 | 5 | sample_token_uri = "https://ipfs.io/ipfs/Qmd9MCGtdVz2miNumBHDbvj8bigSgTwnr4SbyH6DNnpWdt?filename=0-PUG.json" 6 | 7 | def main(): 8 | dev = accounts.add(config["wallets"]["from_key"]) 9 | print(network.show_active()) 10 | simple_collectible = SimpleCollectible[len(SimpleCollectible) - 1] 11 | token_id = simple_collectible.tokenCounter() 12 | transaction = simple_collectible.createCollectible(sample_token_uri, {"from": dev}) 13 | transaction.wait(1) 14 | print( 15 | "Awesome! You can view your NFT at {}".format( 16 | OPENSEA_FORMAT.format(simple_collectible.address, token_id) 17 | ) 18 | ) 19 | print('Please give up to 20 minutes, and hit the "refresh metadata" button') 20 | -------------------------------------------------------------------------------- /scripts/simple_collectible/deploy_simple.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import os 3 | from brownie import SimpleCollectible, accounts, network, config 4 | from dotenv import load_dotenv 5 | 6 | load_dotenv() 7 | 8 | 9 | def main(): 10 | dev = accounts.add(config["wallets"]["from_key"]) 11 | print(network.show_active()) 12 | publish_source = True if os.getenv("ETHERSCAN_TOKEN") else False 13 | SimpleCollectible.deploy({"from": dev}, publish_source=publish_source) 14 | -------------------------------------------------------------------------------- /scripts/upload_to_pinata.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import os 3 | from pathlib import Path 4 | 5 | PINATA_BASE_URL = 'https://api.pinata.cloud/' 6 | endpoint = 'pinning/pinFileToIPFS' 7 | # Change this to upload a different file 8 | filepath = './img/pug.png' 9 | filename = filepath.split('/')[-1:][0] 10 | headers = {'pinata_api_key': os.getenv('PINATA_API_KEY'), 11 | 'pinata_secret_api_key': os.getenv('PINATA_API_SECRET')} 12 | 13 | 14 | with Path(filepath).open("rb") as fp: 15 | image_binary = fp.read() 16 | response = requests.post(PINATA_BASE_URL + endpoint, 17 | files={"file": (filename, image_binary)}, 18 | headers=headers) 19 | print(response.json()) 20 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from brownie import ( 3 | config, 4 | network, 5 | accounts, 6 | MockV3Aggregator, 7 | VRFCoordinatorMock, 8 | LinkToken, 9 | Contract, 10 | MockOracle, 11 | ) 12 | 13 | 14 | @pytest.fixture 15 | def get_eth_usd_price_feed_address(): 16 | if network.show_active() == "development": 17 | mock_price_feed = MockV3Aggregator.deploy(18, 2000, {"from": accounts[0]}) 18 | return mock_price_feed.address 19 | if network.show_active() in config["networks"]: 20 | return config["networks"][network.show_active()]["eth_usd_price_feed"] 21 | else: 22 | pytest.skip("Invalid network specified ") 23 | return 24 | 25 | 26 | @pytest.fixture(scope="module") 27 | def get_account(): 28 | if ( 29 | network.show_active() == "development" 30 | or network.show_active() == "mainnet-fork" 31 | ): 32 | return accounts[0] 33 | if network.show_active() in config["networks"]: 34 | dev_account = accounts.add(config["wallets"]["from_key"]) 35 | return dev_account 36 | else: 37 | pytest.skip("Invalid network/wallet specified ") 38 | 39 | 40 | @pytest.fixture(scope="module") 41 | def get_link_token(get_account): 42 | if network.show_active() == "development" or "fork" in network.show_active(): 43 | link_token = LinkToken.deploy({"from": get_account}) 44 | return link_token 45 | if network.show_active() in config["networks"]: 46 | return Contract.from_abi( 47 | "link_token", 48 | config["networks"][network.show_active()]["link_token"], 49 | LinkToken.abi, 50 | ) 51 | else: 52 | pytest.skip("Invalid network/link token specified ") 53 | 54 | 55 | @pytest.fixture 56 | def get_vrf_coordinator(get_account, get_link_token): 57 | if network.show_active() == "development" or "fork" in network.show_active(): 58 | mock_vrf_coordinator = VRFCoordinatorMock.deploy( 59 | get_link_token.address, {"from": get_account} 60 | ) 61 | return mock_vrf_coordinator 62 | if network.show_active() in config["networks"]: 63 | vrf_coordinator = Contract.from_abi( 64 | "vrf_coordinator", 65 | config["networks"][network.show_active()]["vrf_coordinator"], 66 | VRFCoordinatorMock.abi, 67 | ) 68 | return vrf_coordinator 69 | else: 70 | pytest.skip("Invalid network specified") 71 | 72 | 73 | @pytest.fixture 74 | def get_keyhash(get_account, get_link_token): 75 | if network.show_active() == "development" or "fork" in network.show_active(): 76 | return 0 77 | if network.show_active() in config["networks"]: 78 | return config["networks"][network.show_active()]["keyhash"] 79 | else: 80 | pytest.skip("Invalid network/link token specified ") 81 | 82 | 83 | @pytest.fixture 84 | def get_job_id(): 85 | if network.show_active() == "development" or "fork" in network.show_active(): 86 | return 0 87 | if network.show_active() in config["networks"]: 88 | return config["networks"][network.show_active()]["jobId"] 89 | else: 90 | pytest.skip("Invalid network/link token specified") 91 | 92 | 93 | @pytest.fixture 94 | def get_seed(): 95 | return 777 96 | 97 | 98 | @pytest.fixture 99 | def get_data(): 100 | return 100 101 | 102 | 103 | @pytest.fixture 104 | def get_oracle(get_link_token, get_account): 105 | if network.show_active() == "development" or "fork" in network.show_active(): 106 | mock_oracle = MockOracle.deploy(get_link_token.address, {"from": get_account}) 107 | return mock_oracle 108 | if network.show_active() in config["networks"]: 109 | mock_oracle = Contract.from_abi( 110 | "mock_oracle", 111 | config["networks"][network.show_active()]["oracle"], 112 | MockOracle.abi, 113 | ) 114 | return mock_oracle 115 | else: 116 | pytest.skip("Invalid network specified") 117 | 118 | 119 | @pytest.fixture 120 | def dev_account(): 121 | return accounts[0] 122 | 123 | 124 | @pytest.fixture 125 | def node_account(): 126 | return accounts[1] 127 | 128 | 129 | @pytest.fixture 130 | def chainlink_fee(): 131 | return 1000000000000000000 132 | 133 | 134 | @pytest.fixture 135 | def expiry_time(): 136 | return 300 137 | -------------------------------------------------------------------------------- /tests/test_advanced_collectible.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from brownie import network, AdvancedCollectible 3 | 4 | 5 | def test_can_create_advanced_collectible( 6 | get_account, 7 | get_vrf_coordinator, 8 | get_keyhash, 9 | get_link_token, 10 | chainlink_fee, 11 | get_seed, 12 | ): 13 | # Arrange 14 | if network.show_active() not in ["development"] or "fork" in network.show_active(): 15 | pytest.skip("Only for local testing") 16 | advanced_collectible = AdvancedCollectible.deploy( 17 | get_vrf_coordinator.address, 18 | get_link_token.address, 19 | get_keyhash, 20 | {"from": get_account}, 21 | ) 22 | get_link_token.transfer( 23 | advanced_collectible.address, chainlink_fee * 3, {"from": get_account} 24 | ) 25 | # Act 26 | transaction_receipt = advanced_collectible.createCollectible( 27 | "None", get_seed, {"from": get_account} 28 | ) 29 | requestId = transaction_receipt.events["requestedCollectible"]["requestId"] 30 | assert isinstance(transaction_receipt.txid, str) 31 | get_vrf_coordinator.callBackWithRandomness( 32 | requestId, 777, advanced_collectible.address, {"from": get_account} 33 | ) 34 | # Assert 35 | assert advanced_collectible.tokenCounter() > 0 36 | assert isinstance(advanced_collectible.tokenCounter(), int) 37 | -------------------------------------------------------------------------------- /tests/test_simple_collectible.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from brownie import network, SimpleCollectible, convert 3 | 4 | 5 | def test_can_create_simple_collectible(get_account): 6 | simple_collectible = SimpleCollectible.deploy({"from": get_account}) 7 | simple_collectible.createCollectible("None") 8 | assert simple_collectible.ownerOf(0) == get_account 9 | --------------------------------------------------------------------------------