From 79452ececb3c908398264a6694645dc10195cb2d Mon Sep 17 00:00:00 2001 From: TAdev0 Date: Thu, 11 Apr 2024 23:28:37 +0200 Subject: [PATCH 01/11] deposit reward when adding new template --- onchain/Scarb.lock | 6 +++++ onchain/Scarb.toml | 1 + onchain/src/templates/component.cairo | 35 ++++++++++++++++++++++++++- onchain/src/templates/interface.cairo | 9 ++++++- onchain/src/tests/art_peace.cairo | 4 ++- 5 files changed, 52 insertions(+), 3 deletions(-) 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/templates/component.cairo b/onchain/src/templates/component.cairo index e8f658fa..a4491669 100644 --- a/onchain/src/templates/component.cairo +++ b/onchain/src/templates/component.cairo @@ -1,6 +1,11 @@ #[starknet::component] pub mod TemplateStoreComponent { + use core::num::traits::Zero; + use starknet::ContractAddress; + use art_peace::templates::interface::{ITemplateStore, TemplateMetadata}; + use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; + #[storage] struct Storage { @@ -54,11 +59,15 @@ pub mod TemplateStoreComponent { // TODO: Return idx of the template? fn add_template( - ref self: ComponentState, template_metadata: TemplateMetadata + ref self: ComponentState, + template_metadata: TemplateMetadata, + reward_token: ContractAddress, + reward_amount: u256 ) { 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(), reward_token, reward_amount); self.emit(TemplateAdded { id: template_id, metadata: template_metadata }); } @@ -66,4 +75,28 @@ 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'); + // Next line is commented for current test not to revert + // 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/interface.cairo index 44b9d7d1..54e67071 100644 --- a/onchain/src/templates/interface.cairo +++ b/onchain/src/templates/interface.cairo @@ -1,3 +1,5 @@ +use starknet::ContractAddress; + #[derive(Drop, Copy, Serde, starknet::Store)] pub struct TemplateMetadata { pub hash: felt252, @@ -18,7 +20,12 @@ pub trait ITemplateStore { fn get_template_hash(self: @TContractState, template_id: u32) -> felt252; // Stores a new template image into the contract state w/ metadata. // If the reward/token are set, then the contract escrows the reward for the template. - fn add_template(ref self: TContractState, template_metadata: TemplateMetadata); + fn add_template( + ref self: TContractState, + template_metadata: TemplateMetadata, + reward_token: ContractAddress, + reward_amount: u256 + ); // Returns whether the template is complete. fn is_template_complete(self: @TContractState, template_id: u32) -> bool; } diff --git a/onchain/src/tests/art_peace.cairo b/onchain/src/tests/art_peace.cairo index 8b21f2b3..2a584cef 100644 --- a/onchain/src/tests/art_peace.cairo +++ b/onchain/src/tests/art_peace.cairo @@ -262,7 +262,9 @@ fn template_full_basic_test() { reward_token: contract_address_const::<0>(), }; - template_store.add_template(template_metadata); + let reward_token: ContractAddress = contract_address_const::<0x0>(); + + template_store.add_template(template_metadata, reward_token, 0); assert!(template_store.get_templates_count() == 1, "Templates count is not 1"); assert!(template_store.get_template_hash(0) == template_hash, "Template hash is not correct"); From 6b187151d441f402615191bc4977ebc50c037bfd Mon Sep 17 00:00:00 2001 From: TAdev0 Date: Thu, 11 Apr 2024 23:31:24 +0200 Subject: [PATCH 02/11] fix --- onchain/src/templates/component.cairo | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/onchain/src/templates/component.cairo b/onchain/src/templates/component.cairo index a4491669..6fe15236 100644 --- a/onchain/src/templates/component.cairo +++ b/onchain/src/templates/component.cairo @@ -89,8 +89,7 @@ pub mod TemplateStoreComponent { let caller_address = starknet::get_caller_address(); let contract_address = starknet::get_contract_address(); assert(!template_proposer.is_zero(), 'Invalid caller'); - // Next line is commented for current test not to revert - // assert(!reward_token.is_zero(), 'Invalid token'); + 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'); From d7afe5afdb335fb7cb496017253dfa68f02ea4aa Mon Sep 17 00:00:00 2001 From: Brandon R <54774639+b-j-roberts@users.noreply.github.com> Date: Fri, 12 Apr 2024 10:31:18 -0500 Subject: [PATCH 03/11] [feat] NFTs module & Color pixel quests (#36) * NFTs module, minting from the board, and color based pixel quests * scarb fmt --- onchain/Scarb.lock | 6 + onchain/Scarb.toml | 1 + onchain/src/art_peace.cairo | 45 +++- .../src/{interface.cairo => interfaces.cairo} | 6 + onchain/src/lib.cairo | 21 +- onchain/src/nfts/canvas_nft.cairo | 70 +++++++ onchain/src/nfts/component.cairo | 49 +++++ onchain/src/nfts/interfaces.cairo | 39 ++++ onchain/src/quests/interfaces.cairo | 2 + onchain/src/quests/pixel_quest.cairo | 80 ++++--- onchain/src/templates/component.cairo | 2 +- .../{interface.cairo => interfaces.cairo} | 0 onchain/src/tests/art_peace.cairo | 195 ++++++++++++++++-- 13 files changed, 466 insertions(+), 50 deletions(-) rename onchain/src/{interface.cairo => interfaces.cairo} (93%) create mode 100644 onchain/src/nfts/canvas_nft.cairo create mode 100644 onchain/src/nfts/component.cairo create mode 100644 onchain/src/nfts/interfaces.cairo rename onchain/src/templates/{interface.cairo => interfaces.cairo} (100%) 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"); +} From 1497651c54268668a69b13ed24bb10e80e6f0e36 Mon Sep 17 00:00:00 2001 From: Abdulhakeem Abdulazeez Ayodeji <44169294+Ayoazeez26@users.noreply.github.com> Date: Fri, 12 Apr 2024 22:31:03 +0100 Subject: [PATCH 04/11] feat: display owner of selected pixel (#38) * feat: display owner of selected pixel * feat: removed console log * Panel layout fix --------- Co-authored-by: Brandon Roberts --- frontend/src/App.js | 5 +++-- frontend/src/canvas/Canvas.js | 3 +-- frontend/src/canvas/SelectedPixelPanel.css | 2 +- frontend/src/canvas/SelectedPixelPanel.js | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/src/App.js b/frontend/src/App.js index f42e38eb..ce387b3e 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -38,6 +38,7 @@ function App() { const [pixelSelectedMode, setPixelSelectedMode] = useState(false); const [selectedPositionX, setSelectedPositionX] = useState(null) const [selectedPositionY, setSelectedPositionY] = useState(null) + const [pixelPlacedBy, setPixelPlacedBy] = useState(""); const clearPixelSelection = () => { setSelectedPositionX(null); @@ -57,13 +58,13 @@ function App() { return (
- + { !isDesktopOrLaptop && ( logo )}
{ (!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}

); } From 86d3036a31266e56800228040b88cf7f01888089 Mon Sep 17 00:00:00 2001 From: "Trunks @ Carbonable" Date: Fri, 12 Apr 2024 23:53:58 +0200 Subject: [PATCH 05/11] [feat] Hash Template Images (#37) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ➕ add starknet dependencies * ✨ add pixel data hashing * Update Dockerfile to reflect change in go version --------- Co-authored-by: Brandon Roberts --- backend/Dockerfile | 2 +- backend/go.mod | 25 ++++++--- backend/go.sum | 102 ++++++++++++++++++++++++++++++++++-- backend/routes/templates.go | 16 ++++-- 4 files changed, 129 insertions(+), 16 deletions(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index 8570c68b..4d94b96d 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.21.7-alpine +FROM golang:1.22.2-alpine # TODO: Add psql to the image? # TODO: depends on in docker compose? diff --git a/backend/go.mod b/backend/go.mod index 648edc49..905f3e82 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -1,17 +1,30 @@ module art-peace-backend -go 1.21.7 +go 1.22.0 -require github.com/redis/go-redis/v9 v9.5.1 +toolchain go1.22.2 require ( + github.com/NethermindEth/juno v0.11.5 + github.com/gorilla/websocket v1.5.1 + github.com/jackc/pgx/v5 v5.5.5 + github.com/redis/go-redis/v9 v9.5.1 +) + +require ( + github.com/bits-and-blooms/bitset v1.13.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/consensys/bavard v0.1.13 // indirect + github.com/consensys/gnark-crypto v0.12.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/gorilla/websocket v1.5.1 // indirect + github.com/fxamacker/cbor/v2 v2.5.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect - github.com/jackc/pgx/v5 v5.5.5 // indirect - golang.org/x/crypto v0.17.0 // indirect - golang.org/x/net v0.17.0 // indirect + github.com/mmcloughlin/addchain v0.4.0 // indirect + github.com/x448/float16 v0.8.4 // indirect + golang.org/x/crypto v0.18.0 // indirect + golang.org/x/net v0.20.0 // indirect + golang.org/x/sys v0.16.0 // indirect golang.org/x/text v0.14.0 // indirect + rsc.io/tmplfunc v0.0.3 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index 30602caf..d6546ee3 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,27 +1,121 @@ +github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ= +github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/NethermindEth/juno v0.11.5 h1:Mgqgz0hqSHYqEiti9zaEc0dpgGNtZpvINQ3axp55JMk= +github.com/NethermindEth/juno v0.11.5/go.mod h1:Zc/Zh3OSmu3MTLPtnEehKRReyJ6PCwvUYXIh313PbMU= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE= +github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cockroachdb/errors v1.11.1 h1:xSEW75zKaKCWzR3OfxXUxgrk/NtT4G1MiOv5lWZazG8= +github.com/cockroachdb/errors v1.11.1/go.mod h1:8MUxA3Gi6b25tYlFEBGLf+D8aISL+M4MIpiWMSNRfxw= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/pebble v1.0.0 h1:WZWlV/s78glZbY2ylUITDOWSVBD3cLjcWPLRPFbHNYg= +github.com/cockroachdb/pebble v1.0.0/go.mod h1:bynZ3gvVyhlvjLI7PT6dmZ7g76xzJ7HpxfjgkzCGz6s= +github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= +github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/YjhQ= +github.com/consensys/bavard v0.1.13/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI= +github.com/consensys/gnark-crypto v0.12.1 h1:lHH39WuuFgVHONRl3J0LRBtuYdQTumFSDtJF7HpyG8M= +github.com/consensys/gnark-crypto v0.12.1/go.mod h1:v2Gy7L/4ZRosZ7Ivs+9SfUDr0f5UlG+EM5t7MPHiLuY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/ethereum/go-ethereum v1.13.10 h1:Ppdil79nN+Vc+mXfge0AuUgmKWuVv4eMqzoIVSdqZek= +github.com/ethereum/go-ethereum v1.13.10/go.mod h1:sc48XYQxCzH3fG9BcrXCOOgQk2JfZzNAmIKnceogzsA= +github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE= +github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= +github.com/getsentry/sentry-go v0.26.0 h1:IX3++sF6/4B5JcevhdZfdKIHfyvMmAq/UnqcyT2H6mA= +github.com/getsentry/sentry-go v0.26.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk= +github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU= +github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= +github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c= +github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8= +github.com/libp2p/go-libp2p v0.32.2 h1:s8GYN4YJzgUoyeYNPdW7JZeZ5Ee31iNaIBfGYMAY4FQ= +github.com/libp2p/go-libp2p v0.32.2/go.mod h1:E0LKe+diV/ZVJVnOJby8VC5xzHF0660osg71skcxJvk= +github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= +github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= +github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= +github.com/multiformats/go-multistream v0.5.0 h1:5htLSLl7lvJk3xx3qT/8Zm9J4K8vEOf/QGkvOGQAyiE= +github.com/multiformats/go-multistream v0.5.0/go.mod h1:n6tMZiwiP2wUsR8DgfDWw1dydlEqV3l6N3/GBsX6ILA= +github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= +github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= +github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y= +github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1BTn+g2pa/hQ= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8= github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA= +golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= +rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA= diff --git a/backend/routes/templates.go b/backend/routes/templates.go index 0cf004a4..48c1f4a1 100644 --- a/backend/routes/templates.go +++ b/backend/routes/templates.go @@ -5,11 +5,13 @@ import ( "fmt" "io" "io/ioutil" - "math/rand" "net/http" "os" "os/exec" + "github.com/NethermindEth/juno/core/crypto" + "github.com/NethermindEth/juno/core/felt" + "art-peace-backend/backend" ) @@ -21,10 +23,14 @@ func InitTemplateRoutes() { // TODO: Add specific location for template images -func hashTemplateImage(imageData []byte) string { - // TODO: Use poseidon hash to hash image data and return hash - // Make sure the hash matches the hash generated by the contract - return "hash" + string(rand.Intn(1000000)) +func hashTemplateImage(pixelData []byte) string { + var data[]*felt.Felt + for _, pixel := range pixelData { + f := new(felt.Felt).SetUint64(uint64(pixel)) + data = append(data, f) + } + hash := crypto.PoseidonArray(data...) + return hash.String() } func imageToPixelData(imageData []byte) []byte { From 175165982c1ebcf51c6ff8e6fe47b4f7fb0c66ae Mon Sep 17 00:00:00 2001 From: Brandon R <54774639+b-j-roberts@users.noreply.github.com> Date: Fri, 12 Apr 2024 17:10:14 -0500 Subject: [PATCH 06/11] Contrib: Add ayoazeez26 and tekkac (#39) * :busts_in_silhouette: Add @Ayoazeez26 as a contributor * :busts_in_silhouette: Add @tekkac as a contributor --- .all-contributorsrc | 18 ++++++++++++++++++ README.md | 2 ++ 2 files changed, 20 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 19754125..01de750d 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -36,6 +36,24 @@ "contributions": [ "code" ] + }, + { + "login": "Ayoazeez26", + "name": "Abdulhakeem Abdulazeez Ayodeji", + "avatar_url": "https://avatars.githubusercontent.com/u/44169294?v=4", + "profile": "https://github.com/Ayoazeez26", + "contributions": [ + "code" + ] + }, + { + "login": "tekkac", + "name": "Trunks @ Carbonable", + "avatar_url": "https://avatars.githubusercontent.com/u/98529704?v=4", + "profile": "https://github.com/tekkac", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index b2ff9c88..023cbd3f 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,8 @@ Thanks goes to these wonderful people. Follow the [contributors guide](https://g Brandon R
Brandon R

💻 Adeyemi Gbenga
Adeyemi Gbenga

💻 Tristan
Tristan

💻 + Abdulhakeem Abdulazeez Ayodeji
Abdulhakeem Abdulazeez Ayodeji

💻 + Trunks @ Carbonable
Trunks @ Carbonable

💻 From f2b6d1174803941b954bc44d85c208b0e76f06a2 Mon Sep 17 00:00:00 2001 From: Brandon R <54774639+b-j-roberts@users.noreply.github.com> Date: Fri, 12 Apr 2024 18:00:38 -0500 Subject: [PATCH 07/11] feat: Go formatting CI (#40) * Go formatting CI check * Main.go formatted * go format * Change go module name to github repo and patched issue in integration scripts due to new init param --- .github/workflows/build.yml | 3 + .gitignore | 2 +- backend/backend/backend.go | 33 ------ backend/backend/databases.go | 46 -------- backend/config/backend.go | 50 ++++----- backend/config/canvas.go | 62 +++++----- backend/config/database.go | 64 +++++------ backend/core/backend.go | 33 ++++++ backend/core/databases.go | 46 ++++++++ backend/go.mod | 2 +- backend/main.go | 24 ++-- backend/routes/canvas.go | 55 +++++---- backend/routes/contract.go | 26 ++--- backend/routes/indexer.go | 152 ++++++++++++------------- backend/routes/pixel.go | 146 ++++++++++++------------ backend/routes/routes.go | 14 +-- backend/routes/templates.go | 175 ++++++++++++++--------------- backend/routes/user.go | 26 ++--- backend/routes/websocket.go | 42 +++---- tests/integration/docker/deploy.sh | 4 +- tests/integration/local/deploy.sh | 2 +- 21 files changed, 504 insertions(+), 503 deletions(-) delete mode 100644 backend/backend/backend.go delete mode 100644 backend/backend/databases.go create mode 100644 backend/core/backend.go create mode 100644 backend/core/databases.go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 62078d64..e13890cf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,3 +18,6 @@ jobs: working-directory: onchain - run: scarb build working-directory: onchain + - name: go formatting ( gofmt -s -w . ) + run: if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then exit 1; fi + working-directory: backend diff --git a/.gitignore b/.gitignore index 06696aae..540c8fcd 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,4 @@ build/ node_modules/ # Backend -art-peace-backend +/backend/backend diff --git a/backend/backend/backend.go b/backend/backend/backend.go deleted file mode 100644 index 263f9423..00000000 --- a/backend/backend/backend.go +++ /dev/null @@ -1,33 +0,0 @@ -package backend - -import ( - "fmt" - "net/http" - - "github.com/gorilla/websocket" - - "art-peace-backend/config" -) - -type Backend struct { - Databases *Databases - WSConnections []*websocket.Conn - - CanvasConfig *config.CanvasConfig - BackendConfig *config.BackendConfig -} - -var ArtPeaceBackend *Backend - -func NewBackend(databases *Databases, canvasConfig *config.CanvasConfig, backendConfig *config.BackendConfig) *Backend { - return &Backend{ - Databases: databases, - CanvasConfig: canvasConfig, - BackendConfig: backendConfig, - } -} - -func (b *Backend) Start() { - fmt.Println("Listening on port", b.BackendConfig.Port) - http.ListenAndServe(fmt.Sprintf(":%d", b.BackendConfig.Port), nil) -} diff --git a/backend/backend/databases.go b/backend/backend/databases.go deleted file mode 100644 index bec6a67d..00000000 --- a/backend/backend/databases.go +++ /dev/null @@ -1,46 +0,0 @@ -package backend - -import ( - "context" - "os" - "strconv" - - "github.com/jackc/pgx/v5" - "github.com/redis/go-redis/v9" - - "art-peace-backend/config" -) - -type Databases struct { - DatabaseConfig *config.DatabaseConfig - - Redis *redis.Client - Postgres *pgx.Conn -} - -func NewDatabases(databaseConfig *config.DatabaseConfig) *Databases { - d := &Databases{} - d.DatabaseConfig = databaseConfig - - // Connect to Redis - d.Redis = redis.NewClient(&redis.Options{ - Addr: databaseConfig.Redis.Host + ":" + strconv.Itoa(databaseConfig.Redis.Port), - Password: "", // TODO: Read from env - DB: 0, - }) - - // Connect to Postgres - postgresConnString := "postgresql://" + databaseConfig.Postgres.User + ":" + os.Getenv("POSTGRES_PASSWORD") + "@" + databaseConfig.Postgres.Host + ":" + strconv.Itoa(databaseConfig.Postgres.Port) + "/" + databaseConfig.Postgres.Database - pgConn, err := pgx.Connect(context.Background(), postgresConnString) - if err != nil { - panic(err) - } - d.Postgres = pgConn - - return d -} - -func (d *Databases) Close() { - d.Redis.Close() - d.Postgres.Close(context.Background()) -} diff --git a/backend/config/backend.go b/backend/config/backend.go index 6294c791..d14c7975 100644 --- a/backend/config/backend.go +++ b/backend/config/backend.go @@ -6,40 +6,40 @@ import ( ) type BackendScriptsConfig struct { - PlacePixelDevnet string `json:"place_pixel_devnet"` - AddTemplateHashDevnet string `json:"add_template_hash_devnet"` + PlacePixelDevnet string `json:"place_pixel_devnet"` + AddTemplateHashDevnet string `json:"add_template_hash_devnet"` } type BackendConfig struct { - Host string `json:"host"` - Port int `json:"port"` - Scripts BackendScriptsConfig `json:"scripts"` + Host string `json:"host"` + Port int `json:"port"` + Scripts BackendScriptsConfig `json:"scripts"` } var DefaultBackendConfig = BackendConfig{ - Host: "localhost", - Port: 8080, - Scripts: BackendScriptsConfig{ - PlacePixelDevnet: "../scripts/place_pixel.sh", - AddTemplateHashDevnet: "../scripts/add_template_hash.sh", - }, + Host: "localhost", + Port: 8080, + Scripts: BackendScriptsConfig{ + PlacePixelDevnet: "../scripts/place_pixel.sh", + AddTemplateHashDevnet: "../scripts/add_template_hash.sh", + }, } var DefaultBackendConfigPath = "../configs/backend.config.json" func LoadBackendConfig(backendConfigPath string) (*BackendConfig, error) { - file, err := os.Open(backendConfigPath) - if err != nil { - return nil, err - } - defer file.Close() - - decoder := json.NewDecoder(file) - config := BackendConfig{} - err = decoder.Decode(&config) - if err != nil { - return nil, err - } - - return &config, nil + file, err := os.Open(backendConfigPath) + if err != nil { + return nil, err + } + defer file.Close() + + decoder := json.NewDecoder(file) + config := BackendConfig{} + err = decoder.Decode(&config) + if err != nil { + return nil, err + } + + return &config, nil } diff --git a/backend/config/canvas.go b/backend/config/canvas.go index b3d0d6dd..fba87167 100644 --- a/backend/config/canvas.go +++ b/backend/config/canvas.go @@ -6,49 +6,49 @@ import ( ) type CanvasSize struct { - Width uint `json:"width"` - Height uint `json:"height"` + Width uint `json:"width"` + Height uint `json:"height"` } type CanvasConfig struct { - Canvas CanvasSize `json:"canvas"` - Colors []string `json:"colors"` - ColorsBitWidth uint `json:"colors_bitwidth"` + Canvas CanvasSize `json:"canvas"` + Colors []string `json:"colors"` + ColorsBitWidth uint `json:"colors_bitwidth"` } var DefaultCanvasConfig = &CanvasConfig{ - Canvas: CanvasSize{ - Width: 100, - Height: 100, - }, - Colors: []string{ - "#000000", - "#FFFFFF", - "#FF0000", - "#00FF00", - "#0000FF", - "#FFFF00", - "#FF00FF", - "#00FFFF", - }, - ColorsBitWidth: 5, + Canvas: CanvasSize{ + Width: 100, + Height: 100, + }, + Colors: []string{ + "#000000", + "#FFFFFF", + "#FF0000", + "#00FF00", + "#0000FF", + "#FFFF00", + "#FF00FF", + "#00FFFF", + }, + ColorsBitWidth: 5, } var DefaultCanvasConfigPath = "../configs/canvas.config.json" func LoadCanvasConfig(canvasConfigPath string) (*CanvasConfig, error) { - canvasConfig := &CanvasConfig{} + canvasConfig := &CanvasConfig{} - canvasConfigFile, err := os.Open(canvasConfigPath) - if err != nil { - return nil, err - } - defer canvasConfigFile.Close() + canvasConfigFile, err := os.Open(canvasConfigPath) + if err != nil { + return nil, err + } + defer canvasConfigFile.Close() - jsonParser := json.NewDecoder(canvasConfigFile) - if err = jsonParser.Decode(canvasConfig); err != nil { - return nil, err - } + jsonParser := json.NewDecoder(canvasConfigFile) + if err = jsonParser.Decode(canvasConfig); err != nil { + return nil, err + } - return canvasConfig, nil + return canvasConfig, nil } diff --git a/backend/config/database.go b/backend/config/database.go index 922abb66..ff88c714 100644 --- a/backend/config/database.go +++ b/backend/config/database.go @@ -6,50 +6,50 @@ import ( ) type RedisConfig struct { - Host string `json:"host"` - Port int `json:"port"` + Host string `json:"host"` + Port int `json:"port"` } type PostgresConfig struct { - Host string `json:"host"` - Port int `json:"port"` - User string `json:"user"` - Database string `json:"database"` + Host string `json:"host"` + Port int `json:"port"` + User string `json:"user"` + Database string `json:"database"` } type DatabaseConfig struct { - Redis RedisConfig `json:"redis"` - Postgres PostgresConfig `json:"postgres"` + Redis RedisConfig `json:"redis"` + Postgres PostgresConfig `json:"postgres"` } var DefaultDatabaseConfig = DatabaseConfig{ - Redis: RedisConfig{ - Host: "localhost", - Port: 6379, - }, - Postgres: PostgresConfig{ - Host: "localhost", - Port: 5432, - User: "art-peace-user", - Database: "art-peace-db", - }, + Redis: RedisConfig{ + Host: "localhost", + Port: 6379, + }, + Postgres: PostgresConfig{ + Host: "localhost", + Port: 5432, + User: "art-peace-user", + Database: "art-peace-db", + }, } var DefaultDatabaseConfigPath = "../configs/database.config.json" func LoadDatabaseConfig(databaseConfigPath string) (*DatabaseConfig, error) { - file, err := os.Open(databaseConfigPath) - if err != nil { - return nil, err - } - defer file.Close() - - decoder := json.NewDecoder(file) - config := DatabaseConfig{} - err = decoder.Decode(&config) - if err != nil { - return nil, err - } - - return &config, nil + file, err := os.Open(databaseConfigPath) + if err != nil { + return nil, err + } + defer file.Close() + + decoder := json.NewDecoder(file) + config := DatabaseConfig{} + err = decoder.Decode(&config) + if err != nil { + return nil, err + } + + return &config, nil } diff --git a/backend/core/backend.go b/backend/core/backend.go new file mode 100644 index 00000000..9625ded8 --- /dev/null +++ b/backend/core/backend.go @@ -0,0 +1,33 @@ +package core + +import ( + "fmt" + "net/http" + + "github.com/gorilla/websocket" + + "github.com/keep-starknet-strange/art-peace/backend/config" +) + +type Backend struct { + Databases *Databases + WSConnections []*websocket.Conn + + CanvasConfig *config.CanvasConfig + BackendConfig *config.BackendConfig +} + +var ArtPeaceBackend *Backend + +func NewBackend(databases *Databases, canvasConfig *config.CanvasConfig, backendConfig *config.BackendConfig) *Backend { + return &Backend{ + Databases: databases, + CanvasConfig: canvasConfig, + BackendConfig: backendConfig, + } +} + +func (b *Backend) Start() { + fmt.Println("Listening on port", b.BackendConfig.Port) + http.ListenAndServe(fmt.Sprintf(":%d", b.BackendConfig.Port), nil) +} diff --git a/backend/core/databases.go b/backend/core/databases.go new file mode 100644 index 00000000..a53d59b0 --- /dev/null +++ b/backend/core/databases.go @@ -0,0 +1,46 @@ +package core + +import ( + "context" + "os" + "strconv" + + "github.com/jackc/pgx/v5" + "github.com/redis/go-redis/v9" + + "github.com/keep-starknet-strange/art-peace/backend/config" +) + +type Databases struct { + DatabaseConfig *config.DatabaseConfig + + Redis *redis.Client + Postgres *pgx.Conn +} + +func NewDatabases(databaseConfig *config.DatabaseConfig) *Databases { + d := &Databases{} + d.DatabaseConfig = databaseConfig + + // Connect to Redis + d.Redis = redis.NewClient(&redis.Options{ + Addr: databaseConfig.Redis.Host + ":" + strconv.Itoa(databaseConfig.Redis.Port), + Password: "", // TODO: Read from env + DB: 0, + }) + + // Connect to Postgres + postgresConnString := "postgresql://" + databaseConfig.Postgres.User + ":" + os.Getenv("POSTGRES_PASSWORD") + "@" + databaseConfig.Postgres.Host + ":" + strconv.Itoa(databaseConfig.Postgres.Port) + "/" + databaseConfig.Postgres.Database + pgConn, err := pgx.Connect(context.Background(), postgresConnString) + if err != nil { + panic(err) + } + d.Postgres = pgConn + + return d +} + +func (d *Databases) Close() { + d.Redis.Close() + d.Postgres.Close(context.Background()) +} diff --git a/backend/go.mod b/backend/go.mod index 905f3e82..b25549f6 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -1,4 +1,4 @@ -module art-peace-backend +module github.com/keep-starknet-strange/art-peace/backend go 1.22.0 diff --git a/backend/main.go b/backend/main.go index c3948bb7..1b03b991 100644 --- a/backend/main.go +++ b/backend/main.go @@ -3,15 +3,15 @@ package main import ( "flag" - "art-peace-backend/backend" - "art-peace-backend/config" - "art-peace-backend/routes" + "github.com/keep-starknet-strange/art-peace/backend/config" + "github.com/keep-starknet-strange/art-peace/backend/core" + "github.com/keep-starknet-strange/art-peace/backend/routes" ) func main() { canvasConfigFilename := flag.String("canvas-config", config.DefaultCanvasConfigPath, "Canvas config file") databaseConfigFilename := flag.String("database-config", config.DefaultDatabaseConfigPath, "Database config file") - backendConfigFilename := flag.String("backend-config", config.DefaultBackendConfigPath, "Backend config file") + backendConfigFilename := flag.String("backend-config", config.DefaultBackendConfigPath, "Backend config file") flag.Parse() canvasConfig, err := config.LoadCanvasConfig(*canvasConfigFilename) @@ -24,16 +24,16 @@ func main() { panic(err) } - backendConfig, err := config.LoadBackendConfig(*backendConfigFilename) - if err != nil { - panic(err) - } + backendConfig, err := config.LoadBackendConfig(*backendConfigFilename) + if err != nil { + panic(err) + } - databases := backend.NewDatabases(databaseConfig) + databases := core.NewDatabases(databaseConfig) defer databases.Close() - routes.InitRoutes() + routes.InitRoutes() - backend.ArtPeaceBackend = backend.NewBackend(databases, canvasConfig, backendConfig) - backend.ArtPeaceBackend.Start() + core.ArtPeaceBackend = core.NewBackend(databases, canvasConfig, backendConfig) + core.ArtPeaceBackend.Start() } diff --git a/backend/routes/canvas.go b/backend/routes/canvas.go index b70dd27b..22e8a2f0 100644 --- a/backend/routes/canvas.go +++ b/backend/routes/canvas.go @@ -5,41 +5,40 @@ import ( "fmt" "net/http" - "art-peace-backend/backend" + "github.com/keep-starknet-strange/art-peace/backend/core" ) func InitCanvasRoutes() { - http.HandleFunc("/initCanvas", initCanvas) - http.HandleFunc("/getCanvas", getCanvas) + http.HandleFunc("/initCanvas", initCanvas) + http.HandleFunc("/getCanvas", getCanvas) } func initCanvas(w http.ResponseWriter, r *http.Request) { - // TODO: Check if canvas already exists - totalBitSize := backend.ArtPeaceBackend.CanvasConfig.Canvas.Width * backend.ArtPeaceBackend.CanvasConfig.Canvas.Height * backend.ArtPeaceBackend.CanvasConfig.ColorsBitWidth - totalByteSize := (totalBitSize / 8) - if totalBitSize % 8 != 0 { - // Round up to nearest byte - totalByteSize += 1 - } - - canvas := make([]byte, totalByteSize) - ctx := context.Background() - err := backend.ArtPeaceBackend.Databases.Redis.Set(ctx, "canvas", canvas, 0).Err() - if err != nil { - panic(err) - } - - fmt.Println("Canvas initialized") + // TODO: Check if canvas already exists + totalBitSize := core.ArtPeaceBackend.CanvasConfig.Canvas.Width * core.ArtPeaceBackend.CanvasConfig.Canvas.Height * core.ArtPeaceBackend.CanvasConfig.ColorsBitWidth + totalByteSize := (totalBitSize / 8) + if totalBitSize%8 != 0 { + // Round up to nearest byte + totalByteSize += 1 + } + + canvas := make([]byte, totalByteSize) + ctx := context.Background() + err := core.ArtPeaceBackend.Databases.Redis.Set(ctx, "canvas", canvas, 0).Err() + if err != nil { + panic(err) + } + + fmt.Println("Canvas initialized") } - func getCanvas(w http.ResponseWriter, r *http.Request) { - ctx := context.Background() - val, err := backend.ArtPeaceBackend.Databases.Redis.Get(ctx, "canvas").Result() - if err != nil { - panic(err) - } - - w.Header().Set("Access-Control-Allow-Origin", "*") - w.Write([]byte(val)) + ctx := context.Background() + val, err := core.ArtPeaceBackend.Databases.Redis.Get(ctx, "canvas").Result() + if err != nil { + panic(err) + } + + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Write([]byte(val)) } diff --git a/backend/routes/contract.go b/backend/routes/contract.go index 532f9b04..d01f6dd6 100644 --- a/backend/routes/contract.go +++ b/backend/routes/contract.go @@ -7,23 +7,23 @@ import ( ) func InitContractRoutes() { - http.HandleFunc("/getContractAddress", getContractAddress) - http.HandleFunc("/setContractAddress", setContractAddress) + http.HandleFunc("/getContractAddress", getContractAddress) + http.HandleFunc("/setContractAddress", setContractAddress) } func getContractAddress(w http.ResponseWriter, r *http.Request) { - contractAddress := os.Getenv("ART_PEACE_CONTRACT_ADDRESS") - w.Write([]byte(contractAddress)) + contractAddress := os.Getenv("ART_PEACE_CONTRACT_ADDRESS") + w.Write([]byte(contractAddress)) } func setContractAddress(w http.ResponseWriter, r *http.Request) { - // TODO: Add authentication - data, err := ioutil.ReadAll(r.Body) - if err != nil { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte("Invalid request")) - return - } - os.Setenv("ART_PEACE_CONTRACT_ADDRESS", string(data)) - w.Write([]byte("Contract address set successfully")) + // TODO: Add authentication + data, err := ioutil.ReadAll(r.Body) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("Invalid request")) + return + } + os.Setenv("ART_PEACE_CONTRACT_ADDRESS", string(data)) + w.Write([]byte("Contract address set successfully")) } diff --git a/backend/routes/indexer.go b/backend/routes/indexer.go index d4a91231..a137412f 100644 --- a/backend/routes/indexer.go +++ b/backend/routes/indexer.go @@ -10,11 +10,11 @@ import ( "github.com/gorilla/websocket" - "art-peace-backend/backend" + "github.com/keep-starknet-strange/art-peace/backend/core" ) func InitIndexerRoutes() { - http.HandleFunc("/consumeIndexerMsg", consumeIndexerMsg) + http.HandleFunc("/consumeIndexerMsg", consumeIndexerMsg) } // TODO: Clean up @@ -57,86 +57,86 @@ func InitIndexerRoutes() { // TODO: User might miss some messages between loading canvas and connecting to websocket? func consumeIndexerMsg(w http.ResponseWriter, r *http.Request) { - requestBody, err := io.ReadAll(r.Body) - if err != nil { - fmt.Println("Error reading request body: ", err) - w.WriteHeader(http.StatusInternalServerError) - return - } + requestBody, err := io.ReadAll(r.Body) + if err != nil { + fmt.Println("Error reading request body: ", err) + w.WriteHeader(http.StatusInternalServerError) + return + } - // TODO: Parse message fully, check block status, number, ... - reqBody := map[string]interface{}{} - err = json.Unmarshal(requestBody, &reqBody) - if err != nil { - fmt.Println("Error unmarshalling request body: ", err) - w.WriteHeader(http.StatusInternalServerError) - return - } + // TODO: Parse message fully, check block status, number, ... + reqBody := map[string]interface{}{} + err = json.Unmarshal(requestBody, &reqBody) + if err != nil { + fmt.Println("Error unmarshalling request body: ", err) + w.WriteHeader(http.StatusInternalServerError) + return + } - address := reqBody["data"].(map[string]interface{})["batch"].([]interface{})[0].(map[string]interface{})["events"].([]interface{})[0].(map[string]interface{})["event"].(map[string]interface{})["keys"].([]interface{})[1] - address = address.(string)[2:] - posHex := reqBody["data"].(map[string]interface{})["batch"].([]interface{})[0].(map[string]interface{})["events"].([]interface{})[0].(map[string]interface{})["event"].(map[string]interface{})["keys"].([]interface{})[2] - dayIdxHex := reqBody["data"].(map[string]interface{})["batch"].([]interface{})[0].(map[string]interface{})["events"].([]interface{})[0].(map[string]interface{})["event"].(map[string]interface{})["keys"].([]interface{})[3] - colorHex := reqBody["data"].(map[string]interface{})["batch"].([]interface{})[0].(map[string]interface{})["events"].([]interface{})[0].(map[string]interface{})["event"].(map[string]interface{})["data"].([]interface{})[0] + address := reqBody["data"].(map[string]interface{})["batch"].([]interface{})[0].(map[string]interface{})["events"].([]interface{})[0].(map[string]interface{})["event"].(map[string]interface{})["keys"].([]interface{})[1] + address = address.(string)[2:] + posHex := reqBody["data"].(map[string]interface{})["batch"].([]interface{})[0].(map[string]interface{})["events"].([]interface{})[0].(map[string]interface{})["event"].(map[string]interface{})["keys"].([]interface{})[2] + dayIdxHex := reqBody["data"].(map[string]interface{})["batch"].([]interface{})[0].(map[string]interface{})["events"].([]interface{})[0].(map[string]interface{})["event"].(map[string]interface{})["keys"].([]interface{})[3] + colorHex := reqBody["data"].(map[string]interface{})["batch"].([]interface{})[0].(map[string]interface{})["events"].([]interface{})[0].(map[string]interface{})["event"].(map[string]interface{})["data"].([]interface{})[0] - // Convert hex to int - position, err := strconv.ParseInt(posHex.(string), 0, 64) - if err != nil { - fmt.Println("Error converting position hex to int: ", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - dayIdx, err := strconv.ParseInt(dayIdxHex.(string), 0, 64) - if err != nil { - fmt.Println("Error converting day index hex to int: ", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - color, err := strconv.ParseInt(colorHex.(string), 0, 64) - if err != nil { - fmt.Println("Error converting color hex to int: ", err) - w.WriteHeader(http.StatusInternalServerError) - return - } + // Convert hex to int + position, err := strconv.ParseInt(posHex.(string), 0, 64) + if err != nil { + fmt.Println("Error converting position hex to int: ", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + dayIdx, err := strconv.ParseInt(dayIdxHex.(string), 0, 64) + if err != nil { + fmt.Println("Error converting day index hex to int: ", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + color, err := strconv.ParseInt(colorHex.(string), 0, 64) + if err != nil { + fmt.Println("Error converting color hex to int: ", err) + w.WriteHeader(http.StatusInternalServerError) + return + } - bitfieldType := "u" + strconv.Itoa(int(backend.ArtPeaceBackend.CanvasConfig.ColorsBitWidth)) - pos := uint(position) * backend.ArtPeaceBackend.CanvasConfig.ColorsBitWidth + bitfieldType := "u" + strconv.Itoa(int(core.ArtPeaceBackend.CanvasConfig.ColorsBitWidth)) + pos := uint(position) * core.ArtPeaceBackend.CanvasConfig.ColorsBitWidth - fmt.Println("Pixel indexed with position: ", position, " and color: ", color) + fmt.Println("Pixel indexed with position: ", position, " and color: ", color) - // Set pixel in redis - ctx := context.Background() - err = backend.ArtPeaceBackend.Databases.Redis.BitField(ctx, "canvas", "SET", bitfieldType, pos, color).Err() - if err != nil { - panic(err) - } + // Set pixel in redis + ctx := context.Background() + err = core.ArtPeaceBackend.Databases.Redis.BitField(ctx, "canvas", "SET", bitfieldType, pos, color).Err() + if err != nil { + panic(err) + } - // Set pixel in postgres - _, err = backend.ArtPeaceBackend.Databases.Postgres.Exec(context.Background(), "INSERT INTO Pixels (address, position, day, color) VALUES ($1, $2, $3, $4)", address, position, dayIdx, color) - if err != nil { - fmt.Println("Error inserting pixel into postgres: ", err) - w.WriteHeader(http.StatusInternalServerError) - return - } + // Set pixel in postgres + _, err = core.ArtPeaceBackend.Databases.Postgres.Exec(context.Background(), "INSERT INTO Pixels (address, position, day, color) VALUES ($1, $2, $3, $4)", address, position, dayIdx, color) + if err != nil { + fmt.Println("Error inserting pixel into postgres: ", err) + w.WriteHeader(http.StatusInternalServerError) + return + } - // Send message to all connected clients - var message = map[string]interface{}{ - "position": position, - "color": color, - } - messageBytes, err := json.Marshal(message) - if err != nil { - fmt.Println("Error marshalling message: ", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - for idx, conn := range backend.ArtPeaceBackend.WSConnections { - if err := conn.WriteMessage(websocket.TextMessage, messageBytes); err != nil { - fmt.Println(err) - // TODO: Should we always remove connection? - // Remove connection - conn.Close() - backend.ArtPeaceBackend.WSConnections = append(backend.ArtPeaceBackend.WSConnections[:idx], backend.ArtPeaceBackend.WSConnections[idx+1:]...) - } - } + // Send message to all connected clients + var message = map[string]interface{}{ + "position": position, + "color": color, + } + messageBytes, err := json.Marshal(message) + if err != nil { + fmt.Println("Error marshalling message: ", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + for idx, conn := range core.ArtPeaceBackend.WSConnections { + if err := conn.WriteMessage(websocket.TextMessage, messageBytes); err != nil { + fmt.Println(err) + // TODO: Should we always remove connection? + // Remove connection + conn.Close() + core.ArtPeaceBackend.WSConnections = append(core.ArtPeaceBackend.WSConnections[:idx], core.ArtPeaceBackend.WSConnections[idx+1:]...) + } + } } diff --git a/backend/routes/pixel.go b/backend/routes/pixel.go index 99560601..59656372 100644 --- a/backend/routes/pixel.go +++ b/backend/routes/pixel.go @@ -10,99 +10,99 @@ import ( "os/exec" "strconv" - "art-peace-backend/backend" + "github.com/keep-starknet-strange/art-peace/backend/core" ) func InitPixelRoutes() { - http.HandleFunc("/getPixel", getPixel) - http.HandleFunc("/getPixelInfo", getPixelInfo) - http.HandleFunc("/placePixelDevnet", placePixelDevnet) - http.HandleFunc("/placePixelRedis", placePixelRedis) + http.HandleFunc("/getPixel", getPixel) + http.HandleFunc("/getPixelInfo", getPixelInfo) + http.HandleFunc("/placePixelDevnet", placePixelDevnet) + http.HandleFunc("/placePixelRedis", placePixelRedis) } func getPixel(w http.ResponseWriter, r *http.Request) { - position, err := strconv.Atoi(r.URL.Query().Get("position")) - if err != nil { - // TODO: panic or return error? - panic(err) - } - bitfieldType := "u" + strconv.Itoa(int(backend.ArtPeaceBackend.CanvasConfig.ColorsBitWidth)) - pos := uint(position) * backend.ArtPeaceBackend.CanvasConfig.ColorsBitWidth + position, err := strconv.Atoi(r.URL.Query().Get("position")) + if err != nil { + // TODO: panic or return error? + panic(err) + } + bitfieldType := "u" + strconv.Itoa(int(core.ArtPeaceBackend.CanvasConfig.ColorsBitWidth)) + pos := uint(position) * core.ArtPeaceBackend.CanvasConfig.ColorsBitWidth - ctx := context.Background() - val, err := backend.ArtPeaceBackend.Databases.Redis.BitField(ctx, "canvas", "GET", bitfieldType, pos).Result() - if err != nil { - panic(err) - } + ctx := context.Background() + val, err := core.ArtPeaceBackend.Databases.Redis.BitField(ctx, "canvas", "GET", bitfieldType, pos).Result() + if err != nil { + panic(err) + } - w.Header().Set("Access-Control-Allow-Origin", "*") - // TODO: Check this - w.Write([]byte(strconv.Itoa(int(val[0])))) + w.Header().Set("Access-Control-Allow-Origin", "*") + // TODO: Check this + w.Write([]byte(strconv.Itoa(int(val[0])))) } func getPixelInfo(w http.ResponseWriter, r *http.Request) { - position := r.URL.Query().Get("position") - w.Header().Set("Access-Control-Allow-Origin", "*") + position := r.URL.Query().Get("position") + w.Header().Set("Access-Control-Allow-Origin", "*") - // Get pixel info from postgres - var address string - err := backend.ArtPeaceBackend.Databases.Postgres.QueryRow(context.Background(), "SELECT address FROM Pixels WHERE position = $1 ORDER BY time DESC LIMIT 1", position).Scan(&address) - if err != nil { - w.Write([]byte("0000000000000000000000000000000000000000000000000000000000000000")) - } else { - w.Write([]byte(address)) - } + // Get pixel info from postgres + var address string + err := core.ArtPeaceBackend.Databases.Postgres.QueryRow(context.Background(), "SELECT address FROM Pixels WHERE position = $1 ORDER BY time DESC LIMIT 1", position).Scan(&address) + if err != nil { + w.Write([]byte("0000000000000000000000000000000000000000000000000000000000000000")) + } else { + w.Write([]byte(address)) + } } func placePixelDevnet(w http.ResponseWriter, r *http.Request) { - reqBody, err := io.ReadAll(r.Body) - if err != nil { - panic(err) - } - var jsonBody map[string]string - err = json.Unmarshal(reqBody, &jsonBody) - if err != nil { - panic(err) - } + reqBody, err := io.ReadAll(r.Body) + if err != nil { + panic(err) + } + var jsonBody map[string]string + err = json.Unmarshal(reqBody, &jsonBody) + if err != nil { + panic(err) + } - position, err := strconv.Atoi(jsonBody["position"]) - if err != nil { - panic(err) - } + position, err := strconv.Atoi(jsonBody["position"]) + if err != nil { + panic(err) + } - shellCmd := backend.ArtPeaceBackend.BackendConfig.Scripts.PlacePixelDevnet - contract := os.Getenv("ART_PEACE_CONTRACT_ADDRESS") - - cmd := exec.Command(shellCmd, contract, "place_pixel", strconv.Itoa(position), jsonBody["color"]) - _, err = cmd.Output() - if err != nil { - fmt.Println("Error executing shell command: ", err) - panic(err) - } + shellCmd := core.ArtPeaceBackend.BackendConfig.Scripts.PlacePixelDevnet + contract := os.Getenv("ART_PEACE_CONTRACT_ADDRESS") - w.Header().Set("Access-Control-Allow-Origin", "*") - w.Write([]byte("Pixel placed")) + cmd := exec.Command(shellCmd, contract, "place_pixel", strconv.Itoa(position), jsonBody["color"]) + _, err = cmd.Output() + if err != nil { + fmt.Println("Error executing shell command: ", err) + panic(err) + } + + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Write([]byte("Pixel placed")) } func placePixelRedis(w http.ResponseWriter, r *http.Request) { - // TODO: Only allow mods to place pixels on redis instance - reqBody, err := io.ReadAll(r.Body) - if err != nil { - panic(err) - } - var jsonBody map[string]uint - err = json.Unmarshal(reqBody, &jsonBody) - if err != nil { - panic(err) - } - position := jsonBody["position"] - color := jsonBody["color"] - bitfieldType := "u" + strconv.Itoa(int(backend.ArtPeaceBackend.CanvasConfig.ColorsBitWidth)) - pos := position * backend.ArtPeaceBackend.CanvasConfig.ColorsBitWidth + // TODO: Only allow mods to place pixels on redis instance + reqBody, err := io.ReadAll(r.Body) + if err != nil { + panic(err) + } + var jsonBody map[string]uint + err = json.Unmarshal(reqBody, &jsonBody) + if err != nil { + panic(err) + } + position := jsonBody["position"] + color := jsonBody["color"] + bitfieldType := "u" + strconv.Itoa(int(core.ArtPeaceBackend.CanvasConfig.ColorsBitWidth)) + pos := position * core.ArtPeaceBackend.CanvasConfig.ColorsBitWidth - ctx := context.Background() - err = backend.ArtPeaceBackend.Databases.Redis.BitField(ctx, "canvas", "SET", bitfieldType, pos, color).Err() - if err != nil { - panic(err) - } + ctx := context.Background() + err = core.ArtPeaceBackend.Databases.Redis.BitField(ctx, "canvas", "SET", bitfieldType, pos, color).Err() + if err != nil { + panic(err) + } } diff --git a/backend/routes/routes.go b/backend/routes/routes.go index 9ae3fe2d..67bdf249 100644 --- a/backend/routes/routes.go +++ b/backend/routes/routes.go @@ -1,11 +1,11 @@ package routes func InitRoutes() { - InitIndexerRoutes() - InitCanvasRoutes() - InitPixelRoutes() - InitWebsocketRoutes() - InitTemplateRoutes() - InitUserRoutes() - InitContractRoutes() + InitIndexerRoutes() + InitCanvasRoutes() + InitPixelRoutes() + InitWebsocketRoutes() + InitTemplateRoutes() + InitUserRoutes() + InitContractRoutes() } diff --git a/backend/routes/templates.go b/backend/routes/templates.go index 48c1f4a1..d1717970 100644 --- a/backend/routes/templates.go +++ b/backend/routes/templates.go @@ -9,113 +9,112 @@ import ( "os" "os/exec" - "github.com/NethermindEth/juno/core/crypto" - "github.com/NethermindEth/juno/core/felt" + "github.com/NethermindEth/juno/core/crypto" + "github.com/NethermindEth/juno/core/felt" - "art-peace-backend/backend" + "github.com/keep-starknet-strange/art-peace/backend/core" ) func InitTemplateRoutes() { - http.HandleFunc("/addTemplateImg", addTemplateImg) - http.HandleFunc("/addTemplateData", addTemplateData) - http.HandleFunc("/addTemplateHashDevnet", addTemplateHashDevnet) + http.HandleFunc("/addTemplateImg", addTemplateImg) + http.HandleFunc("/addTemplateData", addTemplateData) + http.HandleFunc("/addTemplateHashDevnet", addTemplateHashDevnet) } // TODO: Add specific location for template images func hashTemplateImage(pixelData []byte) string { - var data[]*felt.Felt - for _, pixel := range pixelData { - f := new(felt.Felt).SetUint64(uint64(pixel)) - data = append(data, f) - } - hash := crypto.PoseidonArray(data...) - return hash.String() + var data []*felt.Felt + for _, pixel := range pixelData { + f := new(felt.Felt).SetUint64(uint64(pixel)) + data = append(data, f) + } + hash := crypto.PoseidonArray(data...) + return hash.String() } func imageToPixelData(imageData []byte) []byte { - // TODO: Convert image data to pixel data using approximation - // Output should be a byte array with color indexes - return []byte{0, 1, 1, 2, 2, 3} + // TODO: Convert image data to pixel data using approximation + // Output should be a byte array with color indexes + return []byte{0, 1, 1, 2, 2, 3} } func addTemplateImg(w http.ResponseWriter, r *http.Request) { - // TODO: Limit file size / proportions between 5x5 and 64x64 - // Passed like this curl -F "image=@art-peace-low-res-goose.jpg" http://localhost:8080/addTemplateImg - file, _, err := r.FormFile("image") - if err != nil { - panic(err) - } - defer file.Close() - - // Create a temporary file to store the uploaded file - // TODO: change location & determine valid file types - tempFile, err := ioutil.TempFile("temp-images", "upload-*.png") - if err != nil { - panic(err) - } - defer tempFile.Close() - - // Read all data from the uploaded file and write it to the temporary file - fileBytes, err := ioutil.ReadAll(file) - if err != nil { - panic(err) - } - tempFile.Write(fileBytes) - - r.Body.Close() - - imageData := imageToPixelData(fileBytes) - hash := hashTemplateImage(imageData) - // TODO: Store image hash and pixel data in postgres database - - - w.Header().Set("Access-Control-Allow-Origin", "*") - w.Write([]byte(hash)) + // TODO: Limit file size / proportions between 5x5 and 64x64 + // Passed like this curl -F "image=@art-peace-low-res-goose.jpg" http://localhost:8080/addTemplateImg + file, _, err := r.FormFile("image") + if err != nil { + panic(err) + } + defer file.Close() + + // Create a temporary file to store the uploaded file + // TODO: change location & determine valid file types + tempFile, err := ioutil.TempFile("temp-images", "upload-*.png") + if err != nil { + panic(err) + } + defer tempFile.Close() + + // Read all data from the uploaded file and write it to the temporary file + fileBytes, err := ioutil.ReadAll(file) + if err != nil { + panic(err) + } + tempFile.Write(fileBytes) + + r.Body.Close() + + imageData := imageToPixelData(fileBytes) + hash := hashTemplateImage(imageData) + // TODO: Store image hash and pixel data in postgres database + + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Write([]byte(hash)) } func addTemplateData(w http.ResponseWriter, r *http.Request) { - // Passed as byte array w/ color indexes instead of image - reqBody, err := io.ReadAll(r.Body) - if err != nil { - panic(err) - } - var jsonBody map[string]string - err = json.Unmarshal(reqBody, &jsonBody) - if err != nil { - panic(err) - } - - hash := hashTemplateImage([]byte(jsonBody["image"])) - // TODO: Store image hash and pixel data in database - - w.Header().Set("Access-Control-Allow-Origin", "*") - w.Write([]byte(hash)) + // Passed as byte array w/ color indexes instead of image + reqBody, err := io.ReadAll(r.Body) + if err != nil { + panic(err) + } + var jsonBody map[string]string + err = json.Unmarshal(reqBody, &jsonBody) + if err != nil { + panic(err) + } + + hash := hashTemplateImage([]byte(jsonBody["image"])) + // TODO: Store image hash and pixel data in database + + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Write([]byte(hash)) } func addTemplateHashDevnet(w http.ResponseWriter, r *http.Request) { - // TODO: Disable this in production - reqBody, err := io.ReadAll(r.Body) - if err != nil { - panic(err) - } - var jsonBody map[string]string - err = json.Unmarshal(reqBody, &jsonBody) - if err != nil { - panic(err) - } - - // TODO: Create this script - shellCmd := backend.ArtPeaceBackend.BackendConfig.Scripts.AddTemplateHashDevnet - // TODO: remove contract from jsonBody - contract := os.Getenv("ART_PEACE_CONTRACT_ADDRESS") - cmd := exec.Command(shellCmd, contract, "add_template", jsonBody["hash"]) - _, err = cmd.Output() - if err != nil { - fmt.Println("Error executing shell command: ", err) - panic(err) - } - - w.Header().Set("Access-Control-Allow-Origin", "*") - w.Write([]byte("Hash added to devnet")) + // TODO: Disable this in production + reqBody, err := io.ReadAll(r.Body) + if err != nil { + panic(err) + } + var jsonBody map[string]string + err = json.Unmarshal(reqBody, &jsonBody) + if err != nil { + panic(err) + } + + // TODO: Create this script + shellCmd := core.ArtPeaceBackend.BackendConfig.Scripts.AddTemplateHashDevnet + // TODO: remove contract from jsonBody + contract := os.Getenv("ART_PEACE_CONTRACT_ADDRESS") + cmd := exec.Command(shellCmd, contract, "add_template", jsonBody["hash"]) + _, err = cmd.Output() + if err != nil { + fmt.Println("Error executing shell command: ", err) + panic(err) + } + + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Write([]byte("Hash added to devnet")) } diff --git a/backend/routes/user.go b/backend/routes/user.go index 85b66c94..f03d018a 100644 --- a/backend/routes/user.go +++ b/backend/routes/user.go @@ -4,25 +4,25 @@ import ( "context" "net/http" - "art-peace-backend/backend" + "github.com/keep-starknet-strange/art-peace/backend/core" ) func InitUserRoutes() { - http.HandleFunc("/getExtraPixels", getExtraPixels) + http.HandleFunc("/getExtraPixels", getExtraPixels) } func getExtraPixels(w http.ResponseWriter, r *http.Request) { - user := r.URL.Query().Get("address") + user := r.URL.Query().Get("address") - var available string - err := backend.ArtPeaceBackend.Databases.Postgres.QueryRow(context.Background(), "SELECT available FROM ExtraPixels WHERE address = $1", user).Scan(&available); - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) - return - } + var available string + err := core.ArtPeaceBackend.Databases.Postgres.QueryRow(context.Background(), "SELECT available FROM ExtraPixels WHERE address = $1", user).Scan(&available) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } - w.Header().Set("Access-Control-Allow-Origin", "*") - w.WriteHeader(http.StatusOK) - w.Write([]byte(available)) + w.Header().Set("Access-Control-Allow-Origin", "*") + w.WriteHeader(http.StatusOK) + w.Write([]byte(available)) } diff --git a/backend/routes/websocket.go b/backend/routes/websocket.go index e262b8bc..d62fa75a 100644 --- a/backend/routes/websocket.go +++ b/backend/routes/websocket.go @@ -6,39 +6,39 @@ import ( "github.com/gorilla/websocket" - "art-peace-backend/backend" + "github.com/keep-starknet-strange/art-peace/backend/core" ) func InitWebsocketRoutes() { - http.HandleFunc("/ws", wsEndpoint) + http.HandleFunc("/ws", wsEndpoint) } var upgrader = websocket.Upgrader{ - ReadBufferSize: 1024, - WriteBufferSize: 1024, + ReadBufferSize: 1024, + WriteBufferSize: 1024, } func wsReader(conn *websocket.Conn) { - for { - // TODO: exit on close in backend? - messageType, p, err := conn.ReadMessage() - if err != nil { - fmt.Println(err) - return - } - fmt.Println("WS message received: ", messageType, string(p)) - } + for { + // TODO: exit on close in backend? + messageType, p, err := conn.ReadMessage() + if err != nil { + fmt.Println(err) + return + } + fmt.Println("WS message received: ", messageType, string(p)) + } } func wsEndpoint(w http.ResponseWriter, r *http.Request) { - upgrader.CheckOrigin = func(r *http.Request) bool { return true } + upgrader.CheckOrigin = func(r *http.Request) bool { return true } - ws, err := upgrader.Upgrade(w, r, nil) - if err != nil { - fmt.Println(err) - } + ws, err := upgrader.Upgrade(w, r, nil) + if err != nil { + fmt.Println(err) + } - fmt.Println("Client Connected") - backend.ArtPeaceBackend.WSConnections = append(backend.ArtPeaceBackend.WSConnections, ws) - wsReader(ws) + fmt.Println("Client Connected") + core.ArtPeaceBackend.WSConnections = append(core.ArtPeaceBackend.WSConnections, ws) + wsReader(ws) } 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\"..." From 96e3a2faa27c4bf7e04f0304bd032d481f688332 Mon Sep 17 00:00:00 2001 From: TAdev0 Date: Sun, 14 Apr 2024 13:37:05 +0200 Subject: [PATCH 08/11] add tests for reward deposits --- onchain/src/lib.cairo | 5 ++ onchain/src/mocks.cairo | 1 + onchain/src/mocks/erc20_mock.cairo | 42 +++++++++++++++++ onchain/src/templates/component.cairo | 12 +++-- onchain/src/templates/interface.cairo | 7 +-- onchain/src/tests/art_peace.cairo | 67 +++++++++++++++++++++++---- onchain/src/tests/utils.cairo | 23 +++++++++ 7 files changed, 136 insertions(+), 21 deletions(-) create mode 100644 onchain/src/mocks.cairo create mode 100644 onchain/src/mocks/erc20_mock.cairo create mode 100644 onchain/src/tests/utils.cairo diff --git a/onchain/src/lib.cairo b/onchain/src/lib.cairo index d04c0c45..d944a136 100644 --- a/onchain/src/lib.cairo +++ b/onchain/src/lib.cairo @@ -20,7 +20,12 @@ mod templates { }; } +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/templates/component.cairo b/onchain/src/templates/component.cairo index 6fe15236..e992461b 100644 --- a/onchain/src/templates/component.cairo +++ b/onchain/src/templates/component.cairo @@ -59,15 +59,17 @@ pub mod TemplateStoreComponent { // TODO: Return idx of the template? fn add_template( - ref self: ComponentState, - template_metadata: TemplateMetadata, - reward_token: ContractAddress, - reward_amount: u256 + 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(), reward_token, reward_amount); + self + ._deposit( + starknet::get_caller_address(), + template_metadata.reward_token, + template_metadata.reward + ); self.emit(TemplateAdded { id: template_id, metadata: template_metadata }); } diff --git a/onchain/src/templates/interface.cairo b/onchain/src/templates/interface.cairo index 54e67071..a9403a0a 100644 --- a/onchain/src/templates/interface.cairo +++ b/onchain/src/templates/interface.cairo @@ -20,12 +20,7 @@ pub trait ITemplateStore { fn get_template_hash(self: @TContractState, template_id: u32) -> felt252; // Stores a new template image into the contract state w/ metadata. // If the reward/token are set, then the contract escrows the reward for the template. - fn add_template( - ref self: TContractState, - template_metadata: TemplateMetadata, - reward_token: ContractAddress, - reward_amount: u256 - ); + fn add_template(ref self: TContractState, template_metadata: TemplateMetadata); // Returns whether the template is complete. fn is_template_complete(self: @TContractState, template_id: u32) -> bool; } diff --git a/onchain/src/tests/art_peace.cairo b/onchain/src/tests/art_peace.cairo index 2a584cef..c692412b 100644 --- a/onchain/src/tests/art_peace.cairo +++ b/onchain/src/tests/art_peace.cairo @@ -4,13 +4,17 @@ use art_peace::templates::interface::{ 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 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,6 +25,10 @@ fn ART_PEACE_CONTRACT() -> ContractAddress { contract_address_const::<'ArtPeace'>() } +fn ERC20_MOCK_CONTRACT() -> ContractAddress { + contract_address_const::<'erc20mock'>() +} + fn EMPTY_CALLDATA() -> Span { array![].span() } @@ -57,6 +65,24 @@ 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 { @@ -250,21 +276,16 @@ 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, }; - let reward_token: ContractAddress = contract_address_const::<0x0>(); - - template_store.add_template(template_metadata, reward_token, 0); + template_store.add_template(template_metadata); assert!(template_store.get_templates_count() == 1, "Templates count is not 1"); assert!(template_store.get_template_hash(0) == template_hash, "Template hash is not correct"); @@ -349,6 +370,32 @@ 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 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; + } +} From be52a74506e48dafaac45671f6046a61701cc456 Mon Sep 17 00:00:00 2001 From: TAdev0 Date: Thu, 11 Apr 2024 23:28:37 +0200 Subject: [PATCH 09/11] deposit reward when adding new template --- onchain/src/templates/component.cairo | 30 +++++++++++++++++++++++++- onchain/src/templates/interfaces.cairo | 9 +++++++- onchain/src/tests/art_peace.cairo | 4 +++- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/onchain/src/templates/component.cairo b/onchain/src/templates/component.cairo index e159b069..02982639 100644 --- a/onchain/src/templates/component.cairo +++ b/onchain/src/templates/component.cairo @@ -54,11 +54,15 @@ pub mod TemplateStoreComponent { // TODO: Return idx of the template? fn add_template( - ref self: ComponentState, template_metadata: TemplateMetadata + ref self: ComponentState, + template_metadata: TemplateMetadata, + reward_token: ContractAddress, + reward_amount: u256 ) { 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(), reward_token, reward_amount); self.emit(TemplateAdded { id: template_id, metadata: template_metadata }); } @@ -66,4 +70,28 @@ 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'); + // Next line is commented for current test not to revert + // 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/interfaces.cairo b/onchain/src/templates/interfaces.cairo index 44b9d7d1..54e67071 100644 --- a/onchain/src/templates/interfaces.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, @@ -18,7 +20,12 @@ pub trait ITemplateStore { fn get_template_hash(self: @TContractState, template_id: u32) -> felt252; // Stores a new template image into the contract state w/ metadata. // If the reward/token are set, then the contract escrows the reward for the template. - fn add_template(ref self: TContractState, template_metadata: TemplateMetadata); + fn add_template( + ref self: TContractState, + template_metadata: TemplateMetadata, + reward_token: ContractAddress, + reward_amount: u256 + ); // Returns whether the template is complete. fn is_template_complete(self: @TContractState, template_id: u32) -> bool; } diff --git a/onchain/src/tests/art_peace.cairo b/onchain/src/tests/art_peace.cairo index c679dd01..aa2ab79f 100644 --- a/onchain/src/tests/art_peace.cairo +++ b/onchain/src/tests/art_peace.cairo @@ -380,7 +380,9 @@ fn template_full_basic_test() { reward_token: contract_address_const::<0>(), }; - template_store.add_template(template_metadata); + let reward_token: ContractAddress = contract_address_const::<0x0>(); + + template_store.add_template(template_metadata, reward_token, 0); assert!(template_store.get_templates_count() == 1, "Templates count is not 1"); assert!(template_store.get_template_hash(0) == template_hash, "Template hash is not correct"); From b784dcaec2a90362930ebc51f5524afbd9c90b54 Mon Sep 17 00:00:00 2001 From: TAdev0 Date: Thu, 11 Apr 2024 23:31:24 +0200 Subject: [PATCH 10/11] fix --- onchain/src/templates/component.cairo | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/onchain/src/templates/component.cairo b/onchain/src/templates/component.cairo index 02982639..eec4de01 100644 --- a/onchain/src/templates/component.cairo +++ b/onchain/src/templates/component.cairo @@ -84,8 +84,7 @@ pub mod TemplateStoreComponent { let caller_address = starknet::get_caller_address(); let contract_address = starknet::get_contract_address(); assert(!template_proposer.is_zero(), 'Invalid caller'); - // Next line is commented for current test not to revert - // assert(!reward_token.is_zero(), 'Invalid token'); + 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'); From fdceb3f14900a98486a013280b0304fdaeaf99c1 Mon Sep 17 00:00:00 2001 From: TAdev0 Date: Sun, 14 Apr 2024 13:37:05 +0200 Subject: [PATCH 11/11] add tests for reward deposits --- onchain/src/lib.cairo | 5 ++ onchain/src/mocks.cairo | 1 + onchain/src/mocks/erc20_mock.cairo | 42 ++++++++++++++++ onchain/src/templates/component.cairo | 16 ++++-- onchain/src/templates/interfaces.cairo | 7 +-- onchain/src/tests/art_peace.cairo | 67 ++++++++++++++++++++++---- onchain/src/tests/utils.cairo | 23 +++++++++ 7 files changed, 140 insertions(+), 21 deletions(-) create mode 100644 onchain/src/mocks.cairo create mode 100644 onchain/src/mocks/erc20_mock.cairo create mode 100644 onchain/src/tests/utils.cairo diff --git a/onchain/src/lib.cairo b/onchain/src/lib.cairo index 4097103e..36ac92f8 100644 --- a/onchain/src/lib.cairo +++ b/onchain/src/lib.cairo @@ -33,7 +33,12 @@ mod nfts { }; } +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/templates/component.cairo b/onchain/src/templates/component.cairo index eec4de01..bb8dcaec 100644 --- a/onchain/src/templates/component.cairo +++ b/onchain/src/templates/component.cairo @@ -1,6 +1,10 @@ #[starknet::component] pub mod TemplateStoreComponent { + 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,15 +58,17 @@ pub mod TemplateStoreComponent { // TODO: Return idx of the template? fn add_template( - ref self: ComponentState, - template_metadata: TemplateMetadata, - reward_token: ContractAddress, - reward_amount: u256 + 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(), reward_token, reward_amount); + self + ._deposit( + starknet::get_caller_address(), + template_metadata.reward_token, + template_metadata.reward + ); self.emit(TemplateAdded { id: template_id, metadata: template_metadata }); } diff --git a/onchain/src/templates/interfaces.cairo b/onchain/src/templates/interfaces.cairo index 54e67071..a9403a0a 100644 --- a/onchain/src/templates/interfaces.cairo +++ b/onchain/src/templates/interfaces.cairo @@ -20,12 +20,7 @@ pub trait ITemplateStore { fn get_template_hash(self: @TContractState, template_id: u32) -> felt252; // Stores a new template image into the contract state w/ metadata. // If the reward/token are set, then the contract escrows the reward for the template. - fn add_template( - ref self: TContractState, - template_metadata: TemplateMetadata, - reward_token: ContractAddress, - reward_amount: u256 - ); + fn add_template(ref self: TContractState, template_metadata: TemplateMetadata); // Returns whether the template is complete. fn is_template_complete(self: @TContractState, template_id: u32) -> bool; } diff --git a/onchain/src/tests/art_peace.cairo b/onchain/src/tests/art_peace.cairo index aa2ab79f..c9a8ed50 100644 --- a/onchain/src/tests/art_peace.cairo +++ b/onchain/src/tests/art_peace.cairo @@ -9,14 +9,18 @@ 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; @@ -27,6 +31,10 @@ fn ART_PEACE_CONTRACT() -> ContractAddress { contract_address_const::<'ArtPeace'>() } +fn ERC20_MOCK_CONTRACT() -> ContractAddress { + contract_address_const::<'erc20mock'>() +} + fn EMPTY_CALLDATA() -> Span { array![].span() } @@ -82,6 +90,24 @@ 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 { @@ -368,21 +394,16 @@ 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, }; - let reward_token: ContractAddress = contract_address_const::<0x0>(); - - template_store.add_template(template_metadata, reward_token, 0); + template_store.add_template(template_metadata); assert!(template_store.get_templates_count() == 1, "Templates count is not 1"); assert!(template_store.get_template_hash(0) == template_hash, "Template hash is not correct"); @@ -467,6 +488,32 @@ 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. 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; + } +}