├── requirements.txt ├── images ├── enter_item.png ├── mint_item.png ├── create_item.png ├── opensea_item.png └── deploy_contract.png ├── migrations_temp ├── 1_initial_migration.js └── 2_deploy_contracts.js ├── run_docker.sh ├── contracts_temp ├── common │ └── meta-transactions │ │ ├── Initializable.sol │ │ ├── ContentMixin.sol │ │ ├── EIP712Base.sol │ │ └── NativeMetaTransaction.sol ├── Migrations.sol ├── MyNFT.sol └── ERC721Tradable.sol ├── package.json ├── Dockerfile ├── LICENSE ├── .gitignore ├── modules ├── utils.py ├── pinata_api.py └── mint_nft.py ├── README.md ├── truffle.js ├── scripts └── mint_nft.py └── highwind_st.py /requirements.txt: -------------------------------------------------------------------------------- 1 | streamlit 2 | web3 3 | requests -------------------------------------------------------------------------------- /images/enter_item.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/banjtheman/highwind/HEAD/images/enter_item.png -------------------------------------------------------------------------------- /images/mint_item.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/banjtheman/highwind/HEAD/images/mint_item.png -------------------------------------------------------------------------------- /images/create_item.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/banjtheman/highwind/HEAD/images/create_item.png -------------------------------------------------------------------------------- /images/opensea_item.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/banjtheman/highwind/HEAD/images/opensea_item.png -------------------------------------------------------------------------------- /images/deploy_contract.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/banjtheman/highwind/HEAD/images/deploy_contract.png -------------------------------------------------------------------------------- /migrations_temp/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | const Migrations = artifacts.require("Migrations"); 2 | 3 | module.exports = function (deployer) { 4 | deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /run_docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "Starting Highwind docker image" 3 | mkdir -p highwind_jsons 4 | mkdir -p highwind_jsons/items/ 5 | mkdir -p highwind_jsons/contracts/ 6 | mkdir -p build 7 | docker run -d -p 8501:8501 -v $PWD/highwind_jsons:/home/highwind_jsons -v $PWD/build:/home/build highwind 8 | echo "Website live at localhost:8501" -------------------------------------------------------------------------------- /contracts_temp/common/meta-transactions/Initializable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | contract Initializable { 6 | bool inited = false; 7 | 8 | modifier initializer() { 9 | require(!inited, "already inited"); 10 | _; 11 | inited = true; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /migrations_temp/2_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | const NFT = artifacts.require("REPLACE_NAME"); 2 | const proxyRegistryAddress = process.env.OWNER_ADDRESS 3 | 4 | 5 | module.exports = async function (deployer, _network, accounts) { 6 | await deployer.deploy(NFT,proxyRegistryAddress, {gas: 5000000}); 7 | const nft = await NFT.deployed(); 8 | }; 9 | -------------------------------------------------------------------------------- /contracts_temp/Migrations.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.4.22 <0.9.0; 3 | 4 | contract Migrations { 5 | address public owner = msg.sender; 6 | uint public last_completed_migration; 7 | 8 | modifier restricted() { 9 | require( 10 | msg.sender == owner, 11 | "This function is restricted to the contract's owner" 12 | ); 13 | _; 14 | } 15 | 16 | function setCompleted(uint completed) public restricted { 17 | last_completed_migration = completed; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "highwind-nfts", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "truffle-config.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "keywords": [ 13 | "nfts", 14 | "github", 15 | "eth" 16 | ], 17 | "author": "Banjo Obayomi", 18 | "license": "ISC", 19 | "dependencies": { 20 | "@openzeppelin/contracts": "^4.0.0-beta.0", 21 | "ethereumjs-util": "^6.2.1" 22 | }, 23 | "devDependencies": { 24 | "@truffle/hdwallet-provider": "^1.4.0", 25 | "ethereumjs-tx": "^1.3.7", 26 | "openzeppelin-solidity": "^4.1.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nikolaik/python-nodejs:python3.9-nodejs16 2 | WORKDIR /home 3 | 4 | # Install deps 5 | ADD requirements.txt /home/ 6 | ADD package-lock.json /home/ 7 | ADD package.json /home/ 8 | RUN pip install -r requirements.txt 9 | RUN npm install --save-dev 10 | RUN npm install truffle -g 11 | 12 | # Add Files 13 | ADD highwind_st.py /home/ 14 | ADD modules /home/modules/ 15 | ADD migrations_temp /home/migrations_temp/ 16 | ADD contracts_temp /home/contracts_temp/ 17 | ADD truffle.js /home/ 18 | ADD migrations_temp /home/migrations/ 19 | ADD scripts/ /home/scripts/ 20 | 21 | # Make dirs 22 | RUN mkdir -p /home/build 23 | RUN mkdir -p /home/highwind_jsons 24 | RUN mkdir -p /home/highwind_jsons/items/ 25 | RUN mkdir -p /home/highwind_jsons/contracts/ 26 | RUN mkdir -p /home/migrations 27 | 28 | # Expose port 29 | EXPOSE 8501 30 | # Start App 31 | CMD [ "streamlit", "run" ,"highwind_st.py" ] -------------------------------------------------------------------------------- /contracts_temp/common/meta-transactions/ContentMixin.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | abstract contract ContextMixin { 6 | function msgSender() 7 | internal 8 | view 9 | returns (address payable sender) 10 | { 11 | if (msg.sender == address(this)) { 12 | bytes memory array = msg.data; 13 | uint256 index = msg.data.length; 14 | assembly { 15 | // Load the 32 bytes word from memory with the address on the lower 20 bytes, and mask those. 16 | sender := and( 17 | mload(add(array, index)), 18 | 0xffffffffffffffffffffffffffffffffffffffff 19 | ) 20 | } 21 | } else { 22 | sender = payable(msg.sender); 23 | } 24 | return sender; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /contracts_temp/MyNFT.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "./ERC721Tradable.sol"; 5 | 6 | /** 7 | * @title MyNFT 8 | * MyNFT - My NFT contract. 9 | */ 10 | contract REPLACE_NAME is ERC721Tradable { 11 | uint256 public nextTokenId; 12 | address public admin; 13 | 14 | constructor(address _proxyRegistryAddress) 15 | public 16 | ERC721Tradable("REPLACE_NAME", "REPLACE_SYM", _proxyRegistryAddress) 17 | { 18 | admin = msg.sender; 19 | } 20 | 21 | // only our wallet should be able to mint 22 | function mint(address to, string memory tokenURI) external onlyOwner { 23 | _safeMint(to, nextTokenId); 24 | _setTokenURI(nextTokenId, tokenURI); 25 | nextTokenId++; 26 | } 27 | 28 | function baseTokenURI() public pure override returns (string memory) { 29 | return ""; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Banjo Obayomi 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 | -------------------------------------------------------------------------------- /contracts_temp/common/meta-transactions/EIP712Base.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import {Initializable} from "./Initializable.sol"; 6 | 7 | contract EIP712Base is Initializable { 8 | struct EIP712Domain { 9 | string name; 10 | string version; 11 | address verifyingContract; 12 | bytes32 salt; 13 | } 14 | 15 | string constant public ERC712_VERSION = "1"; 16 | 17 | bytes32 internal constant EIP712_DOMAIN_TYPEHASH = keccak256( 18 | bytes( 19 | "EIP712Domain(string name,string version,address verifyingContract,bytes32 salt)" 20 | ) 21 | ); 22 | bytes32 internal domainSeperator; 23 | 24 | // supposed to be called once while initializing. 25 | // one of the contracts that inherits this contract follows proxy pattern 26 | // so it is not possible to do this in a constructor 27 | function _initializeEIP712( 28 | string memory name 29 | ) 30 | internal 31 | initializer 32 | { 33 | _setDomainSeperator(name); 34 | } 35 | 36 | function _setDomainSeperator(string memory name) internal { 37 | domainSeperator = keccak256( 38 | abi.encode( 39 | EIP712_DOMAIN_TYPEHASH, 40 | keccak256(bytes(name)), 41 | keccak256(bytes(ERC712_VERSION)), 42 | address(this), 43 | bytes32(getChainId()) 44 | ) 45 | ); 46 | } 47 | 48 | function getDomainSeperator() public view returns (bytes32) { 49 | return domainSeperator; 50 | } 51 | 52 | function getChainId() public view returns (uint256) { 53 | uint256 id; 54 | assembly { 55 | id := chainid() 56 | } 57 | return id; 58 | } 59 | 60 | /** 61 | * Accept message hash and returns hash message in EIP712 compatible form 62 | * So that it can be used to recover signer from signature signed using EIP712 formatted data 63 | * https://eips.ethereum.org/EIPS/eip-712 64 | * "\\x19" makes the encoding deterministic 65 | * "\\x01" is the version byte to make it compatible to EIP-191 66 | */ 67 | function toTypedMessageHash(bytes32 messageHash) 68 | internal 69 | view 70 | returns (bytes32) 71 | { 72 | return 73 | keccak256( 74 | abi.encodePacked("\x19\x01", getDomainSeperator(), messageHash) 75 | ); 76 | } 77 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Added 2 | node_modules/ 3 | highwind_jsons/ 4 | highwind_notes.txt 5 | 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | pip-wheel-metadata/ 29 | share/python-wheels/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | MANIFEST 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .nox/ 49 | .coverage 50 | .coverage.* 51 | .cache 52 | nosetests.xml 53 | coverage.xml 54 | *.cover 55 | *.py,cover 56 | .hypothesis/ 57 | .pytest_cache/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | db.sqlite3-journal 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | 79 | # PyBuilder 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | .python-version 91 | 92 | # pipenv 93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 96 | # install all needed dependencies. 97 | #Pipfile.lock 98 | 99 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 100 | __pypackages__/ 101 | 102 | # Celery stuff 103 | celerybeat-schedule 104 | celerybeat.pid 105 | 106 | # SageMath parsed files 107 | *.sage.py 108 | 109 | # Environments 110 | .env 111 | .venv 112 | env/ 113 | venv/ 114 | ENV/ 115 | env.bak/ 116 | venv.bak/ 117 | 118 | # Spyder project settings 119 | .spyderproject 120 | .spyproject 121 | 122 | # Rope project settings 123 | .ropeproject 124 | 125 | # mkdocs documentation 126 | /site 127 | 128 | # mypy 129 | .mypy_cache/ 130 | .dmypy.json 131 | dmypy.json 132 | 133 | # Pyre type checker 134 | .pyre/ 135 | -------------------------------------------------------------------------------- /modules/utils.py: -------------------------------------------------------------------------------- 1 | # Python Library Imports 2 | import logging 3 | import os 4 | import json 5 | from subprocess import Popen 6 | from typing import Type, Union, Dict, Any 7 | 8 | 9 | def load_json(path_to_json: str) -> Dict[str, Any]: 10 | """ 11 | Purpose: 12 | Load json files 13 | Args: 14 | path_to_json (String): Path to json file 15 | Returns: 16 | Conf: JSON file if loaded, else None 17 | """ 18 | try: 19 | with open(path_to_json, "r") as config_file: 20 | conf = json.load(config_file) 21 | return conf 22 | 23 | except Exception as error: 24 | logging.error(error) 25 | raise TypeError("Invalid JSON file") 26 | 27 | 28 | def save_json(json_path: str, json_data: Any) -> None: 29 | """ 30 | Purpose: 31 | Save json files 32 | Args: 33 | path_to_json (String): Path to json file 34 | json_data: Data to save 35 | Returns: 36 | N/A 37 | """ 38 | try: 39 | with open(json_path, "w") as outfile: 40 | json.dump(json_data, outfile) 41 | except Exception as error: 42 | raise OSError(error) 43 | 44 | 45 | def append_to_file(file_path: str, file_text: str) -> bool: 46 | """ 47 | Purpose: 48 | Append text to a file 49 | Args/Requests: 50 | file_path: file path 51 | file_text: Text of file 52 | Return: 53 | Status: True if appended, False if failed 54 | """ 55 | 56 | try: 57 | with open(file_path, "a") as myfile: 58 | myfile.write(file_text) 59 | return True 60 | 61 | except Exception as error: 62 | logging.error(error) 63 | return False 64 | 65 | 66 | def read_from_file(file_path: str) -> str: 67 | """ 68 | Purpose: 69 | Read data from a file 70 | Args/Requests: 71 | file_path: file path 72 | Return: 73 | read_data: Text from file 74 | """ 75 | try: 76 | with open(file_path) as f: 77 | read_data = f.read() 78 | 79 | except Exception as error: 80 | logging.error(error) 81 | return None 82 | 83 | return read_data 84 | 85 | 86 | def write_to_file(file_path: str, file_text: str) -> bool: 87 | """ 88 | Purpose: 89 | Write text from a file 90 | Args/Requests: 91 | file_path: file path 92 | file_text: Text of file 93 | Return: 94 | Status: True if appened, False if failed 95 | """ 96 | 97 | try: 98 | with open(file_path, "w") as myfile: 99 | myfile.write(file_text) 100 | return True 101 | 102 | except Exception as error: 103 | logging.error(error) 104 | return False 105 | -------------------------------------------------------------------------------- /modules/pinata_api.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Type, Union, Dict, Any, List 3 | import requests 4 | import os 5 | from pathlib import Path 6 | 7 | 8 | def pinJSONToIPFS( 9 | json_obj: Dict[str, Any], pinata_api_key: str, pinata_secret: str 10 | ) -> Dict[str, Any]: 11 | """ 12 | Purpose: 13 | PIN a json obj to IPFS 14 | Args: 15 | json_obj - The json obj 16 | pinata_api_key - pinata api key 17 | pinata_secret - pinata secret key 18 | Returns: 19 | ipfs json - data from pin 20 | """ 21 | HEADERS = { 22 | "pinata_api_key": pinata_api_key, 23 | "pinata_secret_api_key": pinata_secret, 24 | } 25 | 26 | ipfs_json = { 27 | "pinataMetadata": { 28 | "name": json_obj["name"], 29 | }, 30 | "pinataContent": json_obj, 31 | } 32 | 33 | endpoint_uri = "https://api.pinata.cloud/pinning/pinJSONToIPFS" 34 | response = requests.post(endpoint_uri, headers=HEADERS, json=ipfs_json) 35 | return response.json() 36 | 37 | 38 | def pinContentToIPFS( 39 | filepath: str, pinata_api_key: str, pinata_secret: str 40 | ) -> Dict[str, Any]: 41 | """ 42 | Purpose: 43 | PIN a file obj to IPFS 44 | Args: 45 | filepath - file path 46 | pinata_api_key - pinata api key 47 | pinata_secret - pinata secret key 48 | Returns: 49 | ipfs json - data from pin 50 | """ 51 | 52 | HEADERS = { 53 | "pinata_api_key": pinata_api_key, 54 | "pinata_secret_api_key": pinata_secret, 55 | } 56 | 57 | endpoint_uri = "https://api.pinata.cloud/pinning/pinFileToIPFS" 58 | 59 | filename = filepath.split("/")[-1:][0] 60 | 61 | with Path(filepath).open("rb") as fp: 62 | image_binary = fp.read() 63 | response = requests.post( 64 | endpoint_uri, files={"file": (filename, image_binary)}, headers=HEADERS 65 | ) 66 | print(response.json()) 67 | 68 | # response = requests.post(endpoint_uri, data=multipart_form_data, headers=HEADERS) 69 | # print(response.text) 70 | # print(response.headers) 71 | return response.json() 72 | 73 | 74 | def pinSearch( 75 | query: str, pinata_api_key: str, pinata_secret: str 76 | ) -> List[Dict[str, Any]]: 77 | """ 78 | Purpose: 79 | Query pins for data 80 | Args: 81 | query - the query str 82 | pinata_api_key - pinata api key 83 | pinata_secret - pinata secret key 84 | Returns: 85 | data - array of pined objects 86 | """ 87 | 88 | endpoint_uri = f"https://api.pinata.cloud/data/pinList?{query}" 89 | HEADERS = { 90 | "pinata_api_key": pinata_api_key, 91 | "pinata_secret_api_key": pinata_secret, 92 | } 93 | response = requests.get(endpoint_uri, headers=HEADERS).json() 94 | 95 | # now get the actual data from this 96 | data = [] 97 | if "rows" in response: 98 | 99 | for item in response["rows"]: 100 | ipfs_pin_hash = item["ipfs_pin_hash"] 101 | hash_data = requests.get( 102 | f"https://gateway.pinata.cloud/ipfs/{ipfs_pin_hash}" 103 | ).json() 104 | data.append(hash_data) 105 | 106 | # print(response.json()) 107 | return data 108 | -------------------------------------------------------------------------------- /contracts_temp/common/meta-transactions/NativeMetaTransaction.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import {SafeMath} from "openzeppelin-solidity/contracts/utils/math/SafeMath.sol"; 6 | import {EIP712Base} from "./EIP712Base.sol"; 7 | 8 | contract NativeMetaTransaction is EIP712Base { 9 | using SafeMath for uint256; 10 | bytes32 private constant META_TRANSACTION_TYPEHASH = keccak256( 11 | bytes( 12 | "MetaTransaction(uint256 nonce,address from,bytes functionSignature)" 13 | ) 14 | ); 15 | event MetaTransactionExecuted( 16 | address userAddress, 17 | address payable relayerAddress, 18 | bytes functionSignature 19 | ); 20 | mapping(address => uint256) nonces; 21 | 22 | /* 23 | * Meta transaction structure. 24 | * No point of including value field here as if user is doing value transfer then he has the funds to pay for gas 25 | * He should call the desired function directly in that case. 26 | */ 27 | struct MetaTransaction { 28 | uint256 nonce; 29 | address from; 30 | bytes functionSignature; 31 | } 32 | 33 | function executeMetaTransaction( 34 | address userAddress, 35 | bytes memory functionSignature, 36 | bytes32 sigR, 37 | bytes32 sigS, 38 | uint8 sigV 39 | ) public payable returns (bytes memory) { 40 | MetaTransaction memory metaTx = MetaTransaction({ 41 | nonce: nonces[userAddress], 42 | from: userAddress, 43 | functionSignature: functionSignature 44 | }); 45 | 46 | require( 47 | verify(userAddress, metaTx, sigR, sigS, sigV), 48 | "Signer and signature do not match" 49 | ); 50 | 51 | // increase nonce for user (to avoid re-use) 52 | nonces[userAddress] = nonces[userAddress].add(1); 53 | 54 | emit MetaTransactionExecuted( 55 | userAddress, 56 | payable(msg.sender), 57 | functionSignature 58 | ); 59 | 60 | // Append userAddress and relayer address at the end to extract it from calling context 61 | (bool success, bytes memory returnData) = address(this).call( 62 | abi.encodePacked(functionSignature, userAddress) 63 | ); 64 | require(success, "Function call not successful"); 65 | 66 | return returnData; 67 | } 68 | 69 | function hashMetaTransaction(MetaTransaction memory metaTx) 70 | internal 71 | pure 72 | returns (bytes32) 73 | { 74 | return 75 | keccak256( 76 | abi.encode( 77 | META_TRANSACTION_TYPEHASH, 78 | metaTx.nonce, 79 | metaTx.from, 80 | keccak256(metaTx.functionSignature) 81 | ) 82 | ); 83 | } 84 | 85 | function getNonce(address user) public view returns (uint256 nonce) { 86 | nonce = nonces[user]; 87 | } 88 | 89 | function verify( 90 | address signer, 91 | MetaTransaction memory metaTx, 92 | bytes32 sigR, 93 | bytes32 sigS, 94 | uint8 sigV 95 | ) internal view returns (bool) { 96 | require(signer != address(0), "NativeMetaTransaction: INVALID_SIGNER"); 97 | return 98 | signer == 99 | ecrecover( 100 | toTypedMessageHash(hashMetaTransaction(metaTx)), 101 | sigV, 102 | sigR, 103 | sigS 104 | ); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Highwind 2 | 3 | `Highwind` is a tool that allows you to deploy NFT smart contracts through a Graphic User Interface (GUI). With Highwind you can easily create and mint items that can be sold on [OpenSea](https://opensea.io/). 4 | 5 | Leveraging Streamlit, Docker and web3 this tool is run on your local machine and works straight out of the box without any advanced configruation. 6 | 7 | ![Deploy contract](images/deploy_contract.png) 8 | 9 | ## Requirements 10 | 11 | * docker (https://docs.docker.com/get-docker/) 12 | 13 | ### 3rd Party APIs 14 | 15 | In order to streamline deployments, Highwind leverages these 3rd party APIs. You must register and get an API key from these services to use Highwind 16 | 17 | #### Infura 18 | 19 | [Infura](https://infura.io/) is a blockchain development suite that provides easy access to Ethereum networks. 20 | 21 | #### Pinata 22 | 23 | [Pinata](https://pinata.cloud/) provides an API to easily interact with [IPFS](https://ipfs.io/) 24 | 25 | #### Metamask 26 | 27 | [MetaMask](https://metamask.io/) is a software cryptocurrency wallet used to interact with the Ethereum blockchain. 28 | 29 | ## QuickStart 30 | 31 | Here is how you can easily get started using Highwind 32 | 33 | ```bash 34 | git clone https://github.com/banjtheman/highwind.git 35 | cd highwind 36 | ./build_docker.sh 37 | ./run_docker.sh 38 | # view website on localhost:8501 39 | ``` 40 | 41 | ## Deploy 42 | 43 | With Highwind you can deploy Smart Contracts easily just by entering in your wallet info and infura key. You will need to have some ETH/MATIC in your wallet depending on the network. The faucet links are displayed in the UI. 44 | 45 | ![Deploy contract](images/deploy_contract.png) 46 | 47 | ## Create Item 48 | 49 | With Highwind, you can easily create item metadata that is uploaded to [IPFS](https://ipfs.io/) powered by [Pinata](https://pinata.cloud/). Simply enter in the infromation and press the Pin button 50 | 51 | ![Enter Item](images/enter_item.png) 52 | 53 | ![Create Item](images/create_item.png) 54 | 55 | ## Mint Item 56 | 57 | Once you have created an item, you can now mint it on the blockchain. Enter the address you would like to send the NFT to, and press the mint button. 58 | 59 | ![Mint item](images/mint_item.png) 60 | 61 | ## View on OpenSea 62 | 63 | Once items have been minted, you can view your collection and items on OpenSea. 64 | 65 | ![View on OpenSea](images/opensea_item.png) 66 | 67 | ## Scripts 68 | 69 | You can also run a standalone scripts to interact with NFTs 70 | 71 | ### Mint NFT 72 | 73 | This script allows you to mint and NFT with a token metadata URL from a contract and send the NFT to an address. 74 | 75 | You need to set some environment variables, and pass in arguments to get the script to work. Here is an end-to-end example 76 | ``` 77 | ./build_docker.sh 78 | ./run_docker.sh 79 | docker exec -it DOCKER_HASH 80 | cd scripts/ 81 | export PUBLIC_KEY=PUBLIC_KEY 82 | export PRIVATE_KEY=PRIVATE_KEY 83 | export INFURA_KEY=INFURA_KEY 84 | export NETWORK="mumbai" 85 | python mint_nft.py --contract_address CONTRACT_ADDRESS --abi_path ABI_PATH --to_address TO_ADDRESS --token_metadata_url 86 | ``` 87 | 88 | Example running the script 89 | ``` 90 | python mint_nft.py --contract_address "0x6DF98FB9cDfDa02F7d0A53c544520A3a2f9E6eC8" --abi_path "../build/contracts_WitcherCoin/WitcherCoin.json" --to_address "0xd714c8126D36b286d88c4F5Dc7f7f361b92acF11" --token_metadata_url "ipfs://QmSjxUGeqbUSFL5KWwV1x9aGw7QfygmjDiqM2MLhJdr9kP" 91 | ``` 92 | 93 | Example output 94 | ``` 95 | INFO: Starting mint 96 | INFO: checking if connected to infura...True 97 | INFO: mint txn hash: 0xdf4cd44c67a81444493115f61a4213071d391ca48f8e8c5dcdb5456ccbc7bf17 98 | INFO: Got tokenid: 4 99 | INFO: Scan url for token 4: https://explorer-mumbai.maticvigil.com/tx/0xdf4cd44c67a81444493115f61a4213071d391ca48f8e8c5dcdb5456ccbc7bf17 100 | ``` 101 | 102 | 103 | If you want to mint a bunch of NFTs can easily add in looping logic but be weary of gas fees. 104 | -------------------------------------------------------------------------------- /truffle.js: -------------------------------------------------------------------------------- 1 | const HDWalletProvider = require('@truffle/hdwallet-provider'); 2 | 3 | const MNEMONIC = process.env.MNEMONIC; 4 | const INFURA_KEY = process.env.INFURA_KEY; 5 | const needsNodeAPI = 6 | process.env.npm_config_argv && 7 | (process.env.npm_config_argv.includes("rinkeby") || 8 | process.env.npm_config_argv.includes("live")); 9 | 10 | if ((!MNEMONIC || !INFURA_KEY) && needsNodeAPI) { 11 | console.error("Please set a mnemonic and INFURA_KEY."); 12 | process.exit(0); 13 | } 14 | 15 | /** 16 | * Use this file to configure your truffle project. It's seeded with some 17 | * common settings for different networks and features like migrations, 18 | * compilation and testing. Uncomment the ones you need or modify 19 | * them to suit your project as necessary. 20 | * 21 | * More information about configuration can be found at: 22 | * 23 | * trufflesuite.com/docs/advanced/configuration 24 | * 25 | * To deploy via Infura you'll need a wallet provider (like @truffle/hdwallet-provider) 26 | * to sign your transactions before they're sent to a remote public node. Infura accounts 27 | * are available for free at: infura.io/register. 28 | * 29 | * You'll also need a mnemonic - the twelve word phrase the wallet uses to generate 30 | * public/private key pairs. If you're publishing your code to GitHub make sure you load this 31 | * phrase from a file you've .gitignored so it doesn't accidentally become public. 32 | * 33 | */ 34 | 35 | module.exports = { 36 | /** 37 | * Networks define how you connect to your ethereum client and let you set the 38 | * defaults web3 uses to send transactions. If you don't specify one truffle 39 | * will spin up a development blockchain for you on port 9545 when you 40 | * run `develop` or `test`. You can ask a truffle command to use a specific 41 | * network from the command line, e.g 42 | * 43 | * $ truffle test --network 44 | */ 45 | // contracts_build_directory: './frontend/src/contracts', 46 | contracts_build_directory: process.env.CONTRACTS_BUILD, 47 | contracts_directory: process.env.CONTRACTS_DIR, 48 | 49 | networks: { 50 | rinkeby: { 51 | provider: () => new HDWalletProvider( 52 | MNEMONIC, 53 | "https://rinkeby.infura.io/v3/" + INFURA_KEY, 54 | 0, 55 | 1 56 | ), 57 | network_id: 4, //rinkeby 58 | skipDryRun: false 59 | }, 60 | mumbai: { 61 | provider: () => new HDWalletProvider({ 62 | mnemonic: { 63 | phrase: MNEMONIC 64 | }, 65 | providerOrUrl: 66 | "https://polygon-mumbai.infura.io/v3/" + INFURA_KEY 67 | }), 68 | network_id: 80001, 69 | confirmations: 2, 70 | timeoutBlocks: 200, 71 | skipDryRun: true 72 | }, 73 | polygon: { 74 | provider: () => new HDWalletProvider({ 75 | mnemonic: { 76 | phrase: MNEMONIC 77 | }, 78 | providerOrUrl: 79 | "https://polygon-mainnet.infura.io/v3/" + INFURA_KEY 80 | }), 81 | network_id: 137, 82 | confirmations: 2, 83 | timeoutBlocks: 200, 84 | skipDryRun: true, 85 | chainId: 137 86 | }, 87 | ethereum: { 88 | provider: () => new HDWalletProvider( 89 | MNEMONIC, 90 | "https://mainnet.infura.io/v3/" + INFURA_KEY, 91 | 0, 92 | 1 93 | ), 94 | network_id: 1, //mainnet 95 | skipDryRun: false, 96 | gas: 5000000, // might have to play with 97 | gasPrice: 5000000000, // might have to play with 98 | }, 99 | }, 100 | 101 | // Set default mocha options here, use special reporters etc. 102 | mocha: { 103 | // timeout: 100000 104 | }, 105 | 106 | // Configure your compilers 107 | compilers: { 108 | solc: { 109 | version: "0.8.0", // Fetch exact version from solc-bin (default: truffle's version) 110 | // docker: true, // Use "0.5.1" you've installed locally with docker (default: false) 111 | // settings: { // See the solidity docs for advice about optimization and evmVersion 112 | // optimizer: { 113 | // enabled: false, 114 | // runs: 200 115 | // }, 116 | // evmVersion: "byzantium" 117 | // } 118 | } 119 | } 120 | }; -------------------------------------------------------------------------------- /contracts_temp/ERC721Tradable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "openzeppelin-solidity/contracts/token/ERC721/ERC721.sol"; 6 | import "openzeppelin-solidity/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; 7 | import "openzeppelin-solidity/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; 8 | import "openzeppelin-solidity/contracts/access/Ownable.sol"; 9 | import "openzeppelin-solidity/contracts/utils/math/SafeMath.sol"; 10 | import "openzeppelin-solidity/contracts/utils/Strings.sol"; 11 | 12 | import "./common/meta-transactions/ContentMixin.sol"; 13 | import "./common/meta-transactions/NativeMetaTransaction.sol"; 14 | 15 | contract OwnableDelegateProxy {} 16 | 17 | contract ProxyRegistry { 18 | mapping(address => OwnableDelegateProxy) public proxies; 19 | } 20 | 21 | /** 22 | * @title ERC721Tradable 23 | * ERC721Tradable - ERC721 contract that whitelists a trading address, and has minting functionality. 24 | */ 25 | abstract contract ERC721Tradable is 26 | ContextMixin, 27 | ERC721Enumerable, 28 | ERC721URIStorage, 29 | NativeMetaTransaction, 30 | Ownable 31 | { 32 | using SafeMath for uint256; 33 | 34 | address proxyRegistryAddress; 35 | uint256 private _currentTokenId = 0; 36 | 37 | constructor( 38 | string memory _name, 39 | string memory _symbol, 40 | address _proxyRegistryAddress 41 | ) ERC721(_name, _symbol) { 42 | proxyRegistryAddress = _proxyRegistryAddress; 43 | _initializeEIP712(_name); 44 | } 45 | 46 | /** 47 | * @dev Mints a token to an address with a tokenURI. 48 | * @param _to address of the future owner of the token 49 | */ 50 | function mintTo(address _to) public onlyOwner { 51 | uint256 newTokenId = _getNextTokenId(); 52 | _mint(_to, newTokenId); 53 | _incrementTokenId(); 54 | } 55 | 56 | /** 57 | * @dev calculates the next token ID based on value of _currentTokenId 58 | * @return uint256 for the next token ID 59 | */ 60 | function _getNextTokenId() private view returns (uint256) { 61 | return _currentTokenId.add(1); 62 | } 63 | 64 | /** 65 | * @dev increments the value of _currentTokenId 66 | */ 67 | function _incrementTokenId() private { 68 | _currentTokenId++; 69 | } 70 | 71 | function baseTokenURI() public pure virtual returns (string memory); 72 | 73 | // function tokenURI(uint256 _tokenId) override public pure returns (string memory) { 74 | // return string(abi.encodePacked(baseTokenURI(), Strings.toString(_tokenId))); 75 | // } 76 | 77 | function _beforeTokenTransfer( 78 | address from, 79 | address to, 80 | uint256 tokenId 81 | ) internal override(ERC721, ERC721Enumerable) { 82 | super._beforeTokenTransfer(from, to, tokenId); 83 | } 84 | 85 | function _burn(uint256 tokenId) 86 | internal 87 | override(ERC721, ERC721URIStorage) 88 | { 89 | super._burn(tokenId); 90 | } 91 | 92 | function tokenURI(uint256 tokenId) 93 | public 94 | view 95 | override(ERC721, ERC721URIStorage) 96 | returns (string memory) 97 | { 98 | return super.tokenURI(tokenId); 99 | } 100 | 101 | function supportsInterface(bytes4 interfaceId) 102 | public 103 | view 104 | override(ERC721, ERC721Enumerable) 105 | returns (bool) 106 | { 107 | return super.supportsInterface(interfaceId); 108 | } 109 | 110 | /** 111 | * Override isApprovedForAll to whitelist user's OpenSea proxy accounts to enable gas-less listings. 112 | */ 113 | function isApprovedForAll(address owner, address operator) 114 | public 115 | view 116 | override 117 | returns (bool) 118 | { 119 | // Whitelist OpenSea proxy contract for easy trading. 120 | ProxyRegistry proxyRegistry = ProxyRegistry(proxyRegistryAddress); 121 | if (address(proxyRegistry.proxies(owner)) == operator) { 122 | return true; 123 | } 124 | 125 | return super.isApprovedForAll(owner, operator); 126 | } 127 | 128 | /** 129 | * This is used instead of msg.sender as transactions won't be sent by the original token owner, but by OpenSea. 130 | */ 131 | function _msgSender() internal view override returns (address sender) { 132 | return ContextMixin.msgSender(); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /modules/mint_nft.py: -------------------------------------------------------------------------------- 1 | # Python imports 2 | import os 3 | import json 4 | import logging 5 | from typing import Type, Union, Dict, Any, List 6 | 7 | from web3 import Web3 8 | 9 | 10 | def load_json(path_to_json: str) -> Dict[str, Any]: 11 | """ 12 | Purpose: 13 | Load json files 14 | Args: 15 | path_to_json (String): Path to json file 16 | Returns: 17 | Conf: JSON file if loaded, else None 18 | """ 19 | try: 20 | with open(path_to_json, "r") as config_file: 21 | conf = json.load(config_file) 22 | return conf 23 | 24 | except Exception as error: 25 | logging.error(error) 26 | raise TypeError("Invalid JSON file") 27 | 28 | 29 | def set_up_blockchain( 30 | contract: str, 31 | abi_path: str, 32 | public_key: str, 33 | private_key: str, 34 | infura_key: str, 35 | network: str, 36 | ): 37 | """ 38 | Purpose: 39 | Setup all blockchain items 40 | Args: 41 | contract: contract address 42 | abi_path: abi path 43 | public_key: public key, 44 | private_key: private key, 45 | infura_key: infura key, 46 | network: network, 47 | Returns: 48 | Conf: JSON file with eth details 49 | """ 50 | ############ Ethereum Setup ############ 51 | 52 | PUBLIC_KEY = public_key 53 | PRIVATE_KEY = Web3.toBytes(hexstr=private_key) 54 | INFURA_KEY = infura_key 55 | 56 | network = network 57 | ABI = None 58 | CODE_NFT = None 59 | CHAIN_ID = None 60 | w3 = None 61 | open_sea_url = "" 62 | scan_url = "" 63 | 64 | eth_json = {} 65 | 66 | if network == "rinkeby": 67 | 68 | RINK_API_URL = f"https://rinkeby.infura.io/v3/{INFURA_KEY}" 69 | 70 | w3 = Web3(Web3.HTTPProvider(RINK_API_URL)) 71 | ABI = load_json(abi_path)["abi"] # get the ABI 72 | CODE_NFT = w3.eth.contract(address=contract, abi=ABI) # The contract 73 | CHAIN_ID = 4 74 | 75 | open_sea_url = f"https://testnets.opensea.io/assets/{contract}/" 76 | scan_url = "https://rinkeby.etherscan.io/tx/" 77 | 78 | elif network == "mumbai": 79 | MUMBAI_API_URL = f"https://polygon-mumbai.infura.io/v3/{INFURA_KEY}" 80 | 81 | w3 = Web3(Web3.HTTPProvider(MUMBAI_API_URL)) 82 | ABI = load_json(abi_path)["abi"] # get the ABI 83 | CODE_NFT = w3.eth.contract(address=contract, abi=ABI) # The contract 84 | CHAIN_ID = 80001 85 | 86 | open_sea_url = f"https://testnets.opensea.io/assets/{contract}/" 87 | scan_url = "https://explorer-mumbai.maticvigil.com/tx/" 88 | 89 | elif network == "polygon": 90 | POLYGON_API_URL = f"https://polygon-mainnet.infura.io/v3/{INFURA_KEY}" 91 | 92 | w3 = Web3(Web3.HTTPProvider(POLYGON_API_URL)) 93 | ABI = load_json(abi_path)["abi"] # get the ABI 94 | CODE_NFT = w3.eth.contract(address=contract, abi=ABI) # The contract 95 | CHAIN_ID = 137 96 | 97 | open_sea_url = f"https://opensea.io/assets/matic/{contract}/" 98 | scan_url = "https://polygonscan.com/tx/" 99 | 100 | if network == "ethereum": 101 | 102 | ETH_API_URL = f"https://mainnet.infura.io/v3/{INFURA_KEY}" 103 | 104 | w3 = Web3(Web3.HTTPProvider(ETH_API_URL)) 105 | ABI = load_json(abi_path)["abi"] # get the ABI 106 | CODE_NFT = w3.eth.contract(address=contract, abi=ABI) # The contract 107 | CHAIN_ID = 1 108 | 109 | open_sea_url = f"https://opensea.io/assets/{contract}/" 110 | scan_url = "https://etherscan.io/tx/" 111 | 112 | else: 113 | logging.error("Invalid network") 114 | raise ValueError(f"Invalid {network}") 115 | 116 | logging.info(f"checking if connected to infura...{w3.isConnected()}") 117 | 118 | eth_json["w3"] = w3 119 | eth_json["contract"] = CODE_NFT 120 | eth_json["chain_id"] = CHAIN_ID 121 | eth_json["open_sea_url"] = open_sea_url 122 | eth_json["scan_url"] = scan_url 123 | eth_json["public_key"] = PUBLIC_KEY 124 | eth_json["private_key"] = PRIVATE_KEY 125 | 126 | return eth_json 127 | 128 | 129 | def web3_mint(userAddress: str, tokenURI: str, eth_json: Dict[str, Any]) -> str: 130 | """ 131 | Purpose: 132 | mint a token for user on blockchain 133 | Args: 134 | userAddress - the user to mint for 135 | tokenURI - uri for token 136 | eth_json - blockchain info 137 | Returns: 138 | hash - txn of mint 139 | tokenid - token minted 140 | """ 141 | 142 | PUBLIC_KEY = eth_json["public_key"] 143 | CHAIN_ID = eth_json["chain_id"] 144 | w3 = eth_json["w3"] 145 | CODE_NFT = eth_json["contract"] 146 | PRIVATE_KEY = eth_json["private_key"] 147 | 148 | nonce = w3.eth.get_transaction_count(PUBLIC_KEY) 149 | # logging.info(f"Nonce: {w3.eth.get_transaction_count(PUBLIC_KEY)}") 150 | 151 | # Create the contracrt 152 | mint_txn = CODE_NFT.functions.mint(userAddress, tokenURI).buildTransaction( 153 | { 154 | "chainId": CHAIN_ID, 155 | "gas": 10000000, 156 | "gasPrice": w3.toWei("1", "gwei"), 157 | "nonce": nonce, 158 | } 159 | ) 160 | 161 | signed_txn = w3.eth.account.sign_transaction(mint_txn, private_key=PRIVATE_KEY) 162 | 163 | w3.eth.send_raw_transaction(signed_txn.rawTransaction) 164 | hash = w3.toHex(w3.keccak(signed_txn.rawTransaction)) 165 | 166 | logging.info(f"mint txn hash: {hash} ") 167 | 168 | receipt = w3.eth.wait_for_transaction_receipt(hash) # hmmm have to wait... 169 | 170 | hex_tokenid = receipt["logs"][0]["topics"][3].hex() # this is token id in hex 171 | 172 | # convert from hex to decmial 173 | tokenid = int(hex_tokenid, 16) 174 | logging.info(f"Got tokenid: {tokenid}") 175 | 176 | return hash 177 | -------------------------------------------------------------------------------- /scripts/mint_nft.py: -------------------------------------------------------------------------------- 1 | # Python imports 2 | import argparse 3 | import json 4 | import logging 5 | import os 6 | from typing import Dict, Any 7 | 8 | from web3 import Web3 9 | 10 | 11 | def load_json(path_to_json: str) -> Dict[str, Any]: 12 | """ 13 | Purpose: 14 | Load json files 15 | Args: 16 | path_to_json (String): Path to json file 17 | Returns: 18 | Conf: JSON file if loaded, else None 19 | """ 20 | try: 21 | with open(path_to_json, "r") as config_file: 22 | conf = json.load(config_file) 23 | return conf 24 | 25 | except Exception as error: 26 | logging.error(error) 27 | raise TypeError("Invalid JSON file") 28 | 29 | 30 | def set_up_blockchain(contract: str, abi_path: str): 31 | """ 32 | Purpose: 33 | Setup all blockchain items 34 | Args: 35 | path_to_json (String): Path to json file 36 | Returns: 37 | Conf: JSON file if loaded, else None 38 | """ 39 | ############ Ethereum Setup ############ 40 | 41 | PUBLIC_KEY = os.environ["PUBLIC_KEY"] 42 | PRIVATE_KEY = Web3.toBytes(hexstr=os.environ["PRIVATE_KEY"]) 43 | INFURA_KEY = os.environ["INFURA_KEY"] 44 | 45 | network = os.environ["NETWORK"] 46 | ABI = None 47 | CODE_NFT = None 48 | CHAIN_ID = None 49 | w3 = None 50 | open_sea_url = "" 51 | scan_url = "" 52 | 53 | eth_json = {} 54 | 55 | if network == "rinkeby": 56 | 57 | RINK_API_URL = f"https://rinkeby.infura.io/v3/{INFURA_KEY}" 58 | 59 | w3 = Web3(Web3.HTTPProvider(RINK_API_URL)) 60 | ABI = load_json(abi_path)["abi"] # get the ABI 61 | CODE_NFT = w3.eth.contract(address=contract, abi=ABI) # The contract 62 | CHAIN_ID = 4 63 | 64 | open_sea_url = f"https://testnets.opensea.io/assets/{contract}/" 65 | scan_url = "https://rinkeby.etherscan.io/tx/" 66 | 67 | elif network == "mumbai": 68 | MUMBAI_API_URL = f"https://polygon-mumbai.infura.io/v3/{INFURA_KEY}" 69 | 70 | w3 = Web3(Web3.HTTPProvider(MUMBAI_API_URL)) 71 | ABI = load_json(abi_path)["abi"] # get the ABI 72 | CODE_NFT = w3.eth.contract(address=contract, abi=ABI) # The contract 73 | CHAIN_ID = 80001 74 | 75 | open_sea_url = f"https://testnets.opensea.io/assets/{contract}/" 76 | scan_url = "https://explorer-mumbai.maticvigil.com/tx/" 77 | 78 | elif network == "matic_main": 79 | POLYGON_API_URL = f"https://polygon-mainnet.infura.io/v3/{INFURA_KEY}" 80 | 81 | w3 = Web3(Web3.HTTPProvider(POLYGON_API_URL)) 82 | ABI = load_json(abi_path)["abi"] # get the ABI 83 | CODE_NFT = w3.eth.contract(address=contract, abi=ABI) # The contract 84 | CHAIN_ID = 137 85 | 86 | open_sea_url = f"https://opensea.io/assets/matic/{contract}/" 87 | scan_url = "https://polygonscan.com/tx/" 88 | 89 | else: 90 | logging.error("Invalid network") 91 | raise ValueError(f"Invalid {network}") 92 | 93 | logging.info(f"checking if connected to infura...{w3.isConnected()}") 94 | 95 | eth_json["w3"] = w3 96 | eth_json["contract"] = CODE_NFT 97 | eth_json["chain_id"] = CHAIN_ID 98 | eth_json["open_sea_url"] = open_sea_url 99 | eth_json["scan_url"] = scan_url 100 | eth_json["public_key"] = PUBLIC_KEY 101 | eth_json["private_key"] = PRIVATE_KEY 102 | 103 | return eth_json 104 | 105 | 106 | def web3_mint(userAddress: str, tokenURI: str, eth_json: Dict[str, Any]) -> str: 107 | """ 108 | Purpose: 109 | mint a token for user on blockchain 110 | Args: 111 | userAddress - the user to mint for 112 | tokenURI - metadat info for NFT 113 | eth_json - blockchain info 114 | Returns: 115 | hash - txn of mint 116 | tokenid - token minted 117 | """ 118 | 119 | PUBLIC_KEY = eth_json["public_key"] 120 | CHAIN_ID = eth_json["chain_id"] 121 | w3 = eth_json["w3"] 122 | CODE_NFT = eth_json["contract"] 123 | PRIVATE_KEY = eth_json["private_key"] 124 | 125 | nonce = w3.eth.get_transaction_count(PUBLIC_KEY) 126 | 127 | # Create the contracrt 128 | mint_txn = CODE_NFT.functions.mint(userAddress, tokenURI).buildTransaction( 129 | { 130 | "chainId": CHAIN_ID, 131 | "gas": 10000000, 132 | "gasPrice": w3.toWei("1", "gwei"), 133 | "nonce": nonce, 134 | } 135 | ) 136 | 137 | signed_txn = w3.eth.account.sign_transaction(mint_txn, private_key=PRIVATE_KEY) 138 | 139 | w3.eth.send_raw_transaction(signed_txn.rawTransaction) 140 | hash = w3.toHex(w3.keccak(signed_txn.rawTransaction)) 141 | 142 | logging.info(f"mint txn hash: {hash} ") 143 | 144 | receipt = w3.eth.wait_for_transaction_receipt(hash) # hmmm have to wait... 145 | 146 | hex_tokenid = receipt["logs"][0]["topics"][3].hex() # this is token id in hex 147 | 148 | # convert from hex to decmial 149 | tokenid = int(hex_tokenid, 16) 150 | logging.info(f"Got tokenid: {tokenid}") 151 | 152 | return hash, tokenid 153 | 154 | 155 | def main(): 156 | logging.info("Starting mint") 157 | 158 | parser = argparse.ArgumentParser(description="Mint NFTs") 159 | parser.add_argument( 160 | "--contract_address", 161 | type=str, 162 | help="contract_address for smart contract", 163 | required=True, 164 | ) 165 | parser.add_argument( 166 | "--abi_path", 167 | type=str, 168 | help="abi_path for NFT contract, example: ../build/contracts/contracts_NFTNAME/NFTNANE.json", 169 | required=True, 170 | ) 171 | 172 | parser.add_argument( 173 | "--to_address", type=str, help="address to send token", required=True 174 | ) 175 | 176 | parser.add_argument( 177 | "--token_metadata_url", type=str, help="link to token_metadata", required=True 178 | ) 179 | 180 | args = parser.parse_args() 181 | # Setup blockchain basics 182 | eth_json = set_up_blockchain(args.contract_address, args.abi_path) 183 | # Mint token 184 | txn_hash, tokenid = web3_mint(args.to_address, args.token_metadata_url, eth_json) 185 | logging.info(f"Scan url for token {tokenid}: {eth_json['scan_url']}{txn_hash} ") 186 | 187 | 188 | if __name__ == "__main__": 189 | loglevel = logging.INFO 190 | logging.basicConfig(format="%(levelname)s: %(message)s", level=loglevel) 191 | main() 192 | -------------------------------------------------------------------------------- /highwind_st.py: -------------------------------------------------------------------------------- 1 | """ 2 | Purpose: 3 | Start Highwind UI 4 | """ 5 | 6 | # Python imports 7 | import os 8 | import streamlit as st 9 | from typing import Type, Union, Dict, Any, List 10 | import glob 11 | from pathlib import Path 12 | 13 | 14 | from modules import utils, mint_nft, pinata_api 15 | import streamlit.components.v1 as components 16 | 17 | 18 | ### 19 | # Streamlit Main Functionality 20 | ### 21 | 22 | 23 | def get_items() -> List: 24 | """ 25 | Purpose: 26 | Load list of items 27 | Args: 28 | N/A 29 | Returns: 30 | items 31 | """ 32 | items = {} 33 | 34 | json_files = glob.glob("highwind_jsons/items/*.json") 35 | 36 | for json_file in json_files: 37 | 38 | json_obj = utils.load_json(json_file) 39 | key = Path(json_file).stem 40 | item_name = json_obj["item"]["name"] 41 | items[f"{item_name}_{key}"] = json_obj 42 | 43 | return items 44 | 45 | 46 | def load_contracts() -> List: 47 | """ 48 | Purpose: 49 | Load list of contracts 50 | Args: 51 | N/A 52 | Returns: 53 | contrats 54 | """ 55 | contracts = {} 56 | 57 | json_files = glob.glob("highwind_jsons/contracts/*.json") 58 | 59 | for json_file in json_files: 60 | 61 | json_obj = utils.load_json(json_file) 62 | key = Path(json_file).stem 63 | contracts[key] = json_obj 64 | 65 | return contracts 66 | 67 | 68 | def opensea() -> None: 69 | """ 70 | Purpose: 71 | Shows the opensea page for your contract 72 | Args: 73 | N/A 74 | Returns: 75 | N/A 76 | """ 77 | 78 | st.subheader("OpenSea") 79 | 80 | contracts = load_contracts() 81 | contracts_list = list(contracts.keys()) 82 | 83 | st.subheader("Select Contract") 84 | contract_name = st.selectbox("Contract", contracts_list) 85 | 86 | # if no contracts stop 87 | if len(contracts_list) == 0: 88 | st.warning("Deploy a contract first") 89 | st.stop() 90 | 91 | token_name = contracts[contract_name]["token_name"].lower() 92 | contract = contracts[contract_name]["contract_address"] 93 | network = contracts[contract_name]["network"] 94 | 95 | if network == "mumbai" or network == "rinkeby": 96 | opensea_url = f"https://testnets.opensea.io/collection/{token_name}/" 97 | else: 98 | opensea_url = f"https://opensea.io/collection/{token_name}/" 99 | 100 | st.write(f"Contract address: {contract}") 101 | st.write(f"OpenSea URL: {opensea_url}") 102 | 103 | components.iframe( 104 | f"https://testnets.opensea.io/collection/{token_name}/", 105 | height=1000, 106 | width=1000, 107 | scrolling=True, 108 | ) 109 | 110 | 111 | def create_item(): 112 | """ 113 | Purpose: 114 | Shows the create item screen 115 | Args: 116 | N/A 117 | Returns: 118 | N/A 119 | """ 120 | st.subheader("Create new item") 121 | 122 | item_name = st.text_input("Item name", "", help="Name of the item.") 123 | ext_url = st.text_input( 124 | "External URL", 125 | "", 126 | help="This is the URL that will appear below the asset's image on OpenSea and will allow users to leave OpenSea and view the item on your site.", 127 | ) 128 | item_desc = st.text_input( 129 | "Description", 130 | "", 131 | help="A human readable description of the item. Markdown is supported.", 132 | ) 133 | image_url = st.text_input( 134 | "Image Url", 135 | "", 136 | help="This is the URL to the image of the item. Can be just about any type of image (including SVGs, which will be cached into PNGs by OpenSea, and even MP4s), and can be IPFS URLs or paths. We recommend using a 350 x 350 image.", 137 | ) 138 | 139 | item_color = st.color_picker( 140 | "background_color", 141 | "#ffffff", 142 | help="Background color of the item on OpenSea. Must be a six-character hexadecimal without a pre-pended #.", 143 | ) 144 | 145 | animation_url = st.text_input( 146 | "animation_url", 147 | "", 148 | help="A URL to a multi-media attachment for the item. The file extensions GLTF, GLB, WEBM, MP4, M4V, OGV, and OGG are supported, along with the audio-only extensions MP3, WAV, and OGA.Animation_url also supports HTML pages, allowing you to build rich experiences and interactive NFTs using JavaScript canvas, WebGL, and more. Scripts and relative paths within the HTML page are now supported. However, access to browser extensions is not supported.", 149 | ) 150 | 151 | youtube_url = st.text_input( 152 | "youtube_url", 153 | "", 154 | help="A URL to a YouTube video.", 155 | ) 156 | 157 | # This is where session state will shine 158 | if "attrs" not in st.session_state: 159 | st.session_state.attrs = 1 160 | 161 | if st.button("Add attribute"): 162 | st.session_state.attrs += 1 163 | 164 | if st.button("Remove attribute"): 165 | st.session_state.attrs -= 1 166 | 167 | attr_types = ["Text", "Number", "Date"] 168 | attr_list = [] 169 | 170 | for index in range(st.session_state.attrs): 171 | 172 | st.subheader(f"Attribute {index}") 173 | 174 | attr_json = {} 175 | attr_type = st.selectbox( 176 | "Attribute type", attr_types, key=f"attr_types_index_{index}" 177 | ) 178 | 179 | if attr_type == "Text": 180 | 181 | trait_type = st.text_input("trait type", "", key=f"trait_index_{index}") 182 | value = st.text_input("Value", "", key=f"value_index_{index}") 183 | 184 | attr_json["trait_type"] = trait_type 185 | attr_json["value"] = value 186 | 187 | if attr_type == "Number": 188 | 189 | display_types = ["number", "boost_number", "boost_percentage", "ranking"] 190 | display_type = st.selectbox( 191 | "dislay type", display_types, key=f"display_index_{index}" 192 | ) 193 | 194 | trait_type = st.text_input("trait type", "", key=f"trait_index_{index}") 195 | value = st.text_input("Value", "", key=f"value_index_{index}") 196 | 197 | if not display_type == "ranking": 198 | attr_json["display_type"] = display_type 199 | attr_json["trait_type"] = trait_type 200 | attr_json["value"] = value 201 | 202 | if attr_type == "Date": 203 | 204 | trait_type = st.text_input("trait type", "", key=f"trait_index_{index}") 205 | 206 | st.write("Pass in a unix timestamp for the value") 207 | value = st.text_input("Value", "", key=f"value_index_{index}") 208 | 209 | attr_json["display_type"] = "date" 210 | attr_json["trait_type"] = trait_type 211 | attr_json["value"] = value 212 | 213 | attr_list.append(attr_json) 214 | 215 | item_json = {} 216 | item_json["name"] = item_name 217 | item_json["image"] = image_url 218 | item_json["external_url"] = ext_url 219 | item_json["description"] = item_desc 220 | item_json["background_color"] = item_color.replace("#", "") 221 | item_json["animation_url"] = animation_url 222 | item_json["youtube_url"] = youtube_url 223 | # attrs = st.session_state.attrs 224 | item_json["attributes"] = attr_list 225 | 226 | return item_json 227 | 228 | 229 | def mint() -> None: 230 | """ 231 | Purpose: 232 | Shows the mint Page 233 | Args: 234 | N/A 235 | Returns: 236 | N/A 237 | """ 238 | 239 | st.subheader("Mint items") 240 | 241 | contracts = load_contracts() 242 | contracts_list = list(contracts.keys()) 243 | 244 | st.subheader("Select Contract for minting") 245 | contract_name = st.selectbox("Contract", contracts_list) 246 | 247 | # if no contracts stop 248 | if len(contracts_list) == 0: 249 | st.warning("Deploy a contract first") 250 | st.stop() 251 | 252 | abi_path = contracts[contract_name]["abi_path"] 253 | network = contracts[contract_name]["network"] 254 | contract = contracts[contract_name]["contract_address"] 255 | 256 | # Facuet info 257 | if network == "mumbai": 258 | st.subheader("Faucet") 259 | st.write("The Faucet allows you to get free matic on test networks") 260 | st.write("https://faucet.matic.network/") 261 | 262 | elif network == "rinkeby": 263 | st.subheader("Faucet") 264 | st.write("The Faucet allows you to get free eth on test networks") 265 | st.write("https://faucet.rinkeby.io/") 266 | 267 | elif network == "polygon": 268 | st.info("This is the Polygon Mainnet using REAL FUNDS") 269 | 270 | elif network == "ethereum": 271 | st.info("This is the Ethereum Mainnet using REAL FUNDS") 272 | 273 | st.subheader("NFT Info") 274 | st.write( 275 | f'{contracts[contract_name]["token_name"]} - {contracts[contract_name]["token_symbol"]} ' 276 | ) 277 | st.write(f"Contract address: {contract}") 278 | 279 | st.subheader("API Keys") 280 | st.write("Enter in your infura.io API KEY") 281 | st.write("https://infura.io/dashboard/ethereum") 282 | 283 | infura_key = st.text_input("infura_key", "", type="password") 284 | 285 | st.subheader("Wallet Info") 286 | public_key = st.text_input("Public Key", "") 287 | private_key = st.text_input("Private key", "", type="password") 288 | 289 | new_item = st.checkbox("Create new item?") 290 | 291 | ### Display item info 292 | if new_item: 293 | 294 | item_json = create_item() 295 | 296 | st.subheader("Current Metadata") 297 | st.write(item_json) 298 | 299 | st.write("Enter in your pinata.cloud API keys") 300 | st.write("https://pinata.cloud/keys") 301 | 302 | pinata_key = st.text_input("Pinata Key", "", type="password") 303 | pinata_secret_key = st.text_input("Pinata Secret key", "", type="password") 304 | 305 | if st.button("Pin Metadata to IPFS?"): 306 | hash_info = pinata_api.pinJSONToIPFS( 307 | item_json, pinata_key, pinata_secret_key 308 | ) 309 | 310 | ipfs_hash = hash_info["IpfsHash"] 311 | item_final_json = {} 312 | item_final_json["hash_info"] = hash_info 313 | item_final_json["item"] = item_json 314 | item_final_json["url"] = f"https://gateway.pinata.cloud/ipfs/{ipfs_hash}" 315 | item_final_json["ipfs_url"] = f"ipfs://{ipfs_hash}" 316 | 317 | utils.save_json(f"highwind_jsons/items/{ipfs_hash}.json", item_final_json) 318 | 319 | st.write(item_final_json["url"]) 320 | 321 | else: 322 | items = get_items() 323 | item_list = list(items.keys()) 324 | item_list_name = st.selectbox("Items", item_list) 325 | 326 | if item_list_name: 327 | 328 | item_obj = items[item_list_name] 329 | 330 | token_uri = item_obj["ipfs_url"] 331 | item_json = item_obj["item"] 332 | 333 | st.write("Item metadata") 334 | st.write(item_json) 335 | 336 | token_address = st.text_input("Address to send token", "") 337 | 338 | if st.button(f"Mint token"): 339 | with st.spinner("Minting..."): 340 | eth_json = mint_nft.set_up_blockchain( 341 | contract, 342 | abi_path, 343 | public_key, 344 | private_key, 345 | infura_key, 346 | network, 347 | ) 348 | 349 | txn_hash = mint_nft.web3_mint(token_address, token_uri, eth_json) 350 | 351 | if network == "mumbai": 352 | scan_url = "https://mumbai.polygonscan.com/tx/" 353 | elif network == "rinkeby": 354 | scan_url = "https://rinkeby.etherscan.io/tx/" 355 | elif network == "polygon": 356 | scan_url = "https://polygonscan.com/tx/" 357 | elif network == "ethereum": 358 | scan_url = "https://etherscan.io/tx/" 359 | 360 | st.success(f"txn hash: {txn_hash}") 361 | st.write(f"{scan_url}{txn_hash}") 362 | st.balloons() 363 | 364 | 365 | def deploy() -> None: 366 | """ 367 | Purpose: 368 | Shows the deploy Page 369 | Args: 370 | N/A 371 | Returns: 372 | N/A 373 | """ 374 | 375 | st.subheader("Deploy Contracts") 376 | 377 | # Deploy Contracts 378 | networks = ["mumbai", "rinkeby", "polygon", "ethereum"] 379 | st.subheader("Network") 380 | st.write("Select the network to deploy your smart contract") 381 | network = st.selectbox("Network", networks) 382 | 383 | # Facuet info 384 | 385 | if network == "mumbai": 386 | st.subheader("Faucet") 387 | st.write("The Faucet allows you to get free matic on test networks") 388 | st.write("https://faucet.matic.network/") 389 | 390 | elif network == "rinkeby": 391 | st.subheader("Faucet") 392 | st.write("The Faucet allows you to get free eth on test networks") 393 | st.write("https://faucet.rinkeby.io/") 394 | 395 | elif network == "polygon": 396 | st.info("This is the Polygon Mainnet using REAL FUNDS") 397 | 398 | elif network == "ethereum": 399 | st.info("This is the Ethereum Mainnet using REAL FUNDS") 400 | 401 | st.subheader("NFT Name") 402 | 403 | st.write("Enter in the name and Symbol for your NFT") 404 | nft_name = st.text_input("NFT Name", "MyNFT") 405 | nft_symbol = st.text_input("NFT Symbol", "MyNFT") 406 | 407 | st.subheader("API Keys") 408 | st.write("Enter in your infura.io API KEY") 409 | st.write("https://infura.io/dashboard/ethereum") 410 | 411 | infura_key = st.text_input("infura_key", "", type="password") 412 | 413 | st.subheader("Wallet Info") 414 | public_key = st.text_input("Public Key", "") 415 | mnemonic = st.text_input("mnemonic", "", type="password") 416 | 417 | if st.button(f"Deploy {nft_name} Smart Contract"): 418 | with st.spinner("Deploying..."): 419 | 420 | # TODO do we want to check for invalid chars 421 | nft_name = nft_name.replace(" ", "_") 422 | 423 | # Do a find and replace in the for the Contrant Name 424 | contracts_dir = f"contracts_{nft_name}" 425 | contracts_build = f"./build/contracts_{nft_name}" 426 | 427 | # Replace contract code 428 | copy_command = f"cp -r contracts_temp {contracts_dir}" 429 | os.system(copy_command) 430 | file_data = utils.read_from_file(f"{contracts_dir}/MyNFT.sol") 431 | file_data = file_data.replace("REPLACE_NAME", f"{nft_name}") 432 | file_data = file_data.replace("REPLACE_SYM", f"{nft_symbol}") 433 | utils.write_to_file(f"{contracts_dir}/MyNFT.sol", file_data) 434 | 435 | # Replace migrations file 436 | copy_command = f"cp migrations_temp/2_deploy_contracts.js migrations/2_deploy_contracts.js" 437 | os.system(copy_command) 438 | con_data = utils.read_from_file(f"migrations/2_deploy_contracts.js") 439 | con_data = con_data.replace("REPLACE_NAME", f"{nft_name}") 440 | utils.write_to_file(f"migrations/2_deploy_contracts.js", con_data) 441 | 442 | # Get all env vars 443 | export_statements = f'INFURA_KEY={infura_key} OWNER_ADDRESS={public_key} MNEMONIC="{mnemonic}" CONTRACTS_DIR={contracts_dir} CONTRACTS_BUILD={contracts_build}' 444 | 445 | cmd = f"{export_statements} truffle migrate --reset --network {network}" 446 | output = os.popen(cmd).read() 447 | 448 | with st.beta_expander("Log output"): 449 | st.write(output) 450 | 451 | try: 452 | cas = output.split("contract address: ") 453 | contract_address = cas[2].split(" ")[0].strip() 454 | except: 455 | if network == "mumbai" or network == "polygon": 456 | token = "MATIC" 457 | else: 458 | token = "ETH" 459 | st.warning(f"Do you have enough {token} in wallet {public_key}?") 460 | st.error("Check log output for error details") 461 | st.stop() 462 | 463 | if network == "mumbai": 464 | scan_url = "https://mumbai.polygonscan.com/address/" 465 | elif network == "rinkeby": 466 | scan_url = "https://rinkeby.etherscan.io/address/" 467 | 468 | elif network == "polygon": 469 | scan_url = "https://polygonscan.com/address/" 470 | elif network == "ethereum": 471 | scan_url = "https://etherscan.io/address/" 472 | 473 | st.success(f"Contract Address: {contract_address}") 474 | st.write(f"{scan_url}{contract_address}") 475 | 476 | # Contract metadata 477 | contract_json = {} 478 | contract_json["token_name"] = nft_name 479 | contract_json["token_symbol"] = nft_symbol 480 | contract_json["contract_address"] = contract_address 481 | contract_json["network"] = network 482 | contract_json["scan_url"] = f"{scan_url}{contract_address}" 483 | contract_json["abi_path"] = f"{contracts_build}/{nft_name}.json" 484 | 485 | # do we want to check for duplicates? 486 | 487 | file_name = f"highwind_jsons/contracts/{nft_name}.json" 488 | if os.path.exists(file_name): 489 | # loop until we find a free file name 490 | counter = 1 491 | while True: 492 | file_name = f"highwind_jsons/contracts/{nft_name}_{counter}.json" 493 | 494 | if os.path.exists(file_name): 495 | counter += 1 496 | else: 497 | break 498 | 499 | utils.save_json(file_name, contract_json) 500 | st.subheader("Deployed") 501 | st.balloons() 502 | 503 | 504 | def sidebar() -> None: 505 | """ 506 | Purpose: 507 | Shows the side bar 508 | Args: 509 | N/A 510 | Returns: 511 | N/A 512 | """ 513 | 514 | st.sidebar.title("Highwind") 515 | 516 | # Create the Navigation Section 517 | st.sidebar.header("Navigation") 518 | pages = ["Deploy", "Mint", "OpenSea"] 519 | default_page = 0 520 | page = st.sidebar.selectbox("Go To", options=pages, index=default_page) 521 | 522 | if page == "Deploy": 523 | deploy() 524 | elif page == "Mint": 525 | mint() 526 | elif page == "OpenSea": 527 | opensea() 528 | else: 529 | st.error("Invalid Page") 530 | 531 | 532 | def app() -> None: 533 | """ 534 | Purpose: 535 | Controls the app flow 536 | Args: 537 | N/A 538 | Returns: 539 | N/A 540 | """ 541 | 542 | # Spin up the sidebar, will control which page is loaded in the 543 | # main app 544 | sidebar() 545 | 546 | 547 | def main() -> None: 548 | """ 549 | Purpose: 550 | Controls the flow of the streamlit app 551 | Args: 552 | N/A 553 | Returns: 554 | N/A 555 | """ 556 | 557 | # Start the streamlit app 558 | st.title("Highwind") 559 | st.write("Highwind allows you to manage NFT contracts") 560 | app() 561 | 562 | 563 | if __name__ == "__main__": 564 | main() 565 | --------------------------------------------------------------------------------