Skip to content

Commit

Permalink
[feat] NFTs module & Color pixel quests (#36)
Browse files Browse the repository at this point in the history
* NFTs module, minting from the board, and color based pixel quests

* scarb fmt
  • Loading branch information
b-j-roberts committed Apr 12, 2024
1 parent 5c14814 commit d7afe5a
Show file tree
Hide file tree
Showing 13 changed files with 466 additions and 50 deletions.
6 changes: 6 additions & 0 deletions onchain/Scarb.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions onchain/Scarb.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
45 changes: 44 additions & 1 deletion onchain/src/art_peace.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -35,6 +39,7 @@ pub mod ArtPeace {
main_quests_count: u32,
// Map: quest index -> quest contract address
main_quests: LegacyMap::<u32, ContractAddress>,
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)]
Expand Down Expand Up @@ -77,6 +82,7 @@ pub mod ArtPeace {
pub end_time: u64,
pub daily_quests: Span<ContractAddress>,
pub main_quests: Span<ContractAddress>,
pub nft_contract: ContractAddress,
}

const DAY_IN_SECONDS: u64 = consteval_int!(60 * 60 * 24);
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -406,13 +418,43 @@ 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 {
self.user_pixels_placed.read((day, user, color))
}
}

#[abi(embed_v0)]
impl ArtPeaceNFTMinter of IArtPeaceNFTMinter<ContractState> {
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<ContractState> {
// TODO: Check template function
Expand All @@ -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;
}
Expand Down
6 changes: 6 additions & 0 deletions onchain/src/interface.cairo → onchain/src/interfaces.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,17 @@ pub trait IArtPeace<TContractState> {
fn claim_today_quest(ref self: TContractState, quest_id: u32, calldata: Span<felt252>);
fn claim_main_quest(ref self: TContractState, quest_id: u32, calldata: Span<felt252>);

// 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;
Expand Down
21 changes: 17 additions & 4 deletions onchain/src/lib.cairo
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down
70 changes: 70 additions & 0 deletions onchain/src/nfts/canvas_nft.cairo
Original file line number Diff line number Diff line change
@@ -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<ContractState>;
#[abi(embed_v0)]
impl ERC721MetadataImpl = ERC721Component::ERC721MetadataImpl<ContractState>;
#[abi(embed_v0)]
impl SRC5Impl = SRC5Component::SRC5Impl<ContractState>;
#[abi(embed_v0)]
impl CanvasNFTStoreImpl =
CanvasNFTStoreComponent::CanvasNFTStoreImpl<ContractState>;

impl InternalImpl = ERC721Component::InternalImpl<ContractState>;

#[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<ContractState> {
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 });
}
}
}
49 changes: 49 additions & 0 deletions onchain/src/nfts/component.cairo
Original file line number Diff line number Diff line change
@@ -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::<u256, NFTMetadata>,
}

#[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<TContractState>
> of ICanvasNFTStore<ComponentState<TContractState>> {
fn get_nfts_count(self: @ComponentState<TContractState>) -> u256 {
return self.nfts_count.read();
}

fn get_nft_metadata(self: @ComponentState<TContractState>, token_id: u256) -> NFTMetadata {
return self.nfts_data.read(token_id);
}

fn get_nft_minter(
self: @ComponentState<TContractState>, token_id: u256
) -> starknet::ContractAddress {
let metadata: NFTMetadata = self.nfts_data.read(token_id);
return metadata.minter;
}

fn get_nft_image_hash(self: @ComponentState<TContractState>, token_id: u256) -> felt252 {
let metadata: NFTMetadata = self.nfts_data.read(token_id);
return metadata.image_hash;
}
}
}
39 changes: 39 additions & 0 deletions onchain/src/nfts/interfaces.cairo
Original file line number Diff line number Diff line change
@@ -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<TContractState> {
// 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<TContractState> {
// Mint a new NFT called by the ArtPeaceNFTMinter contract.
fn mint(ref self: TContractState, metadata: NFTMetadata, receiver: starknet::ContractAddress);
}

#[starknet::interface]
pub trait IArtPeaceNFTMinter<TContractState> {
// Mints a new NFT from the canvas using init params, and returns the token ID.
fn mint_nft(self: @TContractState, mint_params: NFTMintParams);
}
2 changes: 2 additions & 0 deletions onchain/src/quests/interfaces.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,6 @@ pub trait IPixelQuest<TContractState> {
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;
}
Loading

0 comments on commit d7afe5a

Please sign in to comment.