From e2d0180837f2af4fa0bd6687d77902e93d690c90 Mon Sep 17 00:00:00 2001 From: Devin Finzer Date: Tue, 6 Nov 2018 11:07:48 -0500 Subject: [PATCH] Rename to OpenSea creatures --- .gitignore | 1 + README.md | 4 +- contracts/Creature.sol | 16 +++ contracts/CreatureFactory.sol | 132 ++++++++++++++++++ ...ptoPuffLootBox.sol => CreatureLootBox.sol} | 22 +-- contracts/CryptoPuff.sol | 16 --- contracts/CryptoPuffFactory.sol | 89 ------------ contracts/Factory.sol | 12 +- flatten.sh | 3 + metadata-api/app.py | 8 +- migrations/2_deploy_contracts.js | 14 +- scripts/mint.js | 8 +- 12 files changed, 192 insertions(+), 133 deletions(-) create mode 100644 contracts/Creature.sol create mode 100644 contracts/CreatureFactory.sol rename contracts/{CryptoPuffLootBox.sol => CreatureLootBox.sol} (55%) delete mode 100644 contracts/CryptoPuff.sol delete mode 100644 contracts/CryptoPuffFactory.sol create mode 100755 flatten.sh diff --git a/.gitignore b/.gitignore index 4d02f51..c7ea448 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ build/ +flattened/ node_modules/ .env diff --git a/README.md b/README.md index ffe2927..1d23262 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -## CryptoPuffs ERC721 contracts +## OpenSeaCreatures ERC721 contracts -### About CryptoPuffs. +### About OpenSeaCreatures. This is a very simple sample ERC721 for the purposes of demonstrating integration with the [OpenSea](https://opensea.io) marketplace. We include a script for minting the items. diff --git a/contracts/Creature.sol b/contracts/Creature.sol new file mode 100644 index 0000000..7341101 --- /dev/null +++ b/contracts/Creature.sol @@ -0,0 +1,16 @@ +pragma solidity ^0.4.24; + +import "./TradeableERC721Token.sol"; +import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; + +/** + * @title Creature + * Creature - a contract for my non-fungible creatures. + */ +contract Creature is TradeableERC721Token { + constructor(address _proxyRegistryAddress) TradeableERC721Token("Creature", "OSC", _proxyRegistryAddress) public { } + + function baseTokenURI() public view returns (string) { + return "https://opensea-creatures-api.herokuapp.com/api/creature/"; + } +} \ No newline at end of file diff --git a/contracts/CreatureFactory.sol b/contracts/CreatureFactory.sol new file mode 100644 index 0000000..c6fa373 --- /dev/null +++ b/contracts/CreatureFactory.sol @@ -0,0 +1,132 @@ +pragma solidity ^0.4.24; + +import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; +import "./Factory.sol"; +import "./Creature.sol"; +import "./CreatureLootBox.sol"; +import "./Strings.sol"; + +contract CreatureFactory is Factory, Ownable { + using Strings for string; + + address public proxyRegistryAddress; + address public nftAddress; + address public lootBoxNftAddress; + string public baseURI = "https://opensea-creature-api.herokuapp.com/api/factory/"; + + /** + * Enforce the existence of only 100 OpenSea creatures. + */ + uint256 PUFF_SUPPLY = 100; + + /** + * Three different options for minting Creatures (basic, premium, and gold). + */ + uint256 NUM_OPTIONS = 3; + uint256 SINGLE_PUFF_OPTION = 0; + uint256 MULTIPLE_PUFF_OPTION = 1; + uint256 LOOTBOX_OPTION = 2; + uint256 NUM_PUFFS_IN_MULTIPLE_PUFF_OPTION = 4; + + constructor(address _proxyRegistryAddress, address _nftAddress) public { + proxyRegistryAddress = _proxyRegistryAddress; + nftAddress = _nftAddress; + lootBoxNftAddress = new CreatureLootBox(_proxyRegistryAddress, this); + } + + function name() external view returns (string) { + return "OpenSeaCreature Item Sale"; + } + + function symbol() external view returns (string) { + return "CPF"; + } + + function supportsFactoryInterface() public view returns (bool) { + return true; + } + + function numOptions() public view returns (uint256) { + return NUM_OPTIONS; + } + + function mint(uint256 _optionId, address _toAddress) public { + require(msg.sender == owner || msg.sender == lootBoxNftAddress); + + // Must be sent from the owner proxy or owner. + ProxyRegistry proxyRegistry = ProxyRegistry(proxyRegistryAddress); + assert(proxyRegistry.proxies(owner) == msg.sender || owner == msg.sender); + require(canMint(_optionId)); + + Creature openSeaCreature = Creature(nftAddress); + if (_optionId == SINGLE_PUFF_OPTION) { + openSeaCreature.mintTo(_toAddress); + } else if (_optionId == MULTIPLE_PUFF_OPTION) { + for (uint256 i = 0; i < NUM_PUFFS_IN_MULTIPLE_PUFF_OPTION; i++) { + openSeaCreature.mintTo(_toAddress); + } + } else if (_optionId == LOOTBOX_OPTION) { + CreatureLootBox openSeaCreatureLootBox = CreatureLootBox(lootBoxNftAddress); + openSeaCreatureLootBox.mintTo(_toAddress); + } + } + + function canMint(uint256 _optionId) public view returns (bool) { + if (_optionId >= NUM_OPTIONS) { + return false; + } + + Creature openSeaCreature = Creature(nftAddress); + uint256 puffSupply = openSeaCreature.totalSupply(); + + uint256 numItemsAllocated = 0; + if (_optionId == SINGLE_PUFF_OPTION) { + numItemsAllocated = 1; + } else if (_optionId == MULTIPLE_PUFF_OPTION) { + numItemsAllocated = NUM_PUFFS_IN_MULTIPLE_PUFF_OPTION; + } else if (_optionId == LOOTBOX_OPTION) { + CreatureLootBox openSeaCreatureLootBox = CreatureLootBox(lootBoxNftAddress); + numItemsAllocated = openSeaCreatureLootBox.itemsPerLootbox(); + } + return puffSupply < (PUFF_SUPPLY - numItemsAllocated); + } + + function tokenURI(uint256 _optionId) public view returns (string) { + return Strings.strConcat( + baseURI, + Strings.uint2str(_optionId) + ); + } + + /** + * Hack to get things to work automatically on OpenSea. + * Use transferFrom so the frontend doesn't have to worry about different method names. + */ + function transferFrom(address _from, address _to, uint256 _tokenId) public { + mint(_tokenId, _to); + } + + /** + * Hack to get things to work automatically on OpenSea. + * Use isApprovedForAll so the frontend doesn't have to worry about different method names. + */ + function isApprovedForAll( + address _owner, + address _operator + ) + public + view + returns (bool) + { + if (owner == _owner && _owner == _operator) { + return true; + } + + ProxyRegistry proxyRegistry = ProxyRegistry(proxyRegistryAddress); + if (owner == _owner && proxyRegistry.proxies(_owner) == _operator) { + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/contracts/CryptoPuffLootBox.sol b/contracts/CreatureLootBox.sol similarity index 55% rename from contracts/CryptoPuffLootBox.sol rename to contracts/CreatureLootBox.sol index 8940aab..c04b47d 100644 --- a/contracts/CryptoPuffLootBox.sol +++ b/contracts/CreatureLootBox.sol @@ -1,21 +1,21 @@ pragma solidity ^0.4.21; import "./TradeableERC721Token.sol"; -import "./CryptoPuff.sol"; +import "./Creature.sol"; import "./Factory.sol"; -import 'openzeppelin-solidity/contracts/ownership/Ownable.sol'; +import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; /** - * @title CryptoPuffLootBox + * @title CreatureLootBox * - * CryptoPuffLootBox - a tradeable loot box of CryptoPuffs. + * CreatureLootBox - a tradeable loot box of Creatures. */ -contract CryptoPuffLootBox is TradeableERC721Token { - uint256 NUM_PUFFS_PER_BOX = 3; +contract CreatureLootBox is TradeableERC721Token { + uint256 NUM_CREATURES_PER_BOX = 3; uint256 OPTION_ID = 0; address factoryAddress; - constructor(address _proxyRegistryAddress, address _factoryAddress) TradeableERC721Token("CryptoPuffLootBox", "PUFFBOX", _proxyRegistryAddress) public { + constructor(address _proxyRegistryAddress, address _factoryAddress) TradeableERC721Token("CreatureLootBox", "PUFFBOX", _proxyRegistryAddress) public { factoryAddress = _factoryAddress; } @@ -23,7 +23,7 @@ contract CryptoPuffLootBox is TradeableERC721Token { require(ownerOf(_tokenId) == msg.sender); // Insert custom logic for configuring the item here. - for (uint256 i = 0; i < NUM_PUFFS_PER_BOX; i++) { + for (uint256 i = 0; i < NUM_CREATURES_PER_BOX; i++) { // Mint the ERC721 item(s). Factory factory = Factory(factoryAddress); factory.mint(OPTION_ID, msg.sender); @@ -34,6 +34,10 @@ contract CryptoPuffLootBox is TradeableERC721Token { } function baseTokenURI() public view returns (string) { - return "https://cryptopuffs-api.herokuapp.com/api/box/"; + return "https://opensea-creature-api.herokuapp.com/api/box/"; + } + + function itemsPerLootbox() public view returns (uint256) { + return NUM_CREATURES_PER_BOX; } } \ No newline at end of file diff --git a/contracts/CryptoPuff.sol b/contracts/CryptoPuff.sol deleted file mode 100644 index 9310d22..0000000 --- a/contracts/CryptoPuff.sol +++ /dev/null @@ -1,16 +0,0 @@ -pragma solidity ^0.4.24; - -import './TradeableERC721Token.sol'; -import 'openzeppelin-solidity/contracts/ownership/Ownable.sol'; - -/** - * @title CryptoPuff - * CryptoPuff - a contract for my non-fungible crypto puffs. - */ -contract CryptoPuff is TradeableERC721Token { - constructor(address _proxyRegistryAddress) TradeableERC721Token("CryptoPuff", "PUFF", _proxyRegistryAddress) public { } - - function baseTokenURI() public view returns (string) { - return "https://cryptopuffs-api.herokuapp.com/api/puff/"; - } -} \ No newline at end of file diff --git a/contracts/CryptoPuffFactory.sol b/contracts/CryptoPuffFactory.sol deleted file mode 100644 index adf7a35..0000000 --- a/contracts/CryptoPuffFactory.sol +++ /dev/null @@ -1,89 +0,0 @@ -pragma solidity ^0.4.24; - -import 'openzeppelin-solidity/contracts/ownership/Ownable.sol'; -import './Factory.sol'; -import './CryptoPuff.sol'; -import './CryptoPuffLootBox.sol'; -import './Strings.sol'; - -contract CryptoPuffFactory is Factory, Ownable { - using Strings for string; - - address proxyRegistryAddress; - address nftAddress; - address lootBoxNftAddress; - string public baseURI = "https://cryptopuffs-api.herokuapp.com/api/factory/"; - - /** - * Enforce the existence of only 100 cryptopuffs (not including those created through lootboxes). - */ - uint256 PUFF_SUPPLY = 100; - - /** - * Lootbox supply. - */ - uint256 LOOTBOX_SUPPLY = 5; - - /** - * Three different options for minting CryptoPuffs (basic, premium, and gold). - */ - uint256 NUM_OPTIONS = 3; - uint256 LOOTBOX_OPTION = 2; - - constructor(address _proxyRegistryAddress, address _nftAddress) public { - proxyRegistryAddress = _proxyRegistryAddress; - nftAddress = _nftAddress; - lootBoxNftAddress = new CryptoPuffLootBox(_proxyRegistryAddress, this); - } - - function supportsFactoryInterface() public view returns (bool) { - return true; - } - - function numOptions() public view returns (uint256) { - return NUM_OPTIONS; - } - - function mint(uint256 _optionId, address _toAddress) external { - require(msg.sender == owner || msg.sender == lootBoxNftAddress); - - // Must be sent from the owner proxy or owner. - ProxyRegistry proxyRegistry = ProxyRegistry(proxyRegistryAddress); - assert(proxyRegistry.proxies(owner) == msg.sender || owner == msg.sender); - require(canMint(_optionId)); - - // Option 2 is a lootbox. - if (_optionId == LOOTBOX_OPTION) { - CryptoPuffLootBox cryptoPuffLootBox = CryptoPuffLootBox(lootBoxNftAddress); - cryptoPuffLootBox.mintTo(_toAddress); - } else { - CryptoPuff cryptoPuff = CryptoPuff(nftAddress); - cryptoPuff.mintTo(_toAddress); - } - } - - function canMint(uint256 _optionId) public view returns (bool) { - if (_optionId >= NUM_OPTIONS) { - return false; - } - - // Case: lootbox option. - uint256 currentSupply = 0; - if (_optionId == LOOTBOX_OPTION) { - CryptoPuffLootBox cryptoPuffLootBox = CryptoPuffLootBox(lootBoxNftAddress); - currentSupply = cryptoPuffLootBox.totalSupply(); - return currentSupply < (LOOTBOX_SUPPLY - 1); - } - - CryptoPuff cryptoPuff = CryptoPuff(nftAddress); - currentSupply = cryptoPuff.totalSupply(); - return currentSupply < (PUFF_SUPPLY - 1); - } - - function tokenURI(uint256 _optionId) public view returns (string) { - return Strings.strConcat( - baseURI, - Strings.uint2str(_optionId) - ); - } -} \ No newline at end of file diff --git a/contracts/Factory.sol b/contracts/Factory.sol index 90e510b..cf52ccd 100644 --- a/contracts/Factory.sol +++ b/contracts/Factory.sol @@ -1,13 +1,21 @@ pragma solidity ^0.4.24; -import 'openzeppelin-solidity/contracts/ownership/Ownable.sol'; - /** * This is a generic factory contract that can be used to mint tokens. The configuration * for minting is specified by an _optionId, which can be used to delineate various * ways of minting. */ interface Factory { + /** + * Returns the name of this factory. + */ + function name() external view returns (string); + + /** + * Returns the symbol for this factory. + */ + function symbol() external view returns (string); + /** * Number of options the factory supports. */ diff --git a/flatten.sh b/flatten.sh new file mode 100755 index 0000000..6035cdd --- /dev/null +++ b/flatten.sh @@ -0,0 +1,3 @@ +./node_modules/.bin/truffle-flattener contracts/Creature.sol > flattened/Creature.sol +./node_modules/.bin/truffle-flattener contracts/CreatureFactory.sol > flattened/CreatureFactory.sol +./node_modules/.bin/truffle-flattener contracts/CreatureLootBox.sol > flattened/CreatureLootBox.sol diff --git a/metadata-api/app.py b/metadata-api/app.py index 789b01e..00b8c2e 100644 --- a/metadata-api/app.py +++ b/metadata-api/app.py @@ -68,8 +68,8 @@ def box(token_id): _add_attribute(attributes, 'number_inside', [3], token_id) return jsonify({ - 'name': "CryptoPuff Loot Box", - 'description': "This lootbox contains some cryptopuffs! It can also be traded!", + 'name': "Creature Loot Box", + 'description': "This lootbox contains some OpenSea Creatures! It can also be traded!", 'imageUrl': image_url, 'externalUrl': 'https://cryptopuff.io/%s' % token_id, 'attributes': attributes @@ -85,8 +85,8 @@ def factory(token_id): _add_attribute(attributes, 'number_inside', [1], token_id) return jsonify({ - 'name': "CryptoPuff Sale", - 'description': "Buy a magical CryptoPuff of random variety!", + 'name': "Creature Sale", + 'description': "Buy a magical Creature of random variety!", 'imageUrl': image_url, 'externalUrl': 'https://cryptopuff.io/%s' % token_id, 'attributes': attributes diff --git a/migrations/2_deploy_contracts.js b/migrations/2_deploy_contracts.js index c53b3ed..31f29a7 100644 --- a/migrations/2_deploy_contracts.js +++ b/migrations/2_deploy_contracts.js @@ -1,6 +1,6 @@ -const CryptoPuff = artifacts.require("./CryptoPuff.sol"); -const CryptoPuffFactory = artifacts.require("./CryptoPuffFactory.sol") -const CryptoPuffLootBox = artifacts.require("./CryptoPuffLootBox.sol"); +const OpenSeaCreature = artifacts.require("./OpenSeaCreature.sol"); +const OpenSeaCreatureFactory = artifacts.require("./OpenSeaCreatureFactory.sol") +const OpenSeaCreatureLootBox = artifacts.require("./OpenSeaCreatureLootBox.sol"); module.exports = function(deployer, network) { // OpenSea proxy registry addresses for rinkeby and mainnet. @@ -10,10 +10,10 @@ module.exports = function(deployer, network) { } else { proxyRegistryAddress = "0xa5409ec958c83c3f309868babaca7c86dcb077c1"; } - deployer.deploy(CryptoPuff, proxyRegistryAddress, {gas: 5000000}).then(() => { - return deployer.deploy(CryptoPuffFactory, proxyRegistryAddress, CryptoPuff.address, {gas: 7000000}); + deployer.deploy(OpenSeaCreature, proxyRegistryAddress, {gas: 5000000}).then(() => { + return deployer.deploy(OpenSeaCreatureFactory, proxyRegistryAddress, OpenSeaCreature.address, {gas: 7000000}); }).then(async() => { - var cryptoPuff = await CryptoPuff.deployed(); - return cryptoPuff.transferOwnership(CryptoPuffFactory.address); + var openSeaCreature = await OpenSeaCreature.deployed(); + return openSeaCreature.transferOwnership(OpenSeaCreatureFactory.address); }) }; \ No newline at end of file diff --git a/scripts/mint.js b/scripts/mint.js index f516c8b..b40c07b 100644 --- a/scripts/mint.js +++ b/scripts/mint.js @@ -5,7 +5,7 @@ const INFURA_KEY = process.env.INFURA_KEY const FACTORY_CONTRACT_ADDRESS = process.env.FACTORY_CONTRACT_ADDRESS const OWNER_ADDRESS = process.env.OWNER_ADDRESS const NETWORK = process.env.NETWORK -const NUM_PUFFS = 12 +const NUM_CREATURES = 12 const NUM_LOOTBOXES = 4 const DEFAULT_OPTION_ID = 0 const LOOTBOX_OPTION_ID = 2 @@ -41,14 +41,14 @@ async function main() { const factoryContract = new web3Instance.eth.Contract(ABI, FACTORY_CONTRACT_ADDRESS, { gasLimit: "1000000" }) - // Puffs issued directly to the owner. - for (var i = 0; i < NUM_PUFFS; i++) { + // Creatures issued directly to the owner. + for (var i = 0; i < NUM_CREATURES; i++) { const result = await factoryContract.methods.mint(DEFAULT_OPTION_ID, OWNER_ADDRESS).send({ from: OWNER_ADDRESS }); console.log(result.transactionHash) } // Lootboxes issued directly to the owner. - for (var i = 0; i < NUM_PUFFS; i++) { + for (var i = 0; i < NUM_LOOTBOXES; i++) { const result = await factoryContract.methods.mint(LOOTBOX_OPTION_ID, OWNER_ADDRESS).send({ from: OWNER_ADDRESS }); console.log(result.transactionHash) }