diff --git a/contracts/RockburgNFT.sol b/contracts/RockburgNFT.sol index e21f5c9..1151bad 100644 --- a/contracts/RockburgNFT.sol +++ b/contracts/RockburgNFT.sol @@ -3,190 +3,304 @@ pragma solidity ^0.8.0; // RockburgNFT contract by m1guelpf.eth -import "./data/Types.sol"; -import "base64-sol/base64.sol"; -import "./interfaces/IVenueRenderer.sol"; -import "./interfaces/IStudioRenderer.sol"; -import "./interfaces/IMusicianRenderer.sol"; -import "@openzeppelin/contracts/access/Ownable.sol"; -import "@openzeppelin/contracts/utils/Counters.sol"; -import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; - -contract RockburgNFT is Ownable, ERC721, ERC721Enumerable { - using Counters for Counters.Counter; - Counters.Counter private _tokenIds; - Counters.Counter private randomnessNonce; - - address internal _musicianRenderer; - address internal _venueRenderer; - address internal _studioRenderer; - - mapping(uint256 => Types) private _tokenTypes; - mapping(uint256 => Musician) private _musicians; - mapping(uint256 => Venue) private _venues; - mapping(uint256 => Studio) private _studios; - mapping(uint256 => Band) private _bands; - mapping(uint256 => Song) private _songs; - - constructor(address musicianRenderer, address venueRenderer, address studioRenderer) ERC721("Rockburg", "RCKBRG") { - _musicianRenderer = musicianRenderer; - _venueRenderer = venueRenderer; - _studioRenderer = studioRenderer; - } - - /** - * @dev Creates a new artist token with pseudo-randomized stats and returns its identifier. - * For simplicity, we do not check wether the caller implements ERC721Receiver. - */ - function mintArtist(string calldata name, string calldata role) public returns (uint256) { - _tokenIds.increment(); - - uint256 tokenId = _tokenIds.current(); - - _tokenTypes[tokenId] = Types.MUSICIAN; - Musician storage musician = _musicians[tokenId]; - musician.name = name; - musician.role = role; - musician.skillPoints = randNum(abi.encodePacked(name, role)) % 100; - musician.egoPoints = randNum(abi.encodePacked(name, role)) % 100; - musician.lookPoints = randNum(abi.encodePacked(name, role)) % 100; - musician.creativePoints = randNum(abi.encodePacked(name, role)) % 100; - - _mint(_msgSender(), tokenId); - - return tokenId; - } - - /** - * @dev Creates a new venue token with pseudo-randomized stats and returns its identifier. - * For simplicity, we do not check wether the caller implements ERC721Receiver. - */ - function mintVenue(string calldata name, string calldata location) public returns (uint256) { - _tokenIds.increment(); - - uint256 tokenId = _tokenIds.current(); - - _tokenTypes[tokenId] = Types.VENUE; - Venue storage venue = _venues[tokenId]; - venue.name = name; - venue.location = location; - venue.visitorCap = randNum(abi.encodePacked(name, location)) % 100; - venue.dollarCost = randNum(abi.encodePacked(name, location)) % 100; - venue.cleanlinessPoints = randNum(abi.encodePacked(name, location)) % 100; - venue.reputationPoints = randNum(abi.encodePacked(name, location)) % 100; - - _mint(_msgSender(), tokenId); - - return tokenId; - } - - /** - * @dev Creates a new studio token with pseudo-randomized stats and returns its identifier. - * For simplicity, we do not check wether the caller implements ERC721Receiver. - */ - function mintStudio(string calldata name, string calldata location) public returns (uint256) { - _tokenIds.increment(); - - uint256 tokenId = _tokenIds.current(); - - _tokenTypes[tokenId] = Types.STUDIO; - Studio storage studio = _studios[tokenId]; - studio.name = name; - studio.location = location; - studio.dollarCost = randNum(abi.encodePacked(name, location)) % 50; - studio.leadTime = randNum(abi.encodePacked(name, location)) % 12; - studio.reputationPoints = randNum(abi.encodePacked(name, location)) % 100; - - _mint(_msgSender(), tokenId); - - return tokenId; - } - - function getType(uint256 tokenId) public view virtual returns (Types) { - return _tokenTypes[tokenId]; - } - - function getMusician(uint256 tokenId) public view virtual returns (Musician memory) { - require(_exists(tokenId), "token does not exist"); - require(_tokenTypes[tokenId] == Types.MUSICIAN, "token is not a musician"); - - return _musicians[tokenId]; - } - - function getVenue(uint256 tokenId) public view virtual returns (Venue memory) { - require(_exists(tokenId), "token does not exist"); - require(_tokenTypes[tokenId] == Types.VENUE, "token is not a venue"); - - return _venues[tokenId]; - } - - function getStudio(uint256 tokenId) public view virtual returns (Studio memory) { - require(_exists(tokenId), "token does not exist"); - require(_tokenTypes[tokenId] == Types.STUDIO, "token is not a studio"); - - return _studios[tokenId]; - } - - function getBand(uint256 tokenId) public view virtual returns (Band memory) { - require(_exists(tokenId), "token does not exist"); - require(_tokenTypes[tokenId] == Types.BAND, "token is not a studio"); - - return _bands[tokenId]; - } - - function getSong(uint256 tokenId) public view virtual returns (Song memory) { - require(_exists(tokenId), "token does not exist"); - require(_tokenTypes[tokenId] == Types.SONG, "token is not a song"); - - return _songs[tokenId]; - } - - /** - * @dev Returns the Uniform Resource Identifier (URI) for `tokenId` token. - */ - function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { - require(_exists(tokenId), "token does not exist"); - - Types tokenType = _tokenTypes[tokenId]; - - if (tokenType == Types.MUSICIAN) { - return IMusicianRenderer(_musicianRenderer).constructTokenURI(_musicians[tokenId], tokenId); - } - if (tokenType == Types.VENUE) { - return IVenueRenderer(_venueRenderer).constructTokenURI(_venues[tokenId], tokenId); - } - if (tokenType == Types.STUDIO) { - return IStudioRenderer(_studioRenderer).constructTokenURI(_studios[tokenId], tokenId); - } - - return "TBD"; - } - - /** - * @dev Returns a pseudo-random `uint256`, based off a provided seed, the sender, block number, block difficulty, and an internal nonce. - */ - function randNum(bytes memory seed) internal returns (uint256) { - randomnessNonce.increment(); - - return uint256(keccak256(abi.encodePacked(seed, msg.sender, block.number, block.difficulty, randomnessNonce.current()))); - } - - /** - * @dev Hook that is called before any token transfer. This includes minting and burning. - */ - function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal virtual override(ERC721, ERC721Enumerable) { - super._beforeTokenTransfer(from, to, tokenId); - } - - /** - * @dev Returns true if this contract implements the interface defined by `interfaceId`. See the corresponding - * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] - * to learn more about how these ids are created. - * - * This function call must use less than 30 000 gas. - */ - function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721, ERC721Enumerable) returns (bool) { - return super.supportsInterface(interfaceId); - } +import './data/Types.sol'; +import 'base64-sol/base64.sol'; +import './interfaces/IVenueRenderer.sol'; +import './interfaces/IStudioRenderer.sol'; +import './interfaces/IMusicianRenderer.sol'; +import '@openzeppelin/contracts/access/Ownable.sol'; +import '@openzeppelin/contracts/utils/Counters.sol'; +import '@openzeppelin/contracts/token/ERC721/ERC721.sol'; +import '@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol'; +import '@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol'; + +import 'hardhat/console.sol'; + +contract RockburgNFT is Ownable, ERC721, ERC721Enumerable, ERC721Holder { + using Counters for Counters.Counter; + Counters.Counter private _tokenIds; + Counters.Counter private randomnessNonce; + + address internal _musicianRenderer; + address internal _venueRenderer; + address internal _studioRenderer; + + mapping(uint256 => Types) private _tokenTypes; + mapping(uint256 => Musician) private _musicians; + mapping(uint256 => Venue) private _venues; + mapping(uint256 => Studio) private _studios; + mapping(uint256 => Band) private _bands; + mapping(uint256 => Song) private _songs; + + constructor( + address musicianRenderer, + address venueRenderer, + address studioRenderer + ) ERC721('Rockburg', 'RCKBRG') { + _musicianRenderer = musicianRenderer; + _venueRenderer = venueRenderer; + _studioRenderer = studioRenderer; + } + + /** + * @dev Creates a new band token with pseudo-randomized stats and returns its identifier. + * For simplicity, we do not check wether the caller implements ERC721Receiver. + */ + function mintBand(string calldata name, string calldata bandType) internal returns (uint256) { + _tokenIds.increment(); + + uint256 tokenId = _tokenIds.current(); + + _tokenTypes[tokenId] = Types.BAND; + Band storage band = _bands[tokenId]; + + band.name = name; + band.bandType = bandType; + band.fanCount = randNum(abi.encodePacked(name, bandType)) % 100; + band.buzzPoints = randNum(abi.encodePacked(name, bandType)) % 100; + + _mint(_msgSender(), tokenId); + + return tokenId; + } + + /** + * @dev Creates a new artist token with pseudo-randomized stats and returns its identifier. + * For simplicity, we do not check wether the caller implements ERC721Receiver. + */ + function mintArtist(string calldata name, Role role) public returns (uint256) { + _tokenIds.increment(); + + uint256 tokenId = _tokenIds.current(); + + _tokenTypes[tokenId] = Types.MUSICIAN; + Musician storage musician = _musicians[tokenId]; + musician.name = name; + musician.role = role; + musician.skillPoints = randNum(abi.encodePacked(name, role)) % 100; + musician.egoPoints = randNum(abi.encodePacked(name, role)) % 100; + musician.lookPoints = randNum(abi.encodePacked(name, role)) % 100; + musician.creativePoints = randNum(abi.encodePacked(name, role)) % 100; + + _mint(_msgSender(), tokenId); + + return tokenId; + } + + /** + * @dev Creates a new venue token with pseudo-randomized stats and returns its identifier. + * For simplicity, we do not check wether the caller implements ERC721Receiver. + */ + function mintVenue(string calldata name, string calldata location) public returns (uint256) { + _tokenIds.increment(); + + uint256 tokenId = _tokenIds.current(); + + _tokenTypes[tokenId] = Types.VENUE; + Venue storage venue = _venues[tokenId]; + venue.name = name; + venue.location = location; + venue.visitorCap = randNum(abi.encodePacked(name, location)) % 100; + venue.dollarCost = randNum(abi.encodePacked(name, location)) % 100; + venue.cleanlinessPoints = randNum(abi.encodePacked(name, location)) % 100; + venue.reputationPoints = randNum(abi.encodePacked(name, location)) % 100; + + _mint(_msgSender(), tokenId); + + return tokenId; + } + + /** + * @dev Creates a new studio token with pseudo-randomized stats and returns its identifier. + * For simplicity, we do not check wether the caller implements ERC721Receiver. + */ + function mintStudio(string calldata name, string calldata location) public returns (uint256) { + _tokenIds.increment(); + + uint256 tokenId = _tokenIds.current(); + + _tokenTypes[tokenId] = Types.STUDIO; + Studio storage studio = _studios[tokenId]; + studio.name = name; + studio.location = location; + studio.dollarCost = randNum(abi.encodePacked(name, location)) % 50; + studio.leadTime = randNum(abi.encodePacked(name, location)) % 12; + studio.reputationPoints = randNum(abi.encodePacked(name, location)) % 100; + + _mint(_msgSender(), tokenId); + + return tokenId; + } + + function getType(uint256 tokenId) public view virtual returns (Types) { + return _tokenTypes[tokenId]; + } + + function getMusician(uint256 tokenId) public view virtual returns (Musician memory) { + require(_exists(tokenId), 'token does not exist'); + require(_tokenTypes[tokenId] == Types.MUSICIAN, 'token is not a musician'); + + return _musicians[tokenId]; + } + + function getVenue(uint256 tokenId) public view virtual returns (Venue memory) { + require(_exists(tokenId), 'token does not exist'); + require(_tokenTypes[tokenId] == Types.VENUE, 'token is not a venue'); + + return _venues[tokenId]; + } + + function getStudio(uint256 tokenId) public view virtual returns (Studio memory) { + require(_exists(tokenId), 'token does not exist'); + require(_tokenTypes[tokenId] == Types.STUDIO, 'token is not a studio'); + + return _studios[tokenId]; + } + + function getBand(uint256 tokenId) public view virtual returns (Band memory) { + require(_exists(tokenId), 'token does not exist'); + require(_tokenTypes[tokenId] == Types.BAND, 'token is not a band'); + + return _bands[tokenId]; + } + + function getSong(uint256 tokenId) public view virtual returns (Song memory) { + require(_exists(tokenId), 'token does not exist'); + require(_tokenTypes[tokenId] == Types.SONG, 'token is not a song'); + + return _songs[tokenId]; + } + + function checkArtistRole(uint256 tokenId, Role expectedRole) internal view returns (bool) { + return _musicians[tokenId].role == expectedRole; + } + + /** + * @dev Form a band + */ + function formBand( + string calldata name, + string calldata bandType, + uint256 vocalistId, + uint256 leadGuitarId, + uint256 rhythmGuitarId, + uint256 bassId, + uint256 drumsId + ) public returns (Band memory) { + // Check the type of the token + require(_tokenTypes[vocalistId] == Types.MUSICIAN, 'vocalistId is not an artist'); + require(_tokenTypes[leadGuitarId] == Types.MUSICIAN, 'leadGuitarId is not an artist'); + require(_tokenTypes[rhythmGuitarId] == Types.MUSICIAN, 'rhythmGuitarId is not an artist'); + require(_tokenTypes[bassId] == Types.MUSICIAN, 'bassId is not an artist'); + require(_tokenTypes[drumsId] == Types.MUSICIAN, 'drumsId is not an artist'); + + require(checkArtistRole(vocalistId, Role.VOCALIST), 'vocalistId is not a Vocalist'); + require(checkArtistRole(leadGuitarId, Role.LEAD_GUITAR), 'leadGuitarId is not a Lead Guitar'); + require(checkArtistRole(rhythmGuitarId, Role.RHYTHM_GUITAR), 'rhythmGuitarId is not a Rhythm Guitar'); + require(checkArtistRole(bassId, Role.BASS), 'bassId is not a Bass'); + require(checkArtistRole(drumsId, Role.DRUMS), 'drumsId is not a Drums'); + + // transfer all those artist to the contract itself, this will also automatically + // check that the send owns the token + safeTransferFrom(msg.sender, address(this), vocalistId); + safeTransferFrom(msg.sender, address(this), leadGuitarId); + safeTransferFrom(msg.sender, address(this), rhythmGuitarId); + safeTransferFrom(msg.sender, address(this), bassId); + safeTransferFrom(msg.sender, address(this), drumsId); + + uint256 bandId = mintBand(name, bandType); + + // bind artist to band + Band storage _band = _bands[bandId]; + _band.vocalistId = vocalistId; + _band.leadGuitarId = leadGuitarId; + _band.rhythmGuitarId = rhythmGuitarId; + _band.bassId = bassId; + _band.drumsId = drumsId; + + return _band; + } + + function disbandBand(uint256 bandId) public { + require(_isApprovedOrOwner(msg.sender, bandId), "You don't own the band"); + require(_tokenTypes[bandId] == Types.BAND, 'token is not a band'); + + // bind artist to band + Band storage _band = _bands[bandId]; + + // transfer all those artist to the contract itself, this will also automatically + // check that the send owns the token + + _transfer(address(this), msg.sender, _band.vocalistId); + _transfer(address(this), msg.sender, _band.leadGuitarId); + _transfer(address(this), msg.sender, _band.rhythmGuitarId); + _transfer(address(this), msg.sender, _band.bassId); + _transfer(address(this), msg.sender, _band.drumsId); + + // Should we just reset the band and transfer it back to us or burn it? + + // Safe transfer: option 1 + // _band.vocalistId = 0; + // _band.leadGuitarId = 0; + // _band.rhythmGuitarId = 0; + // _band.bassId = 0; + // _band.drumsId = 0; + // safeTransferFrom(msg.sender, address(this), bandId); + + // Burn it: option 2 + _burn(bandId); + delete _bands[bandId]; + } + + /** + * @dev Returns the Uniform Resource Identifier (URI) for `tokenId` token. + */ + function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { + require(_exists(tokenId), 'token does not exist'); + + Types tokenType = _tokenTypes[tokenId]; + + if (tokenType == Types.MUSICIAN) { + return IMusicianRenderer(_musicianRenderer).constructTokenURI(_musicians[tokenId], tokenId); + } + if (tokenType == Types.VENUE) { + return IVenueRenderer(_venueRenderer).constructTokenURI(_venues[tokenId], tokenId); + } + if (tokenType == Types.STUDIO) { + return IStudioRenderer(_studioRenderer).constructTokenURI(_studios[tokenId], tokenId); + } + + return 'TBD'; + } + + /** + * @dev Returns a pseudo-random `uint256`, based off a provided seed, the sender, block number, block difficulty, and an internal nonce. + */ + function randNum(bytes memory seed) internal returns (uint256) { + randomnessNonce.increment(); + + return uint256(keccak256(abi.encodePacked(seed, msg.sender, block.number, block.difficulty, randomnessNonce.current()))); + } + + /** + * @dev Hook that is called before any token transfer. This includes minting and burning. + */ + function _beforeTokenTransfer( + address from, + address to, + uint256 tokenId + ) internal virtual override(ERC721, ERC721Enumerable) { + super._beforeTokenTransfer(from, to, tokenId); + } + + /** + * @dev Returns true if this contract implements the interface defined by `interfaceId`. See the corresponding + * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] + * to learn more about how these ids are created. + * + * This function call must use less than 30 000 gas. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721, ERC721Enumerable) returns (bool) { + return super.supportsInterface(interfaceId); + } } diff --git a/contracts/data/Types.sol b/contracts/data/Types.sol index dc9f5e0..0ec2f63 100644 --- a/contracts/data/Types.sol +++ b/contracts/data/Types.sol @@ -1,45 +1,64 @@ //SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -enum Types { MUSICIAN, VENUE, STUDIO, BAND, SONG } +enum Types { + MUSICIAN, + VENUE, + STUDIO, + BAND, + SONG +} + +enum Role { + VOCALIST, + LEAD_GUITAR, + RHYTHM_GUITAR, + BASS, + DRUMS +} struct Musician { - string name; - string role; - uint256 skillPoints; - uint256 egoPoints; - uint256 lookPoints; - uint256 creativePoints; + string name; + Role role; + uint256 skillPoints; + uint256 egoPoints; + uint256 lookPoints; + uint256 creativePoints; } struct Venue { - string name; - string location; - uint256 visitorCap; - uint256 dollarCost; - uint256 cleanlinessPoints; - uint256 reputationPoints; + string name; + string location; + uint256 visitorCap; + uint256 dollarCost; + uint256 cleanlinessPoints; + uint256 reputationPoints; } struct Studio { - string name; - string location; - uint256 dollarCost; - uint256 leadTime; - uint256 reputationPoints; + string name; + string location; + uint256 dollarCost; + uint256 leadTime; + uint256 reputationPoints; } struct Band { - string name; - string bandType; - uint256 fanCount; - uint256 buzzPoints; + string name; + string bandType; + uint256 fanCount; + uint256 buzzPoints; + uint256 vocalistId; + uint256 leadGuitarId; + uint256 rhythmGuitarId; + uint256 bassId; + uint256 drumsId; } struct Song { - string name; - string author; - uint256 qualityPoints; - uint256 streamCount; - uint256 ratingPoints; + string name; + string author; + uint256 qualityPoints; + uint256 streamCount; + uint256 ratingPoints; } diff --git a/contracts/renderers/MusicianRenderer.sol b/contracts/renderers/MusicianRenderer.sol index b72b1f2..aeb495d 100644 --- a/contracts/renderers/MusicianRenderer.sol +++ b/contracts/renderers/MusicianRenderer.sol @@ -7,45 +7,61 @@ import "base64-sol/base64.sol"; import "../interfaces/IMusicianRenderer.sol"; contract MusicianRenderer is IMusicianRenderer, Strings { - function constructTokenURI(Musician calldata musician, uint256 tokenId) external pure override returns (string memory) { - string[14] memory parts; - // solhint-disable-next-line quotes - parts[0] = ''; + function roleToString(Role role) public pure returns (string memory) { + if (role == Role.VOCALIST) { + return "Vocalist"; + } else if (role == Role.LEAD_GUITAR) { + return "Lead Guitar"; + } else if (role == Role.RHYTHM_GUITAR) { + return "Rhythm Guitar"; + } else if (role == Role.BASS) { + return "Bass"; + } else if (role == Role.DRUMS) { + return "Drums"; + } else { + return "TBD"; + } + } - // solhint-disable-next-line quotes - parts[1] = ''; - parts[2] = musician.name; + function constructTokenURI(Musician calldata musician, uint256 tokenId) external pure override returns (string memory) { + string[14] memory parts; + // solhint-disable-next-line quotes + parts[0] = ''; - // solhint-disable-next-line quotes - parts[3] = ''; - parts[4] = musician.role; + // solhint-disable-next-line quotes + parts[1] = ''; + parts[2] = musician.name; - // solhint-disable-next-line quotes - parts[5] = 'Skill'; - parts[6] = Strings.uintToString(musician.skillPoints); + // solhint-disable-next-line quotes + parts[3] = ''; + parts[4] = roleToString(musician.role); - // solhint-disable-next-line quotes - parts[7] = 'Ego'; - parts[8] = Strings.uintToString(musician.egoPoints); + // solhint-disable-next-line quotes + parts[5] = 'Skill'; + parts[6] = Strings.uintToString(musician.skillPoints); - // solhint-disable-next-line quotes - parts[9] = 'Looks'; - parts[10] = Strings.uintToString(musician.lookPoints); + // solhint-disable-next-line quotes + parts[7] = 'Ego'; + parts[8] = Strings.uintToString(musician.egoPoints); - // solhint-disable-next-line quotes - parts[11] = 'Creativity'; - parts[12] = Strings.uintToString(musician.creativePoints); + // solhint-disable-next-line quotes + parts[9] = 'Looks'; + parts[10] = Strings.uintToString(musician.lookPoints); - // solhint-disable-next-line quotes - parts[13] = 'Musician'; + // solhint-disable-next-line quotes + parts[11] = 'Creativity'; + parts[12] = Strings.uintToString(musician.creativePoints); - string memory output = string(abi.encodePacked(parts[0], parts[1], parts[2], parts[3], parts[4], parts[5], parts[6], parts[7], parts[8])); - output = string(abi.encodePacked(output, parts[9], parts[10], parts[11], parts[12], parts[13])); + // solhint-disable-next-line quotes + parts[13] = 'Musician'; - // solhint-disable-next-line quotes - string memory json = Base64.encode(bytes(string(abi.encodePacked('{"name": "Musician #', Strings.uintToString(tokenId), '", "description": "Rockburg is a WIP card game on the blockchain.", "image": "data:image/svg+xml;base64,', Base64.encode(bytes(output)), '"}')))); - output = string(abi.encodePacked("data:application/json;base64,", json)); + string memory output = string(abi.encodePacked(parts[0], parts[1], parts[2], parts[3], parts[4], parts[5], parts[6], parts[7], parts[8])); + output = string(abi.encodePacked(output, parts[9], parts[10], parts[11], parts[12], parts[13])); - return output; - } + // solhint-disable-next-line quotes + string memory json = Base64.encode(bytes(string(abi.encodePacked('{"name": "Musician #', Strings.uintToString(tokenId), '", "description": "Rockburg is a WIP card game on the blockchain.", "image": "data:image/svg+xml;base64,', Base64.encode(bytes(output)), '"}')))); + output = string(abi.encodePacked("data:application/json;base64,", json)); + + return output; + } } diff --git a/hardhat.config.ts b/hardhat.config.ts index e8e72ea..0247c81 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -9,7 +9,15 @@ import '@nomiclabs/hardhat-etherscan' import 'solidity-coverage' const config: HardhatUserConfig = { - solidity: '0.8.4', + solidity: { + version: '0.8.4', + settings: { + optimizer: { + enabled: true, + runs: 1000, + }, + }, + }, networks: { hardhat: { initialBaseFeePerGas: 0, // workaround from https://github.com/sc-forks/solidity-coverage/issues/652#issuecomment-896330136 . Remove when that issue is closed. diff --git a/test/band-test.ts b/test/band-test.ts new file mode 100644 index 0000000..3ee6b37 --- /dev/null +++ b/test/band-test.ts @@ -0,0 +1,216 @@ +import { Contract } from '@ethersproject/contracts' + +import { ethers, waffle } from 'hardhat' +import chai from 'chai' +const { expect } = chai +const { deployContract } = waffle + +const renderers = ['Musician', 'Venue', 'Studio'] +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import RockburgNFTArtifact from '../artifacts/contracts/RockburgNFT.sol/RockburgNFT.json' +import { RockburgNFT } from '../typechain/RockburgNFT' + +describe('Band test', function () { + let owner: SignerWithAddress + let addr1: SignerWithAddress + let addr2: SignerWithAddress + let addrs: SignerWithAddress[] + + let contract: RockburgNFT + + beforeEach(async () => { + ;[owner, addr1, addr2, ...addrs] = await ethers.getSigners() + const rendererAddresses = await Promise.all( + renderers.map(async type => { + const Renderer = await ethers.getContractFactory(`${type}Renderer`) + const renderer = await Renderer.deploy() + await renderer.deployed() + + return renderer.address + }) + ) + + contract = (await deployContract(owner, RockburgNFTArtifact, rendererAddresses)) as RockburgNFT + }) + + describe('Test formBand function', () => { + it('form a band successfully', async function () { + await contract.connect(addr1).mintArtist('Vocalist 1', 0) // VOCALIST + await contract.connect(addr1).mintArtist('Lead Guitar 1', 1) // LEAD_GUITAR + await contract.connect(addr1).mintArtist('Rhythm Guitar 1', 2) // RHYTHM_GUITAR + await contract.connect(addr1).mintArtist('Bass 1', 3) // BASS + await contract.connect(addr1).mintArtist('Drums 1', 4) // DRUMS + + // Artists owned by the user + expect(await contract.ownerOf(1)).to.equal(addr1.address) + expect(await contract.ownerOf(2)).to.equal(addr1.address) + expect(await contract.ownerOf(3)).to.equal(addr1.address) + expect(await contract.ownerOf(4)).to.equal(addr1.address) + expect(await contract.ownerOf(5)).to.equal(addr1.address) + + // Form a band + await contract.connect(addr1).formBand('Amazing band', 'Blues', 1, 2, 3, 4, 5) + + const band = await contract.getBand(6) + expect(band.name).to.equal('Amazing band') + expect(band.bandType).to.equal('Blues') + expect(band.fanCount).to.be.within(0, 100) + expect(band.buzzPoints).to.be.within(0, 100) + expect(band.vocalistId).to.equal(1) + expect(band.leadGuitarId).to.equal(2) + expect(band.rhythmGuitarId).to.equal(3) + expect(band.bassId).to.equal(4) + expect(band.drumsId).to.equal(5) + + // I own the bands but I don't own anymore the single artists (owned by the contract) + // Otherwise I could transfer them. + // If I want to own them I need to disband the band + + // Band is owned by the user + expect(await contract.ownerOf(6)).to.equal(addr1.address) + + // Artists are owned by the contract + expect(await contract.ownerOf(1)).to.equal(contract.address) + expect(await contract.ownerOf(2)).to.equal(contract.address) + expect(await contract.ownerOf(3)).to.equal(contract.address) + expect(await contract.ownerOf(4)).to.equal(contract.address) + expect(await contract.ownerOf(5)).to.equal(contract.address) + }) + + it('form a band with tokens that does not exist', async function () { + // Form a band + const tx = contract.connect(addr1).formBand('Amazing band', 'Blues', 7, 8, 9, 10, 11) + + await expect(tx).to.be.revertedWith('leadGuitarId is not a Lead Guitar') + }) + + it('form a band with tokens that are not musicians', async function () { + await contract.connect(addr1).mintArtist('Vocalist 1', 0) // VOCALIST + await contract.connect(addr1).mintArtist('Lead Guitar 1', 1) // LEAD_GUITAR + await contract.connect(addr1).mintArtist('Rhythm Guitar 1', 2) // RHYTHM_GUITAR + await contract.connect(addr1).mintArtist('Bass 1', 3) // BASS + await contract.connect(addr1).mintArtist('Drums 1', 4) // DRUMS + + await contract.connect(addr1).mintVenue('Venue 1', 'Italy') // Venue + + // Vocalist of wrong type + let tx = contract.connect(addr1).formBand('Amazing band', 'Blues', 6, 2, 3, 4, 5) + await expect(tx).to.be.revertedWith('vocalistId is not an artist') + + tx = contract.connect(addr1).formBand('Amazing band', 'Blues', 1, 6, 3, 4, 5) + await expect(tx).to.be.revertedWith('leadGuitarId is not an artist') + + tx = contract.connect(addr1).formBand('Amazing band', 'Blues', 1, 2, 6, 4, 5) + await expect(tx).to.be.revertedWith('rhythmGuitarId is not an artist') + + tx = contract.connect(addr1).formBand('Amazing band', 'Blues', 1, 2, 3, 6, 5) + await expect(tx).to.be.revertedWith('bassId is not an artist') + + tx = contract.connect(addr1).formBand('Amazing band', 'Blues', 1, 2, 3, 4, 6) + await expect(tx).to.be.revertedWith('drumsId is not an artist') + }) + + it('form a band with tokens that are musicians but not respect role', async function () { + await contract.connect(addr1).mintArtist('Vocalist 1', 0) // VOCALIST + await contract.connect(addr1).mintArtist('Lead Guitar 1', 1) // LEAD_GUITAR + await contract.connect(addr1).mintArtist('Rhythm Guitar 1', 2) // RHYTHM_GUITAR + await contract.connect(addr1).mintArtist('Bass 1', 3) // BASS + await contract.connect(addr1).mintArtist('Drums 1', 4) // DRUMS + await contract.connect(addr1).mintArtist('Vocalist 2', 0) // VOCALIST + + await contract.connect(addr1).mintVenue('Venue 1', 'Italy') // Venue + + // Vocalist of wrong type + let tx = contract.connect(addr1).formBand('Amazing band', 'Blues', 2, 1, 3, 4, 5) + await expect(tx).to.be.revertedWith('vocalistId is not a Vocalist') + + tx = contract.connect(addr1).formBand('Amazing band', 'Blues', 1, 3, 2, 4, 5) + await expect(tx).to.be.revertedWith('leadGuitarId is not a Lead Guitar') + + tx = contract.connect(addr1).formBand('Amazing band', 'Blues', 1, 2, 4, 3, 5) + await expect(tx).to.be.revertedWith('rhythmGuitarId is not a Rhythm Guitar') + + tx = contract.connect(addr1).formBand('Amazing band', 'Blues', 1, 2, 3, 5, 4) + await expect(tx).to.be.revertedWith('bassId is not a Bass') + + tx = contract.connect(addr1).formBand('Amazing band', 'Blues', 1, 2, 3, 4, 6) + await expect(tx).to.be.revertedWith('drumsId is not a Drums') + }) + + it("form a band with artists you don't own", async function () { + await contract.connect(addr1).mintArtist('Vocalist 1', 0) // VOCALIST + await contract.connect(addr1).mintArtist('Lead Guitar 1', 1) // LEAD_GUITAR + await contract.connect(addr1).mintArtist('Rhythm Guitar 1', 2) // RHYTHM_GUITAR + await contract.connect(addr1).mintArtist('Bass 1', 3) // BASS + await contract.connect(addr1).mintArtist('Drums 1', 4) // DRUMS + await contract.connect(addr1).mintArtist('Vocalist 2', 0) // VOCALIST + + // Vocalist of wrong type + let tx = contract.connect(addr2).formBand('Amazing band', 'Blues', 1, 2, 3, 4, 5) + await expect(tx).to.be.revertedWith('ERC721: transfer caller is not owner nor approved') + }) + }) + + describe('Test disbandBand function', () => { + it('disband a band successfully', async function () { + await contract.connect(addr1).mintArtist('Vocalist 1', 0) // VOCALIST + await contract.connect(addr1).mintArtist('Lead Guitar 1', 1) // LEAD_GUITAR + await contract.connect(addr1).mintArtist('Rhythm Guitar 1', 2) // RHYTHM_GUITAR + await contract.connect(addr1).mintArtist('Bass 1', 3) // BASS + await contract.connect(addr1).mintArtist('Drums 1', 4) // DRUMS + + // Form a band + await contract.connect(addr1).formBand('Amazing band', 'Blues', 1, 2, 3, 4, 5) + expect(await contract.ownerOf(1)).to.equal(contract.address) + expect(await contract.ownerOf(2)).to.equal(contract.address) + expect(await contract.ownerOf(3)).to.equal(contract.address) + expect(await contract.ownerOf(4)).to.equal(contract.address) + expect(await contract.ownerOf(5)).to.equal(contract.address) + + // Disband it and take back all + // At the moment the "burn" method is implemented + await contract.connect(addr1).disbandBand(6) + + // Artists are owned by the user again + expect(await contract.ownerOf(1)).to.equal(addr1.address) + expect(await contract.ownerOf(2)).to.equal(addr1.address) + expect(await contract.ownerOf(3)).to.equal(addr1.address) + expect(await contract.ownerOf(4)).to.equal(addr1.address) + expect(await contract.ownerOf(5)).to.equal(addr1.address) + + await expect(contract.ownerOf(6)).to.be.revertedWith('ERC721: owner query for nonexistent token') + await expect(contract.getBand(6)).to.be.revertedWith('token does not exist') + }) + + it('disband a band that does not exist', async function () { + // At the moment the "burn" method is implemented + const tx = contract.connect(addr1).disbandBand(6) + + await expect(tx).to.be.revertedWith('ERC721: operator query for nonexistent token') + }) + + it('disband a token that is not a band', async function () { + await contract.connect(addr1).mintArtist('Vocalist 1', 0) // VOCALIST + // At the moment the "burn" method is implemented + const tx = contract.connect(addr1).disbandBand(1) + + await expect(tx).to.be.revertedWith('token is not a band') + }) + + it("disband a band you don't own", async function () { + await contract.connect(addr1).mintArtist('Vocalist 1', 0) // VOCALIST + await contract.connect(addr1).mintArtist('Lead Guitar 1', 1) // LEAD_GUITAR + await contract.connect(addr1).mintArtist('Rhythm Guitar 1', 2) // RHYTHM_GUITAR + await contract.connect(addr1).mintArtist('Bass 1', 3) // BASS + await contract.connect(addr1).mintArtist('Drums 1', 4) // DRUMS + + // Form a band + await contract.connect(addr1).formBand('Amazing band', 'Blues', 1, 2, 3, 4, 5) + + // Disband it and take back all + const tx = contract.connect(addr2).disbandBand(6) + + await expect(tx).to.be.revertedWith("You don't own the band") + }) + }) +}) diff --git a/test/metadata-test.ts b/test/metadata-test.ts index a216591..115f2e9 100644 --- a/test/metadata-test.ts +++ b/test/metadata-test.ts @@ -31,7 +31,7 @@ describe('RockburgNFT', function () { }) it('correctly generates metadata for a musician', async function () { - const mintTx = await contract.mintArtist('Shpigford', 'Coder') + const mintTx = await contract.mintArtist('Shpigford', 0) // wait until the transaction is mined await mintTx.wait() @@ -42,8 +42,7 @@ describe('RockburgNFT', function () { expect(metadata.name).to.equal('Musician #1') expect(metadata.description).to.equal('Rockburg is a WIP card game on the blockchain.') - - expect(generatedSVG).to.equal(generateMusicianSVG(artistStats.name, artistStats.role, artistStats.skillPoints, artistStats.egoPoints, artistStats.lookPoints, artistStats.creativePoints)) + expect(generatedSVG).to.equal(generateMusicianSVG(artistStats.name, 'Vocalist', artistStats.skillPoints, artistStats.egoPoints, artistStats.lookPoints, artistStats.creativePoints)) }) it('correctly generates metadata for a venue', async function () {