From 704c0b9d9b8e70c9b42047cbf32a753f56ad8557 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Thu, 21 Mar 2024 09:24:49 -0500 Subject: [PATCH] Initial work to setup the art/peace contract with some basic tests. Also github repo setup like CONTRIBUTING.md, github workflows, and more. --- .github/CODEOWNERS | 1 + .github/ISSUE_TEMPLATE/01_FEATURE_REQUEST.md | 11 ++ .github/ISSUE_TEMPLATE/02_BUG_REPORT.md | 13 ++ .../ISSUE_TEMPLATE/03_CODEBASE_IMPROVEMENT.md | 7 + .github/PULL_REQUEST_TEMPLATE.md | 8 + .github/linter/base_style.rb | 4 + .github/linter/readme_style.rb | 10 + .github/workflows/build.yml | 18 ++ .github/workflows/check.yml | 26 +++ .gitignore | 2 + .tool-versions | 2 + CONTRIBUTING.md | 63 ++++++ README.md | 16 ++ Scarb.lock | 14 ++ Scarb.toml | 17 ++ src/lib.cairo | 181 ++++++++++++++++++ src/tests/art_peace.cairo | 59 ++++++ 17 files changed, 452 insertions(+) create mode 100644 .github/CODEOWNERS create mode 100644 .github/ISSUE_TEMPLATE/01_FEATURE_REQUEST.md create mode 100644 .github/ISSUE_TEMPLATE/02_BUG_REPORT.md create mode 100644 .github/ISSUE_TEMPLATE/03_CODEBASE_IMPROVEMENT.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/linter/base_style.rb create mode 100644 .github/linter/readme_style.rb create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/check.yml create mode 100644 .gitignore create mode 100644 .tool-versions create mode 100644 CONTRIBUTING.md create mode 100644 Scarb.lock create mode 100644 Scarb.toml create mode 100644 src/lib.cairo create mode 100644 src/tests/art_peace.cairo diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..53811172 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @b-j-roberts diff --git a/.github/ISSUE_TEMPLATE/01_FEATURE_REQUEST.md b/.github/ISSUE_TEMPLATE/01_FEATURE_REQUEST.md new file mode 100644 index 00000000..ff340577 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01_FEATURE_REQUEST.md @@ -0,0 +1,11 @@ +--- +name: Feature request +about: suggest new feature +title: "[feat] " +labels: "feature" +assignees: "" +--- + + + +#### References diff --git a/.github/ISSUE_TEMPLATE/02_BUG_REPORT.md b/.github/ISSUE_TEMPLATE/02_BUG_REPORT.md new file mode 100644 index 00000000..57a92f1b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/02_BUG_REPORT.md @@ -0,0 +1,13 @@ +--- +name: Bug report +about: create bug report +title: "[bug] " +labels: "bug" +assignees: "" +--- + +**ver:** + + + +**How to reproduce:** diff --git a/.github/ISSUE_TEMPLATE/03_CODEBASE_IMPROVEMENT.md b/.github/ISSUE_TEMPLATE/03_CODEBASE_IMPROVEMENT.md new file mode 100644 index 00000000..0132b950 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/03_CODEBASE_IMPROVEMENT.md @@ -0,0 +1,7 @@ +--- +name: Codebase improvement +about: docs, ci, tooling, other +title: "[dev] " +labels: "dev" +assignees: "" +--- diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..b39ff73e --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,8 @@ + + +- [ ] issue # +- [ ] follows contribution [guide](https://github.com/keep-starknet-strange/art-peace/blob/main/CONTRIBUTING.md) +- [ ] code change includes tests +- [ ] breaking change + + diff --git a/.github/linter/base_style.rb b/.github/linter/base_style.rb new file mode 100644 index 00000000..8d981050 --- /dev/null +++ b/.github/linter/base_style.rb @@ -0,0 +1,4 @@ +all +# lame rules +exclude_rule 'MD002' +exclude_rule 'MD041' diff --git a/.github/linter/readme_style.rb b/.github/linter/readme_style.rb new file mode 100644 index 00000000..9b6cfc97 --- /dev/null +++ b/.github/linter/readme_style.rb @@ -0,0 +1,10 @@ +all +# allow inline HTML for README fmt +exclude_rule 'MD033' +# badges trigger rule +exclude_rule 'MD034' +# README img serves as 'First Header' +exclude_rule 'MD002' +exclude_rule 'MD041' +# TODO: disable/enable not working for all-contribs +exclude_rule 'MD013' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..a46b0608 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,18 @@ +name: build + +on: + workflow_dispatch: + push: + branches: + - main + pull_request: +permissions: read-all + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: asdf-vm/actions/install@v3 + - run: scarb fmt --check + - run: scarb build diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 00000000..b4a86303 --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,26 @@ +name: check + +on: + workflow_dispatch: + push: + branches: + - main + pull_request: +permissions: read-all + +jobs: + markdown: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: | + sudo gem install mdl + mdl -s .github/linter/readme_style.rb README.md + mdl -s .github/linter/base_style.rb .github CONTRIBUTING.md + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: asdf-vm/actions/install@v3 + - run: snforge test diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..73aa31e6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target +.snfoundry_cache/ diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 00000000..acb7f190 --- /dev/null +++ b/.tool-versions @@ -0,0 +1,2 @@ +scarb 2.6.3 +starknet-foundry 0.19.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..c95d5ea9 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,63 @@ +## 🛠️ Contributing to art/peace 🛠️ + +Welcome, contributing to `art/peace` is easy! + +1. Submit or comment your intent on an issue +1. We will try to respond quickly +1. Fork this repo +1. Submit your PR against `main` +1. Address PR Review + +### Issue + +Project tracking is done via GitHub [issues](https://github.com/keep-starknet-strange/art-peace/issues). +First look at open issues to see if your request is already submitted. +If it is comment on the issue requesting assignment, if not open an issue. + +We use 3 issue labels for development: + +- `feat` -> suggest new feature +- `bug` -> create a reproducible bug report +- `dev` -> non-functional repository changes + +These labels are used as prefixes as follows for `issue`, `branch name`, `pr title`: + +- `[feat]` -> `feat/{issue #}-{issue name}` -> `feat:` +- `[bug]` -> `bug/{issue #}-{issue name}` -> `bug:` +- `[dev]` -> `dev/{issue #}-{issue name}` -> `dev:` + +#### TODO + +If your PR includes a `TODO` comment please open an issue and comment the relevant +code with `TODO(#ISSUE_NUM):`. + +### Submit PR + +Ensure your code is well formatted, well tested and well documented. A core contributor +will review your work. Address changes, ensure ci passes, +and voilà you're a `art/peace` contributor. + +Markdown [linter](https://github.com/markdownlint/markdownlint?tab=readme-ov-file#markdown-lint-tool): + +```bash +mdl -s .github/linter/readme_style.rb README.md +``` + +Scarb linter: + +```bash +scarb fmt +``` + +### Additional Resources + +- [Cairo Book](https://book.cairo-lang.org/) +- [Starknet Book](https://book.starknet.io/) +- [Starknet Foundry Book](https://foundry-rs.github.io/starknet-foundry/) +- [Starknet By Example](https://starknet-by-example.voyager.online/) +- [Starkli Book](https://book.starkli.rs/) +- [Syncing a Fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) + +## + +Thank you for your contribution! diff --git a/README.md b/README.md index 6ea28af5..6a749d02 100644 --- a/README.md +++ b/README.md @@ -16,3 +16,19 @@ Some of the features include : ## References - [r/place technical document](https://www.redditinc.com/blog/how-we-built-rplace) + +## Build + +To build the project, run: + +```bash +scarb build +``` + +## Test + +To test the project, run ( uses `snforge` ): + +```bash +scarb test +``` diff --git a/Scarb.lock b/Scarb.lock new file mode 100644 index 00000000..6309431f --- /dev/null +++ b/Scarb.lock @@ -0,0 +1,14 @@ +# Code generated by scarb DO NOT EDIT. +version = 1 + +[[package]] +name = "art_peace" +version = "0.1.0" +dependencies = [ + "snforge_std", +] + +[[package]] +name = "snforge_std" +version = "0.19.0" +source = "git+https://github.com/foundry-rs/starknet-foundry?tag=v0.19.0#a3391dce5bdda51c63237032e6cfc64fb7a346d4" diff --git a/Scarb.toml b/Scarb.toml new file mode 100644 index 00000000..4db6ad35 --- /dev/null +++ b/Scarb.toml @@ -0,0 +1,17 @@ +[package] +name = "art_peace" +version = "0.1.0" +edition = "2023_11" + +# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html + +[dependencies] +snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = "v0.19.0" } +starknet = "2.6.3" + +[scripts] +test = "snforge test" + +[[target.starknet-contract]] +casm = true +sierra = true diff --git a/src/lib.cairo b/src/lib.cairo new file mode 100644 index 00000000..1ab53485 --- /dev/null +++ b/src/lib.cairo @@ -0,0 +1,181 @@ +#[starknet::interface] +trait IArtPeace { + fn place_pixel(ref self: TContractState, pos: u128, color: u8); + fn place_pixel_xy(ref self: TContractState, x: u128, y: u128, color: u8); + fn place_extra_pixels(ref self: TContractState, positions: Array, colors: Array); + + fn get_pixel(self: @TContractState, pos: u128) -> u8; + fn get_pixel_xy(self: @TContractState, x: u128, y: u128) -> u8; + + fn get_total_pixels(self: @TContractState) -> u128; + fn get_width(self: @TContractState) -> u128; + fn get_height(self: @TContractState) -> u128; + + fn get_last_placed(self: @TContractState) -> u64; + fn get_user_last_placed(self: @TContractState, user: starknet::ContractAddress) -> u64; + fn get_time_between_pixels(self: @TContractState) -> u64; + + fn get_extra_pixels_count(self: @TContractState) -> u32; + fn get_user_extra_pixels_count(self: @TContractState, user: starknet::ContractAddress) -> u32; + + fn get_color_count(self: @TContractState) -> u8; + fn get_colors(self: @TContractState) -> Array; +} + +#[starknet::contract] +mod ArtPeace { + use starknet::ContractAddress; + + #[storage] + struct Storage { + board: LegacyMap::, + board_width: u128, + board_height: u128, + total_pixels: u128, + // Maps the users contract address to the last time they placed a pixel + user_last_placed: LegacyMap::, + time_between_pixels: u64, + // Maps the users contract address to the amount of extra pixels they have + extra_pixels: LegacyMap::, + // 3 byte HEX colors + color_count: u8, // TODO: Remove and use colors.len()? + colors: LegacyMap::, // TODO + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + PixelPlaced: PixelPlaced, + } + + #[derive(Drop, starknet::Event)] + struct PixelPlaced { + #[key] + placed_by: ContractAddress, + #[key] + pos: u128, + color: u8, + } + + #[constructor] + fn constructor( + ref self: ContractState, board_width: u128, board_height: u128, time_between_pixels: u64 + ) { + self.board_width.write(board_width); + self.board_height.write(board_height); + self.total_pixels.write(board_width * board_height); + + self.time_between_pixels.write(time_between_pixels); + + // TODO + self.color_count.write(12); + //self.color_count.write(colors.len().try_into().unwrap()); + //let mut i = 0; + //while i < colors.len() { + // self.colors.write(i, *colors.at(i)); + // i += 1; + //} + } + + #[abi(embed_v0)] + impl ArtPeaceImpl of super::IArtPeace { + fn place_pixel(ref self: ContractState, pos: u128, color: u8) { + assert!(pos < self.total_pixels.read()); + assert!(color < self.color_count.read()); // TODO: remove and consider outside range as base color? + let now = starknet::get_block_timestamp(); + let caller = starknet::get_caller_address(); + // TODO: Only if the user has placed a pixel before? + assert!(now - self.user_last_placed.read(caller) >= self.time_between_pixels.read()); + self.board.write(pos, color); + self.user_last_placed.write(caller, now); + self.emit(PixelPlaced { placed_by: caller, pos, color }); + } + + fn place_pixel_xy(ref self: ContractState, x: u128, y: u128, color: u8) { + let pos = x + y * self.board_width.read(); + self.place_pixel(pos, color); + } + + fn place_extra_pixels(ref self: ContractState, positions: Array, colors: Array) { + assert!(positions.len() == colors.len()); + let caller = starknet::get_caller_address(); + let extra_pixels = self.extra_pixels.read(caller); + let pixel_count = positions.len(); + assert!(pixel_count <= extra_pixels); + let mut i = 0; + while i < pixel_count { + let pos = *positions.at(i); + let color = *colors.at(i); + assert!(pos < self.total_pixels.read()); + assert!(color < self.color_count.read()); + self.board.write(pos, color); + self.emit(PixelPlaced { placed_by: caller, pos, color }); + i += 1; + }; + self.extra_pixels.write(caller, extra_pixels - pixel_count); + } + + fn get_pixel(self: @ContractState, pos: u128) -> u8 { + self.board.read(pos) + } + + fn get_pixel_xy(self: @ContractState, x: u128, y: u128) -> u8 { + let pos = x + y * self.board_width.read(); + self.board.read(pos) + } + + fn get_total_pixels(self: @ContractState) -> u128 { + self.total_pixels.read() + } + + fn get_width(self: @ContractState) -> u128 { + self.board_width.read() + } + + fn get_height(self: @ContractState) -> u128 { + self.board_height.read() + } + + fn get_last_placed(self: @ContractState) -> u64 { + self.user_last_placed.read(starknet::get_caller_address()) + } + + fn get_user_last_placed(self: @ContractState, user: ContractAddress) -> u64 { + self.user_last_placed.read(user) + } + + fn get_time_between_pixels(self: @ContractState) -> u64 { + self.time_between_pixels.read() + } + + fn get_extra_pixels_count(self: @ContractState) -> u32 { + self.extra_pixels.read(starknet::get_caller_address()) + } + + fn get_user_extra_pixels_count(self: @ContractState, user: ContractAddress) -> u32 { + self.extra_pixels.read(user) + } + + fn get_color_count(self: @ContractState) -> u8 { + self.color_count.read() + } + + fn get_colors(self: @ContractState) -> Array { + let color_count = self.color_count.read(); + let mut colors = array![]; + let mut i = 0; + while i < color_count { + // TODO + colors.append(0x000000); + //colors.append(self.colors.read(i)); + i += 1; + }; + colors + } + } +} + +#[cfg(test)] +mod tests { + mod art_peace; +} diff --git a/src/tests/art_peace.cairo b/src/tests/art_peace.cairo new file mode 100644 index 00000000..c6496392 --- /dev/null +++ b/src/tests/art_peace.cairo @@ -0,0 +1,59 @@ +use art_peace::{IArtPeaceDispatcher, IArtPeaceDispatcherImpl}; + +use snforge_std as snf; +use snforge_std::{CheatTarget, ContractClassTrait}; +use starknet::ContractAddress; + +const WIDTH: u128 = 100; +const HEIGHT: u128 = 100; +const TIME_BETWEEN_PIXELS: u64 = 10; + +fn deploy_contract() -> ContractAddress { + let contract = snf::declare("ArtPeace"); + let calldata: Array = array![WIDTH.into(), HEIGHT.into(), TIME_BETWEEN_PIXELS.into()]; + let contract_addr = contract.deploy(@calldata).unwrap(); + snf::start_warp(CheatTarget::One(contract_addr), TIME_BETWEEN_PIXELS); + contract_addr +} + +fn warp_to_next_available_time(art_peace: IArtPeaceDispatcher) { + let last_time = art_peace.get_last_placed(); + snf::start_warp(CheatTarget::One(art_peace.contract_address), last_time + TIME_BETWEEN_PIXELS); +} + +#[test] +fn deploy_test() { + let art_peace = IArtPeaceDispatcher { contract_address: deploy_contract() }; + assert!(art_peace.get_width() == WIDTH, "Deployed contract has wrong width"); + assert!(art_peace.get_height() == HEIGHT, "Deployed contract has wrong height"); + assert!( + art_peace.get_total_pixels() == WIDTH * HEIGHT, "Deployed contract has wrong total pixels" + ); +} + +// TODO: To fuzz test +// TODO: Test out of bounds, other assert failures +// TODO: event spy? +// TODO: all getters & setters + +#[test] +fn place_pixel_test() { + let art_peace = IArtPeaceDispatcher { contract_address: deploy_contract() }; + + let x = 10; + let y = 20; + let pos = x + y * WIDTH; + let color = 0x5; + art_peace.place_pixel(pos, color); + assert!(art_peace.get_pixel(pos) == color, "Pixel was not placed correctly at pos"); + assert!(art_peace.get_pixel_xy(x, y) == color, "Pixel was not placed correctly at xy"); + + warp_to_next_available_time(art_peace); + let x = 15; + let y = 25; + let pos = x + y * WIDTH; + let color = 0x7; + art_peace.place_pixel_xy(x, y, color); + assert!(art_peace.get_pixel_xy(x, y) == color, "Pixel xy was not placed correctly at xy"); + assert!(art_peace.get_pixel(pos) == color, "Pixel xy was not placed correctly at pos"); +}