diff --git a/onchain/Scarb.lock b/onchain/Scarb.lock index 576c9446..e6e8e9f8 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.21.0" diff --git a/onchain/Scarb.toml b/onchain/Scarb.toml index 28b58fd0..ad672acb 100644 --- a/onchain/Scarb.toml +++ b/onchain/Scarb.toml @@ -7,6 +7,7 @@ edition = "2023_11" [dependencies] snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = "v0.21.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 92f65449..9d1bd625 100644 --- a/onchain/src/art_peace.cairo +++ b/onchain/src/art_peace.cairo @@ -3,8 +3,12 @@ pub mod ArtPeace { use starknet::ContractAddress; use art_peace::{IArtPeace, Pixel}; use art_peace::quests::interfaces::{IQuestDispatcher, IQuestDispatcherTrait}; + use art_peace::nfts::interfaces::{ + IArtPeaceNFTMinter, NFTMetadata, NFTMintParams, ICanvasNFTAdditionalDispatcher, + ICanvasNFTAdditionalDispatcherTrait + }; use art_peace::templates::component::TemplateStoreComponent; - use art_peace::templates::interface::{ITemplateVerifier, TemplateMetadata}; + use art_peace::templates::interfaces::{ITemplateVerifier, ITemplateStore, TemplateMetadata}; component!(path: TemplateStoreComponent, storage: templates, event: TemplateEvent); @@ -35,6 +39,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)] @@ -77,6 +82,7 @@ pub mod ArtPeace { pub end_time: u64, pub daily_quests: Span, pub main_quests: Span, + pub nft_contract: ContractAddress, } const DAY_IN_SECONDS: u64 = consteval_int!(60 * 60 * 24); @@ -121,6 +127,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)] @@ -375,6 +383,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; @@ -406,6 +418,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 { @@ -413,6 +438,23 @@ 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 @@ -439,6 +481,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/interfaces.cairo similarity index 93% rename from onchain/src/interface.cairo rename to onchain/src/interfaces.cairo index 6e73f926..235cd6b5 100644 --- a/onchain/src/interface.cairo +++ b/onchain/src/interfaces.cairo @@ -62,11 +62,17 @@ pub trait IArtPeace { fn claim_today_quest(ref self: TContractState, quest_id: u32, calldata: Span); fn claim_main_quest(ref self: TContractState, quest_id: u32, calldata: Span); + // 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 d04c0c45..4097103e 100644 --- a/onchain/src/lib.cairo +++ b/onchain/src/lib.cairo @@ -1,7 +1,7 @@ pub mod art_peace; -pub mod interface; +pub mod interfaces; use art_peace::ArtPeace; -use interface::{IArtPeace, IArtPeaceDispatcher, IArtPeaceDispatcherTrait, Pixel}; +use interfaces::{IArtPeace, IArtPeaceDispatcher, IArtPeaceDispatcherTrait, Pixel}; mod quests { pub mod interfaces; @@ -11,15 +11,28 @@ mod quests { } mod templates { - pub mod interface; + pub mod interfaces; pub mod component; - use interface::{ + use interfaces::{ TemplateMetadata, ITemplateVerifier, ITemplateStoreDispatcher, ITemplateStoreDispatcherTrait, ITemplateVerifierDispatcher, ITemplateVerifierDispatcherTrait }; } +mod nfts { + pub mod interfaces; + pub mod component; + mod canvas_nft; + + use interfaces::{ + 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..537d6ed2 --- /dev/null +++ b/onchain/src/nfts/canvas_nft.cairo @@ -0,0 +1,70 @@ +#[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..e0d8eb70 --- /dev/null +++ b/onchain/src/nfts/component.cairo @@ -0,0 +1,49 @@ +#[starknet::component] +pub mod CanvasNFTStoreComponent { + use art_peace::nfts::interfaces::{ICanvasNFTStore, NFTMetadata}; + + #[storage] + struct Storage { + nfts_count: u256, + // Map: nft's token_id -> nft's metadata + nfts_data: LegacyMap::, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub 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/interfaces.cairo b/onchain/src/nfts/interfaces.cairo new file mode 100644 index 00000000..dccf722d --- /dev/null +++ b/onchain/src/nfts/interfaces.cairo @@ -0,0 +1,39 @@ +#[derive(Drop, Serde)] +pub struct NFTMintParams { + pub position: u128, + pub width: u128, + pub height: u128, +} + +#[derive(Drop, Serde, PartialEq, starknet::Store)] +pub struct NFTMetadata { + pub position: u128, + pub width: u128, + pub height: u128, + pub image_hash: felt252, + pub block_number: u64, + pub 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/interfaces.cairo b/onchain/src/quests/interfaces.cairo index ad576d9f..5010679a 100644 --- a/onchain/src/quests/interfaces.cairo +++ b/onchain/src/quests/interfaces.cairo @@ -23,4 +23,6 @@ pub 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; } diff --git a/onchain/src/quests/pixel_quest.cairo b/onchain/src/quests/pixel_quest.cairo index 425c4087..9abbf2e3 100644 --- a/onchain/src/quests/pixel_quest.cairo +++ b/onchain/src/quests/pixel_quest.cairo @@ -1,5 +1,5 @@ #[starknet::contract] -mod PixelQuest { +pub mod PixelQuest { use starknet::{ContractAddress, get_caller_address}; use art_peace::{IArtPeaceDispatcher, IArtPeaceDispatcherTrait}; use art_peace::quests::{IQuest, IPixelQuest, QuestClaimed}; @@ -10,9 +10,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] @@ -21,20 +24,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)] @@ -54,6 +63,14 @@ mod PixelQuest { fn claim_day(self: @ContractState) -> u32 { self.claim_day.read() } + + fn is_color(self: @ContractState) -> bool { + return self.is_color.read(); + } + + fn color(self: @ContractState) -> u8 { + return self.color.read(); + } } #[abi(embed_v0)] @@ -72,18 +89,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); - - placement_count >= self.pixels_needed.read() } else { // Main Pixel Quest - let placement_count = art_peace.get_user_pixels_placed(user); - - 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(); + } } } @@ -92,6 +123,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, calldata) { return 0; } diff --git a/onchain/src/templates/component.cairo b/onchain/src/templates/component.cairo index e8f658fa..e159b069 100644 --- a/onchain/src/templates/component.cairo +++ b/onchain/src/templates/component.cairo @@ -1,6 +1,6 @@ #[starknet::component] pub mod TemplateStoreComponent { - use art_peace::templates::interface::{ITemplateStore, TemplateMetadata}; + use art_peace::templates::interfaces::{ITemplateStore, TemplateMetadata}; #[storage] struct Storage { diff --git a/onchain/src/templates/interface.cairo b/onchain/src/templates/interfaces.cairo similarity index 100% rename from onchain/src/templates/interface.cairo rename to onchain/src/templates/interfaces.cairo diff --git a/onchain/src/tests/art_peace.cairo b/onchain/src/tests/art_peace.cairo index 8b21f2b3..c679dd01 100644 --- a/onchain/src/tests/art_peace.cairo +++ b/onchain/src/tests/art_peace.cairo @@ -1,6 +1,11 @@ use art_peace::{IArtPeaceDispatcher, IArtPeaceDispatcherTrait}; use art_peace::ArtPeace::InitParams; -use art_peace::templates::interface::{ +use art_peace::quests::pixel_quest::PixelQuest::PixelQuestInitParams; +use art_peace::nfts::interfaces::{ + IArtPeaceNFTMinterDispatcher, IArtPeaceNFTMinterDispatcherTrait, ICanvasNFTStoreDispatcher, + ICanvasNFTStoreDispatcherTrait, NFTMintParams, NFTMetadata +}; +use art_peace::templates::interfaces::{ ITemplateStoreDispatcher, ITemplateStoreDispatcherTrait, ITemplateVerifierDispatcher, ITemplateVerifierDispatcherTrait, TemplateMetadata }; @@ -8,6 +13,7 @@ use art_peace::templates::interface::{ use core::poseidon::PoseidonTrait; use core::hash::{HashStateTrait, HashStateExTrait}; +use openzeppelin::token::erc721::interface::{IERC721Dispatcher, IERC721DispatcherTrait}; use snforge_std as snf; use snforge_std::{CheatTarget, ContractClassTrait}; use starknet::{ContractAddress, contract_address_const}; @@ -25,7 +31,25 @@ fn EMPTY_CALLDATA() -> Span { array![].span() } +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 { @@ -49,6 +73,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(); @@ -60,6 +85,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 { @@ -83,6 +110,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(); @@ -91,18 +119,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]; +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(); - pixel_quest.deploy(@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]; +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] +} - pixel_quest.deploy(@calldata).unwrap() +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) { @@ -169,9 +254,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()) }; @@ -187,11 +271,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()) }; @@ -204,16 +287,20 @@ fn pixel_quest_test() { let color = 0x5; art_peace.place_pixel(pos, color); art_peace.claim_daily_quest(0, 0, EMPTY_CALLDATA()); + art_peace.claim_daily_quest(0, 1, EMPTY_CALLDATA()); art_peace.claim_main_quest(0, EMPTY_CALLDATA()); + art_peace.claim_main_quest(1, EMPTY_CALLDATA()); 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, EMPTY_CALLDATA()); + art_peace.claim_daily_quest(0, 1, EMPTY_CALLDATA()); art_peace.claim_main_quest(0, EMPTY_CALLDATA()); + art_peace.claim_main_quest(1, EMPTY_CALLDATA()); assert!(art_peace.get_extra_pixels_count() == 0, "Extra pixels are wrong after invalid claims"); warp_to_next_available_time(art_peace); @@ -223,20 +310,51 @@ fn pixel_quest_test() { let color = 0x9; art_peace.place_pixel(pos, color); art_peace.claim_daily_quest(0, 0, EMPTY_CALLDATA()); + art_peace.claim_daily_quest(0, 1, EMPTY_CALLDATA()); art_peace.claim_main_quest(0, EMPTY_CALLDATA()); + art_peace.claim_main_quest(1, EMPTY_CALLDATA()); 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, EMPTY_CALLDATA()); + art_peace.claim_daily_quest(0, 1, EMPTY_CALLDATA()); art_peace.claim_main_quest(0, EMPTY_CALLDATA()); + art_peace.claim_main_quest(1, EMPTY_CALLDATA()); assert!( - art_peace.get_extra_pixels_count() == 30, "Extra pixels are wrong after main quest claim" + 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, EMPTY_CALLDATA()); + art_peace.claim_daily_quest(0, 1, EMPTY_CALLDATA()); + art_peace.claim_main_quest(0, EMPTY_CALLDATA()); + art_peace.claim_main_quest(1, EMPTY_CALLDATA()); + 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, EMPTY_CALLDATA()); + art_peace.claim_daily_quest(0, 1, EMPTY_CALLDATA()); + art_peace.claim_main_quest(0, EMPTY_CALLDATA()); + art_peace.claim_main_quest(1, EMPTY_CALLDATA()); + assert!( + art_peace.get_extra_pixels_count() == 60, "Extra pixels are wrong after main quest 2 claim" ); } @@ -349,4 +467,41 @@ fn increase_day_panic_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"); +}