diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 39f0e4e..c258e50 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -29,7 +29,29 @@ jobs: fi build-and-deploy: - needs: validate-pr # enforce validation before build + needs: validate-pr runs-on: ubuntu-latest steps: - # existing build steps here + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20.x + cache: npm + - name: Install dependencies + run: npm ci + - name: Build frontend + run: npm --workspace frontend run build + - name: Build backend + run: npm --workspace backend run build + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + targets: wasm32-unknown-unknown + - name: Cache Rust dependencies + uses: Swatinem/rust-cache@v2 + with: + workspaces: "contract" + - name: Build contract + run: cargo build --target wasm32-unknown-unknown --release + working-directory: contract diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f1d755c..9e8b4b1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,3 +87,30 @@ jobs: run: npm --workspace frontend exec -- tsc --noEmit -p tsconfig.json - name: Type-check backend run: npm --workspace backend exec -- tsc --noEmit -p tsconfig.json + + contracts: + name: contracts + runs-on: ubuntu-latest + defaults: + run: + working-directory: contract + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + targets: wasm32-unknown-unknown + - name: Cache dependencies + uses: Swatinem/rust-cache@v2 + with: + workspaces: "contract" + - name: Check formatting + run: cargo fmt --all -- --check + - name: Run clippy + run: cargo clippy --all-targets --all-features -- -D warnings + - name: Build Contracts + run: cargo build --target wasm32-unknown-unknown --release + - name: Run tests + run: cargo test diff --git a/.gitignore b/.gitignore index 75937e7..dad009b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ contributoNote.md # Rust build artifacts contract/target/ -contract/Cargo.lock \ No newline at end of file +contract/Cargo.lock + +.DS_Store \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 890c726..7ca1a88 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,8 +30,39 @@ npm --workspace frontend exec -- tsc --noEmit -p tsconfig.json npm --workspace backend exec -- tsc --noEmit -p tsconfig.json ``` +## Contract Development + +### Prerequisites +Install the following tools before working on contracts: + +```bash +# Install Rust +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + +# Add wasm32-unknown-unknown target +rustup target add wasm32-unknown-unknown + +# Install Stellar CLI +cargo install --locked stellar-cli +``` + +**MUST RUN** Local checks from inside `contracts/` before submitting a PR: + +```bash +cd contracts/ + +# Check formatting +cargo fmt --check + +# Build the contract +stellar contract build + +# Run tests +cargo test +``` + ## Branch Protection -main and develop require status checks: lint-imports, build, type-check. +main and develop require status checks: lint-imports, build, type-check, contracts. Require branches to be up-to-date before merging. ## Pull Request Standards diff --git a/contract/Cargo.toml b/contract/Cargo.toml index 48d7f6b..b605889 100644 --- a/contract/Cargo.toml +++ b/contract/Cargo.toml @@ -1,6 +1,24 @@ [package] name = "contract" version = "0.1.0" -edition = "2024" +edition = "2021" [dependencies] +soroban-sdk = "23" + +[dev-dependencies] +soroban-sdk = { version = "23", features = ["testutils"] } + +[profile.release] +opt-level = "z" +overflow-checks = true +debug = 0 +strip = "symbols" +debug-assertions = false +panic = "abort" +codegen-units = 1 +lto = true + +[profile.release-with-logs] +inherits = "release" +debug-assertions = true \ No newline at end of file diff --git a/contract/src/lib.rs b/contract/src/lib.rs index bb1bf22..f52b461 100644 --- a/contract/src/lib.rs +++ b/contract/src/lib.rs @@ -1,5 +1,5 @@ #![no_std] -use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, String, Vec, Map}; +use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, String, Vec}; #[derive(Clone)] #[contracttype] @@ -28,14 +28,9 @@ pub struct MindBlockContract; #[contractimpl] impl MindBlockContract { /// Initialize a new player profile - pub fn register_player( - env: Env, - player: Address, - username: String, - iq_level: u32, - ) -> Player { + pub fn register_player(env: Env, player: Address, username: String, iq_level: u32) -> Player { player.require_auth(); - + let new_player = Player { address: player.clone(), username: username.clone(), @@ -44,16 +39,16 @@ impl MindBlockContract { puzzles_solved: 0, current_streak: 0, }; - + env.storage().instance().set(&player, &new_player); new_player } - + /// Get player profile pub fn get_player(env: Env, player: Address) -> Option { env.storage().instance().get(&player) } - + /// Submit puzzle solution and award XP pub fn submit_puzzle( env: Env, @@ -63,23 +58,24 @@ impl MindBlockContract { score: u32, ) -> u64 { player.require_auth(); - - let mut player_data: Player = env.storage() + + let mut player_data: Player = env + .storage() .instance() .get(&player) .unwrap_or_else(|| panic!("Player not registered")); - + // Calculate XP based on score and IQ level let xp_reward = (score as u64) * (player_data.iq_level as u64) / 10; - + // Update player stats player_data.xp += xp_reward; player_data.puzzles_solved += 1; player_data.current_streak += 1; - + // Save updated player data env.storage().instance().set(&player, &player_data); - + // Record submission let submission = PuzzleSubmission { player: player.clone(), @@ -88,66 +84,63 @@ impl MindBlockContract { score, timestamp: env.ledger().timestamp(), }; - + let submission_key = (player.clone(), puzzle_id); env.storage().instance().set(&submission_key, &submission); - + player_data.xp } - + /// Get top players by XP (leaderboard) - pub fn get_leaderboard(env: Env, limit: u32) -> Vec { + pub fn get_leaderboard(env: Env, _limit: u32) -> Vec { // Note: In production, implement proper pagination and sorting // This is a simplified version - let mut leaderboard = Vec::new(&env); - // This would need to be implemented with proper indexing // For now, returns empty vector as placeholder - leaderboard + Vec::new(&env) } - + /// Update player IQ level pub fn update_iq_level(env: Env, player: Address, new_iq_level: u32) { player.require_auth(); - - let mut player_data: Player = env.storage() + + let mut player_data: Player = env + .storage() .instance() .get(&player) .unwrap_or_else(|| panic!("Player not registered")); - + player_data.iq_level = new_iq_level; env.storage().instance().set(&player, &player_data); } - + /// Reset player streak (called when streak is broken) pub fn reset_streak(env: Env, player: Address) { player.require_auth(); - - let mut player_data: Player = env.storage() + + let mut player_data: Player = env + .storage() .instance() .get(&player) .unwrap_or_else(|| panic!("Player not registered")); - + player_data.current_streak = 0; env.storage().instance().set(&player, &player_data); } - + /// Get player's total XP pub fn get_xp(env: Env, player: Address) -> u64 { - let player_data: Player = env.storage() + let player_data: Player = env + .storage() .instance() .get(&player) .unwrap_or_else(|| panic!("Player not registered")); - + player_data.xp } - + /// Get puzzle submission details - pub fn get_submission( - env: Env, - player: Address, - puzzle_id: u64, - ) -> Option { + pub fn get_submission(env: Env, player: Address, puzzle_id: u64) -> Option { let submission_key = (player, puzzle_id); env.storage().instance().get(&submission_key) } @@ -161,35 +154,35 @@ mod test { #[test] fn test_register_player() { let env = Env::default(); - let contract_id = env.register_contract(None, MindBlockContract); + let contract_id = env.register(MindBlockContract, ()); let client = MindBlockContractClient::new(&env, &contract_id); - + let player = Address::generate(&env); let username = String::from_str(&env, "TestPlayer"); - + env.mock_all_auths(); - + let result = client.register_player(&player, &username, &100); - + assert_eq!(result.xp, 0); assert_eq!(result.iq_level, 100); } - + #[test] fn test_submit_puzzle() { let env = Env::default(); - let contract_id = env.register_contract(None, MindBlockContract); + let contract_id = env.register(MindBlockContract, ()); let client = MindBlockContractClient::new(&env, &contract_id); - + let player = Address::generate(&env); let username = String::from_str(&env, "TestPlayer"); let category = String::from_str(&env, "coding"); - + env.mock_all_auths(); - + client.register_player(&player, &username, &100); let xp = client.submit_puzzle(&player, &1, &category, &95); - + assert!(xp > 0); } -} \ No newline at end of file +}