-
+
{ !isDesktopOrLaptop && (
)}
{ (!isPortrait ? pixelSelectedMode : pixelSelectedMode && activeTab === tabs[0]) && (
-
+
)}
diff --git a/frontend/src/canvas/Canvas.js b/frontend/src/canvas/Canvas.js
index b30aab17..43302efb 100644
--- a/frontend/src/canvas/Canvas.js
+++ b/frontend/src/canvas/Canvas.js
@@ -88,7 +88,6 @@ const Canvas = props => {
}, [])
const [setup, setSetup] = useState(false)
- const [pixelPlacedBy, setPixelPlacedBy] = useState("")
const draw = useCallback((ctx, imageData) => {
ctx.canvas.width = width
@@ -191,7 +190,7 @@ const Canvas = props => {
}).then(data => {
// TODO: Cache pixel info & clear cache on update from websocket
// TODO: Dont query if hover select ( until 1s after hover? )
- setPixelPlacedBy(data)
+ props.setPixelPlacedBy(data)
}).catch(error => {
console.error(error)
});
diff --git a/frontend/src/canvas/SelectedPixelPanel.css b/frontend/src/canvas/SelectedPixelPanel.css
index cc1a5a76..b4b47e83 100644
--- a/frontend/src/canvas/SelectedPixelPanel.css
+++ b/frontend/src/canvas/SelectedPixelPanel.css
@@ -17,7 +17,7 @@
.SelectedPixelPanel__item {
font-size: 1.2rem;
- text-wrap: nowrap;
+ white-space: nowrap;
margin: 0.3rem;
}
diff --git a/frontend/src/canvas/SelectedPixelPanel.js b/frontend/src/canvas/SelectedPixelPanel.js
index 020cf5a7..9a07a46c 100644
--- a/frontend/src/canvas/SelectedPixelPanel.js
+++ b/frontend/src/canvas/SelectedPixelPanel.js
@@ -6,7 +6,7 @@ const SelectedPixelPanel = props => {
props.clearPixelSelection()}>X
Pos : ({props.selectedPositionX}, {props.selectedPositionY})
-
Owner : 0xplaced_by
+
Owner : 0x{props.pixelPlacedBy}
);
}
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..36ac92f8 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,16 +11,34 @@ 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
+ };
+}
+
+mod mocks {
+ pub mod erc20_mock;
+}
+
#[cfg(test)]
mod tests {
mod art_peace;
+ mod utils;
}
diff --git a/onchain/src/mocks.cairo b/onchain/src/mocks.cairo
new file mode 100644
index 00000000..1cdba4b0
--- /dev/null
+++ b/onchain/src/mocks.cairo
@@ -0,0 +1 @@
+pub mod erc20_mock;
diff --git a/onchain/src/mocks/erc20_mock.cairo b/onchain/src/mocks/erc20_mock.cairo
new file mode 100644
index 00000000..8bd72de4
--- /dev/null
+++ b/onchain/src/mocks/erc20_mock.cairo
@@ -0,0 +1,42 @@
+//
+// https://github.com/OpenZeppelin/cairo-contracts/blob/main/src/tests/mocks/erc20_mocks.cairo
+//
+
+#[starknet::contract]
+pub mod SnakeERC20Mock {
+ use openzeppelin::token::erc20::ERC20Component;
+ use starknet::ContractAddress;
+
+ component!(path: ERC20Component, storage: erc20, event: ERC20Event);
+
+ #[abi(embed_v0)]
+ impl ERC20Impl = ERC20Component::ERC20Impl;
+ #[abi(embed_v0)]
+ impl ERC20MetadataImpl = ERC20Component::ERC20MetadataImpl;
+ impl InternalImpl = ERC20Component::InternalImpl;
+
+ #[storage]
+ struct Storage {
+ #[substorage(v0)]
+ erc20: ERC20Component::Storage
+ }
+
+ #[event]
+ #[derive(Drop, starknet::Event)]
+ enum Event {
+ #[flat]
+ ERC20Event: ERC20Component::Event
+ }
+
+ #[constructor]
+ fn constructor(
+ ref self: ContractState,
+ name: ByteArray,
+ symbol: ByteArray,
+ initial_supply: u256,
+ recipient: ContractAddress
+ ) {
+ self.erc20.initializer(name, symbol);
+ self.erc20._mint(recipient, initial_supply);
+ }
+}
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..bb8dcaec 100644
--- a/onchain/src/templates/component.cairo
+++ b/onchain/src/templates/component.cairo
@@ -1,6 +1,10 @@
#[starknet::component]
pub mod TemplateStoreComponent {
- use art_peace::templates::interface::{ITemplateStore, TemplateMetadata};
+ use core::num::traits::Zero;
+ use starknet::ContractAddress;
+
+ use art_peace::templates::interfaces::{ITemplateStore, TemplateMetadata};
+ use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait};
#[storage]
struct Storage {
@@ -54,11 +58,17 @@ pub mod TemplateStoreComponent {
// TODO: Return idx of the template?
fn add_template(
- ref self: ComponentState, template_metadata: TemplateMetadata
+ ref self: ComponentState, template_metadata: TemplateMetadata,
) {
let template_id = self.templates_count.read();
self.templates.write(template_id, template_metadata);
self.templates_count.write(template_id + 1);
+ self
+ ._deposit(
+ starknet::get_caller_address(),
+ template_metadata.reward_token,
+ template_metadata.reward
+ );
self.emit(TemplateAdded { id: template_id, metadata: template_metadata });
}
@@ -66,4 +76,27 @@ pub mod TemplateStoreComponent {
self.completed_templates.read(template_id)
}
}
+
+ #[generate_trait]
+ impl InternalImpl<
+ TContractState, +HasComponent
+ > of InternalTrait {
+ fn _deposit(
+ ref self: ComponentState,
+ template_proposer: ContractAddress,
+ reward_token: ContractAddress,
+ reward_amount: u256
+ ) {
+ let caller_address = starknet::get_caller_address();
+ let contract_address = starknet::get_contract_address();
+ assert(!template_proposer.is_zero(), 'Invalid caller');
+ assert(!reward_token.is_zero(), 'Invalid token');
+ let erc20_dispatcher = IERC20Dispatcher { contract_address: reward_token };
+ let allowance = erc20_dispatcher.allowance(caller_address, contract_address);
+ assert(allowance >= reward_amount, 'Insufficient allowance');
+ let success = erc20_dispatcher
+ .transfer_from(caller_address, contract_address, reward_amount);
+ assert(success, 'Transfer failed');
+ }
+ }
}
diff --git a/onchain/src/templates/interface.cairo b/onchain/src/templates/interfaces.cairo
similarity index 97%
rename from onchain/src/templates/interface.cairo
rename to onchain/src/templates/interfaces.cairo
index 44b9d7d1..a9403a0a 100644
--- a/onchain/src/templates/interface.cairo
+++ b/onchain/src/templates/interfaces.cairo
@@ -1,3 +1,5 @@
+use starknet::ContractAddress;
+
#[derive(Drop, Copy, Serde, starknet::Store)]
pub struct TemplateMetadata {
pub hash: felt252,
diff --git a/onchain/src/tests/art_peace.cairo b/onchain/src/tests/art_peace.cairo
index 8b21f2b3..10eba197 100644
--- a/onchain/src/tests/art_peace.cairo
+++ b/onchain/src/tests/art_peace.cairo
@@ -1,16 +1,26 @@
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
};
+use art_peace::mocks::erc20_mock::SnakeERC20Mock;
+use art_peace::tests::utils;
use core::poseidon::PoseidonTrait;
use core::hash::{HashStateTrait, HashStateExTrait};
+use openzeppelin::token::erc20::interface::{IERC20, IERC20Dispatcher, IERC20DispatcherTrait};
+use openzeppelin::token::erc721::interface::{IERC721Dispatcher, IERC721DispatcherTrait};
+
use snforge_std as snf;
use snforge_std::{CheatTarget, ContractClassTrait};
-use starknet::{ContractAddress, contract_address_const};
+use starknet::{ContractAddress, contract_address_const, get_contract_address};
const DAY_IN_SECONDS: u64 = consteval_int!(60 * 60 * 24);
const WIDTH: u128 = 100;
@@ -21,11 +31,33 @@ fn ART_PEACE_CONTRACT() -> ContractAddress {
contract_address_const::<'ArtPeace'>()
}
+fn ERC20_MOCK_CONTRACT() -> ContractAddress {
+ contract_address_const::<'erc20mock'>()
+}
+
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 +81,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();
@@ -57,9 +90,29 @@ fn deploy_contract() -> ContractAddress {
contract_addr
}
+fn deploy_erc20_mock() -> ContractAddress {
+ let contract = snf::declare("SnakeERC20Mock");
+ let name: ByteArray = "erc20 mock";
+ let symbol: ByteArray = "ERC20MOCK";
+ let initial_supply: u256 = 10 * utils::pow_256(10, 18);
+ let recipient: ContractAddress = get_contract_address();
+
+ let mut calldata: Array = array![];
+ Serde::serialize(@name, ref calldata);
+ Serde::serialize(@symbol, ref calldata);
+ Serde::serialize(@initial_supply, ref calldata);
+ Serde::serialize(@recipient, ref calldata);
+
+ let contract_addr = contract.deploy_at(@calldata, ERC20_MOCK_CONTRACT()).unwrap();
+
+ contract_addr
+}
+
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 +136,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 +145,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 +280,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 +297,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 +313,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 +336,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 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() == 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"
);
}
@@ -250,16 +394,13 @@ fn template_full_basic_test() {
assert!(template_store.get_templates_count() == 0, "Templates count is not 0");
+ let erc20_mock: ContractAddress = deploy_erc20_mock();
+
// 2x2 template image
let template_image = array![1, 2, 3, 4];
let template_hash = compute_template_hash(template_image.span());
let template_metadata = TemplateMetadata {
- hash: template_hash,
- position: 0,
- width: 2,
- height: 2,
- reward: 0,
- reward_token: contract_address_const::<0>(),
+ hash: template_hash, position: 0, width: 2, height: 2, reward: 0, reward_token: erc20_mock,
};
template_store.add_template(template_metadata);
@@ -347,6 +488,70 @@ fn increase_day_panic_test() {
snf::start_warp(CheatTarget::One(art_peace_address), DAY_IN_SECONDS - 1);
art_peace.increase_day_index();
}
+
+#[test]
+fn deposit_reward_test() {
+ let art_peace_address = deploy_contract();
+ let art_peace = IArtPeaceDispatcher { contract_address: art_peace_address };
+ let template_store = ITemplateStoreDispatcher { contract_address: art_peace.contract_address };
+
+ let erc20_mock: ContractAddress = deploy_erc20_mock();
+ let reward_amount: u256 = 1 * utils::pow_256(10, 18);
+
+ // 2x2 template image
+ let template_image = array![1, 2, 3, 4];
+ let template_hash = compute_template_hash(template_image.span());
+ let template_metadata = TemplateMetadata {
+ hash: template_hash,
+ position: 0,
+ width: 2,
+ height: 2,
+ reward: reward_amount,
+ reward_token: erc20_mock,
+ };
+
+ IERC20Dispatcher { contract_address: erc20_mock }.approve(art_peace_address, reward_amount);
+
+ template_store.add_template(template_metadata);
+}
+
// 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");
+}
diff --git a/onchain/src/tests/utils.cairo b/onchain/src/tests/utils.cairo
new file mode 100644
index 00000000..1d9a66bc
--- /dev/null
+++ b/onchain/src/tests/utils.cairo
@@ -0,0 +1,23 @@
+use core::num::traits::Zero;
+
+// Math
+pub(crate) fn pow_256(self: u256, mut exponent: u8) -> u256 {
+ if self.is_zero() {
+ return 0;
+ }
+ let mut result = 1;
+ let mut base = self;
+
+ loop {
+ if exponent & 1 == 1 {
+ result = result * base;
+ }
+
+ exponent = exponent / 2;
+ if exponent == 0 {
+ break result;
+ }
+
+ base = base * base;
+ }
+}
diff --git a/tests/integration/docker/deploy.sh b/tests/integration/docker/deploy.sh
index ada9913b..f517daa5 100755
--- a/tests/integration/docker/deploy.sh
+++ b/tests/integration/docker/deploy.sh
@@ -47,8 +47,8 @@ COLOR_COUNT=$(jq -r '.colors[]' $CANVAS_CONFIG | wc -l | tr -d ' ')
COLORS=$(jq -r '.colors[]' $CANVAS_CONFIG | sed 's/^/0x/')
END_TIME=3000000000
-# [WIDTH, HEIGHT, TIME_BETWEEN_PIXELS, COLOR_PALLETE_LEN, COLORS, END_TIME, DAILY_QUESTS_LEN, D AILY_QUESTS, DAILY_QUESTS_LEN, MAIN_QUESTS]
-CALLDATA=$(echo -n $WIDTH $HEIGHT $PLACE_DELAY $COLOR_COUNT $COLORS $END_TIME 0 0)
+# [WIDTH, HEIGHT, TIME_BETWEEN_PIXELS, COLOR_PALLETE_LEN, COLORS, END_TIME, DAILY_QUESTS_LEN, D AILY_QUESTS, DAILY_QUESTS_LEN, MAIN_QUESTS, NFT_CONTRACT]
+CALLDATA=$(echo -n $WIDTH $HEIGHT $PLACE_DELAY $COLOR_COUNT $COLORS $END_TIME 0 0 0)
# TODO: calldata passed as parameters
echo "Deploying contract \"$CLASS_NAME\"..."
diff --git a/tests/integration/local/deploy.sh b/tests/integration/local/deploy.sh
index 54d7f00a..cf398e4e 100755
--- a/tests/integration/local/deploy.sh
+++ b/tests/integration/local/deploy.sh
@@ -48,7 +48,7 @@ COLORS=$(jq -r '.colors[]' $CANVAS_CONFIG | sed 's/^/0x/')
END_TIME=3000000000
# [WIDTH, HEIGHT, TIME_BETWEEN_PIXELS, COLOR_PALLETE_LEN, COLORS, END_TIME, DAILY_QUESTS_LEN, DAILY_QUESTS, DAILY_QUESTS_LEN, MAIN_QUESTS]
-CALLDATA=$(echo -n $WIDTH $HEIGHT $PLACE_DELAY $COLOR_COUNT $COLORS $END_TIME 0 0)
+CALLDATA=$(echo -n $WIDTH $HEIGHT $PLACE_DELAY $COLOR_COUNT $COLORS $END_TIME 0 0 0)
echo "Calldata: $CALLDATA"
# TODO: calldata passed as parameters
echo "Deploying contract \"$CLASS_NAME\"..."