From 6208780db8b78d71206c333c1785081412289939 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Fri, 12 Apr 2024 09:35:17 -0500 Subject: [PATCH] NFTs module, minting from the board, and color based pixel quests --- onchain/Scarb.lock | 6 + onchain/Scarb.toml | 1 + onchain/src/art_peace.cairo | 42 ++++++ onchain/src/interface.cairo | 6 + onchain/src/lib.cairo | 10 ++ onchain/src/nfts/canvas_nft.cairo | 64 +++++++++ onchain/src/nfts/component.cairo | 47 +++++++ onchain/src/nfts/interface.cairo | 39 ++++++ onchain/src/quests/pixel_quest.cairo | 78 ++++++++--- onchain/src/tests/art_peace.cairo | 198 ++++++++++++++++++++++++--- 10 files changed, 451 insertions(+), 40 deletions(-) create mode 100644 onchain/src/nfts/canvas_nft.cairo create mode 100644 onchain/src/nfts/component.cairo create mode 100644 onchain/src/nfts/interface.cairo diff --git a/onchain/Scarb.lock b/onchain/Scarb.lock index bb1ac929..a3a02cd3 100644 --- a/onchain/Scarb.lock +++ b/onchain/Scarb.lock @@ -5,9 +5,15 @@ version = 1 name = "art_peace" version = "0.1.0" dependencies = [ + "openzeppelin", "snforge_std", ] +[[package]] +name = "openzeppelin" +version = "0.11.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.11.0#a83f36b23f1af6e160288962be4a2701c3ecbcda" + [[package]] name = "snforge_std" version = "0.20.0" diff --git a/onchain/Scarb.toml b/onchain/Scarb.toml index 99ed1283..d26fd7fe 100644 --- a/onchain/Scarb.toml +++ b/onchain/Scarb.toml @@ -6,6 +6,7 @@ version = "0.1.0" [dependencies] snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = "v0.20.0" } +openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.11.0" } starknet = "2.6.3" [scripts] diff --git a/onchain/src/art_peace.cairo b/onchain/src/art_peace.cairo index 11f79653..276c9c3f 100644 --- a/onchain/src/art_peace.cairo +++ b/onchain/src/art_peace.cairo @@ -3,6 +3,7 @@ pub mod ArtPeace { use starknet::ContractAddress; use art_peace::{IArtPeace, Pixel}; use art_peace::quests::{IQuestDispatcher, IQuestDispatcherTrait}; + use art_peace::nfts::{IArtPeaceNFTMinter, NFTMetadata, NFTMintParams, ICanvasNFTAdditionalDispatcher, ICanvasNFTAdditionalDispatcherTrait}; use art_peace::templates::component::TemplateStoreComponent; use art_peace::templates::{ITemplateVerifier, TemplateMetadata}; @@ -34,6 +35,7 @@ pub mod ArtPeace { main_quests_count: u32, // Map: quest index -> quest contract address main_quests: LegacyMap::, + nft_contract: ContractAddress, // Map: (day_index, user's address, color index) -> amount of pixels placed user_pixels_placed: LegacyMap::<(u32, ContractAddress, u8), u32>, #[substorage(v0)] @@ -69,6 +71,7 @@ pub mod ArtPeace { pub end_time: u64, pub daily_quests: Span, pub main_quests: Span, + pub nft_contract: ContractAddress, } #[constructor] @@ -110,6 +113,8 @@ pub mod ArtPeace { self.main_quests.write(i, *init_params.main_quests.at(i)); i += 1; }; + + self.nft_contract.write(init_params.nft_contract); } #[abi(embed_v0)] @@ -346,6 +351,10 @@ pub mod ArtPeace { } } + fn get_nft_contract(self: @ContractState) -> ContractAddress { + self.nft_contract.read() + } + fn get_user_pixels_placed(self: @ContractState, user: ContractAddress) -> u32 { let mut i = 0; let mut total = 0; @@ -375,6 +384,19 @@ pub mod ArtPeace { total } + fn get_user_pixels_placed_color( + self: @ContractState, user: ContractAddress, color: u8 + ) -> u32 { + let mut total = 0; + let last_day = self.day_index.read() + 1; + let mut i = 0; + while i < last_day { + total += self.user_pixels_placed.read((i, user, color)); + i += 1; + }; + total + } + fn get_user_pixels_placed_day_color( self: @ContractState, user: ContractAddress, day: u32, color: u8 ) -> u32 { @@ -382,6 +404,25 @@ pub mod ArtPeace { } } + #[abi(embed_v0)] + impl ArtPeaceNFTMinter of IArtPeaceNFTMinter { + fn mint_nft( + self: @ContractState, mint_params: NFTMintParams + ) { + let metadata = NFTMetadata { + position: mint_params.position, + width: mint_params.width, + height: mint_params.height, + image_hash: 0, // TODO + block_number: starknet::get_block_number(), + minter: starknet::get_caller_address(), + }; + ICanvasNFTAdditionalDispatcher { + contract_address: self.nft_contract.read(), + }.mint(metadata, starknet::get_caller_address()); + } + } + #[abi(embed_v0)] impl ArtPeaceTemplateVerifier of ITemplateVerifier { // TODO: Check template function @@ -408,6 +449,7 @@ pub mod ArtPeace { let pos = template_pos_x + x + (template_pos_y + y) * canvas_width; let color = *template_image .at((x + y * template_metadata.width).try_into().unwrap()); + // TODO: Check if the color is transparent if color == self.canvas.read(pos).color { matches += 1; } diff --git a/onchain/src/interface.cairo b/onchain/src/interface.cairo index 5d1b785a..5c02f0c9 100644 --- a/onchain/src/interface.cairo +++ b/onchain/src/interface.cairo @@ -57,11 +57,17 @@ pub trait IArtPeace { fn claim_today_quest(ref self: TContractState, quest_id: u32); fn claim_main_quest(ref self: TContractState, quest_id: u32); + // NFT info + fn get_nft_contract(self: @TContractState) -> starknet::ContractAddress; + // Stats fn get_user_pixels_placed(self: @TContractState, user: starknet::ContractAddress) -> u32; fn get_user_pixels_placed_day( self: @TContractState, user: starknet::ContractAddress, day: u32 ) -> u32; + fn get_user_pixels_placed_color( + self: @TContractState, user: starknet::ContractAddress, color: u8 + ) -> u32; fn get_user_pixels_placed_day_color( self: @TContractState, user: starknet::ContractAddress, day: u32, color: u8 ) -> u32; diff --git a/onchain/src/lib.cairo b/onchain/src/lib.cairo index a49dc556..58417a34 100644 --- a/onchain/src/lib.cairo +++ b/onchain/src/lib.cairo @@ -20,6 +20,16 @@ mod templates { }; } +mod nfts { + pub mod interface; + mod component; + mod canvas_nft; + + use interface::{ + NFTMintParams, NFTMetadata, IArtPeaceNFTMinter, ICanvasNFTStoreDispatcher, ICanvasNFTStoreDispatcherTrait, IArtPeaceNFTMinterDispatcher, IArtPeaceNFTMinterDispatcherTrait, ICanvasNFTAdditional, ICanvasNFTAdditionalDispatcher, ICanvasNFTAdditionalDispatcherTrait + }; +} + #[cfg(test)] mod tests { mod art_peace; diff --git a/onchain/src/nfts/canvas_nft.cairo b/onchain/src/nfts/canvas_nft.cairo new file mode 100644 index 00000000..9f0871e0 --- /dev/null +++ b/onchain/src/nfts/canvas_nft.cairo @@ -0,0 +1,64 @@ +#[starknet::contract] +mod CanvasNFT { + use openzeppelin::token::erc721::ERC721Component; + use openzeppelin::introspection::src5::SRC5Component; + use starknet::ContractAddress; + use art_peace::nfts::component::CanvasNFTStoreComponent; + use art_peace::nfts::{ICanvasNFTAdditional, NFTMetadata}; + + component!(path: ERC721Component, storage: erc721, event: ERC721Event); + component!(path: SRC5Component, storage: src5, event: SRC5Event); + component!(path: CanvasNFTStoreComponent, storage: nfts, event: NFTEvent); + + #[abi(embed_v0)] + impl ERC721Impl = ERC721Component::ERC721Impl; + #[abi(embed_v0)] + impl ERC721MetadataImpl = ERC721Component::ERC721MetadataImpl; + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl; + #[abi(embed_v0)] + impl CanvasNFTStoreImpl = CanvasNFTStoreComponent::CanvasNFTStoreImpl; + + impl InternalImpl = ERC721Component::InternalImpl; + + #[storage] + struct Storage { + art_peace: ContractAddress, + #[substorage(v0)] + erc721: ERC721Component::Storage, + #[substorage(v0)] + src5: SRC5Component::Storage, + #[substorage(v0)] + nfts: CanvasNFTStoreComponent::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC721Event: ERC721Component::Event, + #[flat] + SRC5Event: SRC5Component::Event, + #[flat] + NFTEvent: CanvasNFTStoreComponent::Event, + } + + #[constructor] + fn constructor(ref self: ContractState, name: ByteArray, symbol: ByteArray, art_peace_addr: ContractAddress) { + self.art_peace.write(art_peace_addr); + let base_uri = format!("{:?}", art_peace_addr); + self.erc721.initializer(name, symbol, base_uri); + } + + #[abi(embed_v0)] + impl CanvasNFTAdditional of ICanvasNFTAdditional { + fn mint(ref self: ContractState, metadata: NFTMetadata, receiver: ContractAddress) { + assert(self.art_peace.read() == starknet::get_caller_address(), 'Only ArtPeace contract can mint'); + let token_id = self.nfts.get_nfts_count(); + self.nfts.nfts_data.write(token_id, metadata); + self.erc721._mint(receiver, token_id); + self.nfts.nfts_count.write(token_id + 1); + // TODO: self.emit(Event::NFTEvent::CanvasNFTMinted { token_id, metadata }); + } + } +} diff --git a/onchain/src/nfts/component.cairo b/onchain/src/nfts/component.cairo new file mode 100644 index 00000000..4c9aa38f --- /dev/null +++ b/onchain/src/nfts/component.cairo @@ -0,0 +1,47 @@ +#[starknet::component] +mod CanvasNFTStoreComponent { + use art_peace::nfts::interface::{ICanvasNFTStore, NFTMetadata}; + + #[storage] + struct Storage { + nfts_count: u256, + // Map: nft's token_id -> nft's metadata + nfts_data: LegacyMap::, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + CanvasNFTMinted: CanvasNFTMinted, + } + + #[derive(Drop, starknet::Event)] + struct CanvasNFTMinted { + #[key] + token_id: u256, + metadata: NFTMetadata, + } + + #[embeddable_as(CanvasNFTStoreImpl)] + impl CanvasNFTStore< + TContractState, +HasComponent + > of ICanvasNFTStore> { + fn get_nfts_count(self: @ComponentState) -> u256 { + return self.nfts_count.read(); + } + + fn get_nft_metadata(self: @ComponentState, token_id: u256) -> NFTMetadata { + return self.nfts_data.read(token_id); + } + + fn get_nft_minter(self: @ComponentState, token_id: u256) -> starknet::ContractAddress { + let metadata: NFTMetadata = self.nfts_data.read(token_id); + return metadata.minter; + } + + fn get_nft_image_hash(self: @ComponentState, token_id: u256) -> felt252 { + let metadata: NFTMetadata = self.nfts_data.read(token_id); + return metadata.image_hash; + } + } +} diff --git a/onchain/src/nfts/interface.cairo b/onchain/src/nfts/interface.cairo new file mode 100644 index 00000000..4fd4d4e8 --- /dev/null +++ b/onchain/src/nfts/interface.cairo @@ -0,0 +1,39 @@ +#[derive(Drop, Serde)] +pub struct NFTMintParams { + position: u128, + width: u128, + height: u128, +} + +#[derive(Drop, Serde, PartialEq, starknet::Store)] +pub struct NFTMetadata { + position: u128, + width: u128, + height: u128, + image_hash: felt252, + block_number: u64, + minter: starknet::ContractAddress, +} + +#[starknet::interface] +pub trait ICanvasNFTStore { + // Returns the on-chain metadata of the NFT. + fn get_nft_metadata(self: @TContractState, token_id: u256) -> NFTMetadata; + fn get_nft_minter(self: @TContractState, token_id: u256) -> starknet::ContractAddress; + fn get_nft_image_hash(self: @TContractState, token_id: u256) -> felt252; + + // Returns the number of NFTs stored in the contract state. + fn get_nfts_count(self: @TContractState) -> u256; +} + +#[starknet::interface] +pub trait ICanvasNFTAdditional { + // Mint a new NFT called by the ArtPeaceNFTMinter contract. + fn mint(ref self: TContractState, metadata: NFTMetadata, receiver: starknet::ContractAddress); +} + +#[starknet::interface] +pub trait IArtPeaceNFTMinter { + // Mints a new NFT from the canvas using init params, and returns the token ID. + fn mint_nft(self: @TContractState, mint_params: NFTMintParams); +} diff --git a/onchain/src/quests/pixel_quest.cairo b/onchain/src/quests/pixel_quest.cairo index efddc53e..a7393614 100644 --- a/onchain/src/quests/pixel_quest.cairo +++ b/onchain/src/quests/pixel_quest.cairo @@ -4,6 +4,8 @@ trait IPixelQuest { fn get_pixels_needed(self: @TContractState) -> u32; fn is_daily(self: @TContractState) -> bool; fn claim_day(self: @TContractState) -> u32; + fn is_color(self: @TContractState) -> bool; + fn color(self: @TContractState) -> u8; } #[starknet::contract] @@ -19,9 +21,12 @@ mod PixelQuest { reward: u32, claimed: LegacyMap, pixels_needed: u32, - is_daily: bool, + // Quest types + is_daily: bool, // If the quest is a daily quest // The day idx the quest can be claimed ( if daily ) claim_day: u32, + is_color: bool, // If the quest is for a specific color + color: u8, } #[event] @@ -30,20 +35,26 @@ mod PixelQuest { QuestClaimed: QuestClaimed, } + #[derive(Drop, Serde)] + pub struct PixelQuestInitParams { + pub art_peace: ContractAddress, + pub reward: u32, + pub pixels_needed: u32, + pub is_daily: bool, + pub claim_day: u32, + pub is_color: bool, + pub color: u8, + } + #[constructor] - fn constructor( - ref self: ContractState, - art_peace: ContractAddress, - reward: u32, - pixels_needed: u32, - is_daily: bool, - claim_day: u32 - ) { - self.art_peace.write(IArtPeaceDispatcher { contract_address: art_peace }); - self.reward.write(reward); - self.pixels_needed.write(pixels_needed); - self.is_daily.write(is_daily); - self.claim_day.write(claim_day); + fn constructor(ref self: ContractState, init_params: PixelQuestInitParams,) { + self.art_peace.write(IArtPeaceDispatcher { contract_address: init_params.art_peace }); + self.reward.write(init_params.reward); + self.pixels_needed.write(init_params.pixels_needed); + self.is_daily.write(init_params.is_daily); + self.claim_day.write(init_params.claim_day); + self.is_color.write(init_params.is_color); + self.color.write(init_params.color); } #[abi(embed_v0)] @@ -63,6 +74,14 @@ mod PixelQuest { fn claim_day(self: @ContractState) -> u32 { return self.claim_day.read(); } + + fn is_color(self: @ContractState) -> bool { + return self.is_color.read(); + } + + fn color(self: @ContractState) -> u8 { + return self.color.read(); + } } // TODO: Test all @@ -79,16 +98,32 @@ mod PixelQuest { } if self.is_daily.read() { // Daily Pixel Quest - let day = art_peace.get_day(); - if day != self.claim_day.read() { - return false; + if self.is_color.read() { + let day = art_peace.get_day(); + if day != self.claim_day.read() { + return false; + } + let placement_count = art_peace + .get_user_pixels_placed_day_color(user, day, self.color.read()); + return placement_count >= self.pixels_needed.read(); + } else { + let day = art_peace.get_day(); + if day != self.claim_day.read() { + return false; + } + let placement_count = art_peace.get_user_pixels_placed_day(user, day); + return placement_count >= self.pixels_needed.read(); } - let placement_count = art_peace.get_user_pixels_placed_day(user, day); - return placement_count >= self.pixels_needed.read(); } else { // Main Pixel Quest - let placement_count = art_peace.get_user_pixels_placed(user); - return placement_count >= self.pixels_needed.read(); + if self.is_color.read() { + let placement_count = art_peace + .get_user_pixels_placed_color(user, self.color.read()); + return placement_count >= self.pixels_needed.read(); + } else { + let placement_count = art_peace.get_user_pixels_placed(user); + return placement_count >= self.pixels_needed.read(); + } } } @@ -97,6 +132,7 @@ mod PixelQuest { get_caller_address() == self.art_peace.read().contract_address, 'Only ArtPeace can claim quests' ); + // TODO: should we revert if the quest is not claimable? if !self.is_claimable(user) { return 0; } diff --git a/onchain/src/tests/art_peace.cairo b/onchain/src/tests/art_peace.cairo index ce0fa0a9..7570ea02 100644 --- a/onchain/src/tests/art_peace.cairo +++ b/onchain/src/tests/art_peace.cairo @@ -1,10 +1,13 @@ use art_peace::{IArtPeaceDispatcher, IArtPeaceDispatcherTrait}; use art_peace::ArtPeace::InitParams; +use art_peace::quests::pixel_quest::PixelQuest::PixelQuestInitParams; +use art_peace::nfts::{IArtPeaceNFTMinterDispatcher, IArtPeaceNFTMinterDispatcherTrait, ICanvasNFTStoreDispatcher, ICanvasNFTStoreDispatcherTrait, NFTMintParams, NFTMetadata}; use art_peace::templates::{ ITemplateStoreDispatcher, ITemplateStoreDispatcherTrait, ITemplateVerifierDispatcher, ITemplateVerifierDispatcherTrait, TemplateMetadata }; +use openzeppelin::token::erc721::interface::{IERC721Dispatcher, IERC721DispatcherTrait}; use snforge_std as snf; use snforge_std::{CheatTarget, ContractClassTrait}; use starknet::{ContractAddress, contract_address_const}; @@ -17,7 +20,25 @@ fn ART_PEACE_CONTRACT() -> ContractAddress { contract_address_const::<'ArtPeace'>() } +fn EMPTY_QUEST_CONTRACT() -> ContractAddress { + contract_address_const::<'EmptyQuest'>() +} + +fn NFT_CONTRACT() -> ContractAddress { + contract_address_const::<'CanvasNFT'>() +} + +fn PLAYER1() -> ContractAddress { + contract_address_const::<'Player1'>() +} + +fn PLAYER2() -> ContractAddress { + contract_address_const::<'Player2'>() +} + fn deploy_contract() -> ContractAddress { + deploy_nft_contract(); + let contract = snf::declare("ArtPeace"); let mut calldata = array![]; InitParams { @@ -41,6 +62,7 @@ fn deploy_contract() -> ContractAddress { end_time: 1000000, daily_quests: array![].span(), main_quests: array![].span(), + nft_contract: NFT_CONTRACT(), } .serialize(ref calldata); let contract_addr = contract.deploy_at(@calldata, ART_PEACE_CONTRACT()).unwrap(); @@ -51,6 +73,8 @@ fn deploy_contract() -> ContractAddress { fn deploy_with_quests_contract( daily_quests: Span, main_quests: Span ) -> ContractAddress { + deploy_nft_contract(); + let contract = snf::declare("ArtPeace"); let mut calldata = array![]; InitParams { @@ -74,6 +98,7 @@ fn deploy_with_quests_contract( end_time: 1000000, daily_quests: daily_quests, main_quests: main_quests, + nft_contract: NFT_CONTRACT(), } .serialize(ref calldata); let contract_addr = contract.deploy_at(@calldata, ART_PEACE_CONTRACT()).unwrap(); @@ -81,16 +106,75 @@ fn deploy_with_quests_contract( contract_addr } -fn deploy_pixel_quest_daily(pixel_quest: snf::ContractClass) -> ContractAddress { - // art_peace, reward, pixels_needed, is_daily, claim_day - let mut calldata: Array = array![ART_PEACE_CONTRACT().into(), 10, 3, 1, 0]; - pixel_quest.deploy(@calldata).unwrap() +fn deploy_pixel_quests_daily(pixel_quest: snf::ContractClass) -> Array { + let mut daily_pixel_calldata = array![]; + PixelQuestInitParams { + art_peace: ART_PEACE_CONTRACT(), + reward: 10, + pixels_needed: 3, + is_daily: true, + claim_day: 0, + is_color: false, + color: 0, + } + .serialize(ref daily_pixel_calldata); + let daily_pixel_quest = pixel_quest.deploy(@daily_pixel_calldata).unwrap(); + + let mut daily_color_calldata = array![]; + PixelQuestInitParams { + art_peace: ART_PEACE_CONTRACT(), + reward: 10, + pixels_needed: 3, + is_daily: true, + claim_day: 0, + is_color: true, + color: 0x1, + } + .serialize(ref daily_color_calldata); + let daily_color_quest = pixel_quest.deploy(@daily_color_calldata).unwrap(); + + array![daily_pixel_quest, daily_color_quest, EMPTY_QUEST_CONTRACT()] } -fn deploy_pixel_quest_main(pixel_quest: snf::ContractClass) -> ContractAddress { - // art_peace, reward, pixels_needed, is_daily, claim_day - let mut calldata: Array = array![ART_PEACE_CONTRACT().into(), 20, 4, 0, 0]; - pixel_quest.deploy(@calldata).unwrap() +fn deploy_pixel_quests_main(pixel_quest: snf::ContractClass) -> Array { + let mut main_pixel_calldata = array![]; + PixelQuestInitParams { + art_peace: ART_PEACE_CONTRACT(), + reward: 20, + pixels_needed: 4, + is_daily: false, + claim_day: 0, + is_color: false, + color: 0, + } + .serialize(ref main_pixel_calldata); + let main_pixel_quest = pixel_quest.deploy(@main_pixel_calldata).unwrap(); + + let mut main_color_calldata = array![]; + PixelQuestInitParams { + art_peace: ART_PEACE_CONTRACT(), + reward: 20, + pixels_needed: 4, + is_daily: false, + claim_day: 0, + is_color: true, + color: 0x1, + } + .serialize(ref main_color_calldata); + let main_color_quest = pixel_quest.deploy(@main_color_calldata).unwrap(); + + array![main_pixel_quest, main_color_quest] +} + +fn deploy_nft_contract() -> ContractAddress { + let contract = snf::declare("CanvasNFT"); + let mut calldata = array![]; + let name: ByteArray = "CanvasNFTs"; + let symbol: ByteArray = "A/P"; + name.serialize(ref calldata); + symbol.serialize(ref calldata); + calldata.append(ART_PEACE_CONTRACT().into()); + contract.deploy_at(@calldata, NFT_CONTRACT()).unwrap() } fn warp_to_next_available_time(art_peace: IArtPeaceDispatcher) { @@ -141,9 +225,8 @@ fn place_pixel_test() { #[test] fn deploy_quest_test() { let pixel_quest = snf::declare("PixelQuest"); - let empty_quest = contract_address_const::<'EmptyQuest'>(); - let daily_quests = array![deploy_pixel_quest_daily(pixel_quest), empty_quest, empty_quest]; - let main_quests = array![deploy_pixel_quest_main(pixel_quest)]; + let daily_quests = deploy_pixel_quests_daily(pixel_quest); + let main_quests = deploy_pixel_quests_main(pixel_quest); let art_peace = IArtPeaceDispatcher { contract_address: deploy_with_quests_contract(daily_quests.span(), main_quests.span()) }; @@ -159,11 +242,10 @@ fn deploy_quest_test() { // TODO: Daily quest test day 2, stats, other fields ... #[test] -fn pixel_quest_test() { +fn pixel_quests_test() { let pixel_quest = snf::declare("PixelQuest"); - let empty_quest = contract_address_const::<'EmptyQuest'>(); - let daily_quests = array![deploy_pixel_quest_daily(pixel_quest), empty_quest, empty_quest]; - let main_quests = array![deploy_pixel_quest_main(pixel_quest)]; + let daily_quests = deploy_pixel_quests_daily(pixel_quest); + let main_quests = deploy_pixel_quests_main(pixel_quest); let art_peace = IArtPeaceDispatcher { contract_address: deploy_with_quests_contract(daily_quests.span(), main_quests.span()) }; @@ -176,16 +258,20 @@ fn pixel_quest_test() { let color = 0x5; art_peace.place_pixel(pos, color); art_peace.claim_daily_quest(0, 0); + art_peace.claim_daily_quest(0, 1); art_peace.claim_main_quest(0); + art_peace.claim_main_quest(1); assert!(art_peace.get_extra_pixels_count() == 0, "Extra pixels are wrong after invalid claims"); warp_to_next_available_time(art_peace); let x = 15; let y = 25; - let color = 0x7; + let color = 0x1; art_peace.place_pixel_xy(x, y, color); art_peace.claim_daily_quest(0, 0); + art_peace.claim_daily_quest(0, 1); art_peace.claim_main_quest(0); + art_peace.claim_main_quest(1); assert!(art_peace.get_extra_pixels_count() == 0, "Extra pixels are wrong after invalid claims"); warp_to_next_available_time(art_peace); @@ -195,20 +281,51 @@ fn pixel_quest_test() { let color = 0x9; art_peace.place_pixel(pos, color); art_peace.claim_daily_quest(0, 0); + art_peace.claim_daily_quest(0, 1); art_peace.claim_main_quest(0); + art_peace.claim_main_quest(1); assert!( - art_peace.get_extra_pixels_count() == 10, "Extra pixels are wrong after daily quest claim" + art_peace.get_extra_pixels_count() == 10, "Extra pixels are wrong after daily quest 1 claim" ); warp_to_next_available_time(art_peace); let x = 25; let y = 35; - let color = 0xB; + let color = 0x1; + art_peace.place_pixel_xy(x, y, color); + art_peace.claim_daily_quest(0, 0); + art_peace.claim_daily_quest(0, 1); + art_peace.claim_main_quest(0); + art_peace.claim_main_quest(1); + assert!( + art_peace.get_extra_pixels_count() == 30, "Extra pixels are wrong after main quest 1 claim" + ); + + warp_to_next_available_time(art_peace); + let x = 30; + let y = 40; + let pos = x + y * WIDTH; + let color = 0x1; + art_peace.place_pixel(pos, color); + art_peace.claim_daily_quest(0, 0); + art_peace.claim_daily_quest(0, 1); + art_peace.claim_main_quest(0); + art_peace.claim_main_quest(1); + assert!( + art_peace.get_extra_pixels_count() == 40, "Extra pixels are wrong after daily quest 2 claim" + ); + + warp_to_next_available_time(art_peace); + let x = 35; + let y = 45; + let color = 0x1; art_peace.place_pixel_xy(x, y, color); art_peace.claim_daily_quest(0, 0); + art_peace.claim_daily_quest(0, 1); art_peace.claim_main_quest(0); + art_peace.claim_main_quest(1); assert!( - art_peace.get_extra_pixels_count() == 30, "Extra pixels are wrong after main quest claim" + art_peace.get_extra_pixels_count() == 60, "Extra pixels are wrong after main quest 2 claim" ); } @@ -312,4 +429,47 @@ fn template_full_basic_test() { } // TODO: test invalid template inputs +// TODO: Deploy test for nft that checks name, symbol, uri, etc. +#[test] +fn nft_mint_test() { + let art_peace = IArtPeaceDispatcher { contract_address: deploy_contract() }; + let nft_minter = IArtPeaceNFTMinterDispatcher { + contract_address: art_peace.contract_address + }; + let nft_store = ICanvasNFTStoreDispatcher { contract_address: NFT_CONTRACT() }; + let nft = IERC721Dispatcher { contract_address: NFT_CONTRACT() }; + + let mint_params = NFTMintParams { + position: 10, + width: 16, + height: 16, + }; + snf::start_prank(CheatTarget::One(nft_minter.contract_address), PLAYER1()); + nft_minter.mint_nft(mint_params); + snf::stop_prank(CheatTarget::One(nft_minter.contract_address)); + + let expected_metadata = NFTMetadata { + position: 10, + width: 16, + height: 16, + image_hash: 0, + block_number: 2000, // TODO + minter: PLAYER1(), + }; + let nft_metadata = nft_store.get_nft_metadata(0); + + assert!(nft_store.get_nfts_count() == 1, "NFTs count is not 1"); + assert!(nft_metadata == expected_metadata, "NFT metadata is not correct"); + assert!(nft.owner_of(0) == PLAYER1(), "NFT owner is not correct"); + assert!(nft.balance_of(PLAYER1()) == 1, "NFT balance is not correct"); + assert!(nft.balance_of(PLAYER2()) == 0, "NFT balance is not correct"); + + snf::start_prank(CheatTarget::One(nft.contract_address), PLAYER1()); + nft.transfer_from(PLAYER1(), PLAYER2(), 0); + snf::stop_prank(CheatTarget::One(nft.contract_address)); + + assert!(nft.owner_of(0) == PLAYER2(), "NFT owner is not correct after transfer"); + assert!(nft.balance_of(PLAYER1()) == 0, "NFT balance is not correct after transfer"); + assert!(nft.balance_of(PLAYER2()) == 1, "NFT balance is not correct after transfer"); +}