diff --git a/.github/workflows/anchor.yml b/.github/workflows/anchor.yml index 27b756e89..3d7d90fcc 100644 --- a/.github/workflows/anchor.yml +++ b/.github/workflows/anchor.yml @@ -1,36 +1,13 @@ -# Solana / Anchor CI -# -# Three independent pipelines — one per program directory: -# -# bounty-registry contracts/bounty-registry/ -# fndry-staking contracts/staking-program/ -# rust-quality clippy + rustfmt + cargo audit (all programs) -# -# Each program pipeline: -# 1. anchor build — compile the Solana program -# 2. cargo test — run Rust unit tests (no validator needed) -# 3. anchor test — run TypeScript integration tests (local validator) -# 4. coverage — generate c8 LCOV report, upload to artifacts -# -# Triggers on any PR or push that touches contracts/ or this workflow. - name: Anchor CI on: - pull_request: - branches: [main] - paths: - - 'contracts/**' - - '.github/workflows/anchor.yml' push: branches: [main] - paths: - - 'contracts/**' - - '.github/workflows/anchor.yml' - workflow_dispatch: + pull_request: + branches: [main] concurrency: - group: anchor-${{ github.head_ref || github.ref }} + group: anchor-ci-${{ github.head_ref || github.run_id }} cancel-in-progress: true env: @@ -39,16 +16,20 @@ env: RUST_VERSION: '1.79' NODE_VERSION: '20' -# ══════════════════════════════════════════════════════════════════ -# BOUNTY REGISTRY -# ══════════════════════════════════════════════════════════════════ - jobs: - bounty-registry-build: - name: "Bounty Registry — Build" + anchor-build-test: + name: Anchor Build & Test — bounty-registry runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + cache-dependency-path: frontend/package.json - name: Install Rust uses: dtolnay/rust-toolchain@stable @@ -57,7 +38,7 @@ jobs: components: clippy, rustfmt - name: Setup Solana - uses: metadaoproject/setup-solana@v1 + uses: metaplex-foundation/setup-solana@v1 with: solana-cli-version: ${{ env.SOLANA_VERSION }} @@ -67,132 +48,40 @@ jobs: - name: Generate Solana keypair run: solana-keygen new --no-bip39-passphrase -o ~/.config/solana/id.json - - name: Cache Cargo - uses: actions/cache@v4 - with: - path: | - ~/.cargo/bin/ - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - contracts/bounty-registry/target/ - key: bounty-registry-cargo-${{ runner.os }}-${{ hashFiles('contracts/bounty-registry/programs/bounty-registry/Cargo.toml') }} - restore-keys: bounty-registry-cargo-${{ runner.os }}- + - name: Security audit — bounty-registry + working-directory: contracts/bounty-registry + run: cargo audit + continue-on-error: true - - name: Build program + - name: Build Anchor Legacy (bounty-registry) working-directory: contracts/bounty-registry run: anchor build - - name: Upload build artifacts - uses: actions/upload-artifact@v4 - with: - name: bounty-registry-build - path: | - contracts/bounty-registry/target/deploy/ - contracts/bounty-registry/target/idl/ - retention-days: 7 - - bounty-registry-rust-tests: - name: "Bounty Registry — Rust Unit Tests" - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - with: - toolchain: ${{ env.RUST_VERSION }} - - - name: Cache Cargo - uses: actions/cache@v4 - with: - path: | - ~/.cargo/bin/ - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - contracts/bounty-registry/target/ - key: bounty-registry-cargo-${{ runner.os }}-${{ hashFiles('contracts/bounty-registry/programs/bounty-registry/Cargo.toml') }} - restore-keys: bounty-registry-cargo-${{ runner.os }}- - - - name: Run Rust unit tests + - name: Run Integration Tests — bounty-registry working-directory: contracts/bounty-registry - run: cargo test --manifest-path programs/bounty-registry/Cargo.toml + run: anchor test + env: + ANCHOR_PROVIDER_URL: http://127.0.0.1:8899 - bounty-registry-integration-tests: - name: "Bounty Registry — Integration Tests (localnet)" + anchor-staking-audit: + name: Anchor Build & Test — staking-program runs-on: ubuntu-latest - needs: bounty-registry-build steps: - - uses: actions/checkout@v4 - - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - with: - toolchain: ${{ env.RUST_VERSION }} - - - name: Setup Solana - uses: metadaoproject/setup-solana@v1 - with: - solana-cli-version: ${{ env.SOLANA_VERSION }} + - name: Checkout + uses: actions/checkout@v4 - - name: Install Anchor CLI - run: npm install -g @coral-xyz/anchor-cli@${{ env.ANCHOR_VERSION }} - - - name: Generate Solana keypair - run: solana-keygen new --no-bip39-passphrase -o ~/.config/solana/id.json - - - name: Setup Node.js + - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - cache-dependency-path: contracts/bounty-registry/package.json - - - name: Install npm dependencies - working-directory: contracts/bounty-registry - run: npm install - - - name: Download build artifacts - uses: actions/download-artifact@v4 - with: - name: bounty-registry-build - path: contracts/bounty-registry/target/ - - - name: Run integration tests (localnet) - working-directory: contracts/bounty-registry - run: anchor test --provider.cluster localnet - timeout-minutes: 10 - - - name: Generate coverage report - working-directory: contracts/bounty-registry - run: npm run test:coverage || true - - - name: Upload coverage - uses: actions/upload-artifact@v4 - with: - name: bounty-registry-coverage - path: contracts/bounty-registry/coverage/ - retention-days: 14 - -# ══════════════════════════════════════════════════════════════════ -# FNDRY STAKING -# ══════════════════════════════════════════════════════════════════ - - staking-build: - name: "Staking — Build" - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - name: Install Rust uses: dtolnay/rust-toolchain@stable with: toolchain: ${{ env.RUST_VERSION }} - components: clippy, rustfmt - name: Setup Solana - uses: metadaoproject/setup-solana@v1 + uses: metaplex-foundation/setup-solana@v1 with: solana-cli-version: ${{ env.SOLANA_VERSION }} @@ -202,160 +91,43 @@ jobs: - name: Generate Solana keypair run: solana-keygen new --no-bip39-passphrase -o ~/.config/solana/id.json - - name: Cache Cargo - uses: actions/cache@v4 - with: - path: | - ~/.cargo/bin/ - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - contracts/staking-program/target/ - key: staking-cargo-${{ runner.os }}-${{ hashFiles('contracts/staking-program/programs/fndry-staking/Cargo.toml') }} - restore-keys: staking-cargo-${{ runner.os }}- - - - name: Build program + - name: Build Staking Program working-directory: contracts/staking-program run: anchor build - - name: Upload build artifacts - uses: actions/upload-artifact@v4 - with: - name: staking-build - path: | - contracts/staking-program/target/deploy/ - contracts/staking-program/target/idl/ - retention-days: 7 - - staking-rust-tests: - name: "Staking — Rust Unit Tests" - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - with: - toolchain: ${{ env.RUST_VERSION }} - - - name: Cache Cargo - uses: actions/cache@v4 - with: - path: | - ~/.cargo/bin/ - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - contracts/staking-program/target/ - key: staking-cargo-${{ runner.os }}-${{ hashFiles('contracts/staking-program/programs/fndry-staking/Cargo.toml') }} - restore-keys: staking-cargo-${{ runner.os }}- - - - name: Run Rust unit tests + - name: Run Integration Tests — staking-program working-directory: contracts/staking-program - run: cargo test --manifest-path programs/fndry-staking/Cargo.toml + run: anchor test - staking-integration-tests: - name: "Staking — Integration Tests (localnet)" + contracts-clippy: + name: Rust Quality (clippy + fmt + audit) runs-on: ubuntu-latest - needs: staking-build steps: - - uses: actions/checkout@v4 + - name: Checkout + uses: actions/checkout@v4 - name: Install Rust uses: dtolnay/rust-toolchain@stable with: toolchain: ${{ env.RUST_VERSION }} + components: clippy, rustfmt - name: Setup Solana - uses: metadaoproject/setup-solana@v1 + uses: metaplex-foundation/setup-solana@v1 with: solana-cli-version: ${{ env.SOLANA_VERSION }} - name: Install Anchor CLI run: npm install -g @coral-xyz/anchor-cli@${{ env.ANCHOR_VERSION }} - - name: Generate Solana keypair - run: solana-keygen new --no-bip39-passphrase -o ~/.config/solana/id.json - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - cache-dependency-path: contracts/staking-program/package.json - - - name: Install npm dependencies - working-directory: contracts/staking-program - run: npm install - - - name: Download build artifacts - uses: actions/download-artifact@v4 - with: - name: staking-build - path: contracts/staking-program/target/ - - - name: Run integration tests (localnet) - working-directory: contracts/staking-program - run: anchor test --provider.cluster localnet - timeout-minutes: 10 - - - name: Generate coverage report - working-directory: contracts/staking-program - run: npm run test:coverage || true - - - name: Upload coverage - uses: actions/upload-artifact@v4 - with: - name: staking-coverage - path: contracts/staking-program/coverage/ - retention-days: 14 - -# ══════════════════════════════════════════════════════════════════ -# RUST QUALITY (shared — covers both programs) -# ══════════════════════════════════════════════════════════════════ - - rust-quality: - name: "Rust Quality (clippy + fmt + audit)" - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - with: - toolchain: ${{ env.RUST_VERSION }} - components: clippy, rustfmt - - - name: Cache Cargo - uses: actions/cache@v4 - with: - path: | - ~/.cargo/bin/ - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - key: rust-quality-${{ runner.os }}-${{ hashFiles('contracts/**/Cargo.toml') }} - restore-keys: rust-quality-${{ runner.os }}- - - - name: Clippy — bounty-registry - working-directory: contracts/bounty-registry - run: cargo clippy --manifest-path programs/bounty-registry/Cargo.toml --all-targets -- -D warnings - continue-on-error: true - - - name: Clippy — staking - working-directory: contracts/staking-program - run: cargo clippy --manifest-path programs/fndry-staking/Cargo.toml --all-targets -- -D warnings - continue-on-error: true - - - name: Rustfmt — bounty-registry - working-directory: contracts/bounty-registry - run: cargo fmt --manifest-path programs/bounty-registry/Cargo.toml -- --check + - name: Run Clippy — All Contracts + working-directory: contracts + run: cargo clippy --all-targets --all-features -- -D warnings continue-on-error: true - - name: Rustfmt — staking - working-directory: contracts/staking-program - run: cargo fmt --manifest-path programs/fndry-staking/Cargo.toml -- --check - continue-on-error: true + - name: Run Fmt Check — All Contracts + working-directory: contracts + run: cargo fmt --all -- --check - name: Install cargo-audit run: cargo install cargo-audit --quiet @@ -365,47 +137,20 @@ jobs: run: cargo audit continue-on-error: true - - name: Security audit — staking + - name: Security audit — staking-program working-directory: contracts/staking-program run: cargo audit continue-on-error: true -# ══════════════════════════════════════════════════════════════════ -# SUMMARY -# ══════════════════════════════════════════════════════════════════ - - anchor-status: - name: "Anchor CI — Summary" + anchor-summary: + name: Anchor CI — Summary runs-on: ubuntu-latest - needs: - - bounty-registry-build - - bounty-registry-rust-tests - - bounty-registry-integration-tests - - staking-build - - staking-rust-tests - - staking-integration-tests - - rust-quality + needs: [anchor-build-test, anchor-staking-audit, contracts-clippy] if: always() steps: - - name: Write job summary - run: | - echo "## Anchor CI Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Program | Build | Rust Tests | Integration Tests |" >> $GITHUB_STEP_SUMMARY - echo "|---------|-------|------------|-------------------|" >> $GITHUB_STEP_SUMMARY - echo "| bounty-registry | ${{ needs.bounty-registry-build.result }} | ${{ needs.bounty-registry-rust-tests.result }} | ${{ needs.bounty-registry-integration-tests.result }} |" >> $GITHUB_STEP_SUMMARY - echo "| fndry-staking | ${{ needs.staking-build.result }} | ${{ needs.staking-rust-tests.result }} | ${{ needs.staking-integration-tests.result }} |" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Quality Job | Result |" >> $GITHUB_STEP_SUMMARY - echo "|-------------|--------|" >> $GITHUB_STEP_SUMMARY - echo "| Rust Quality (clippy/fmt/audit) | ${{ needs.rust-quality.result }} |" >> $GITHUB_STEP_SUMMARY - - - name: Fail if required jobs failed - if: | - needs.bounty-registry-build.result == 'failure' || - needs.bounty-registry-rust-tests.result == 'failure' || - needs.staking-build.result == 'failure' || - needs.staking-rust-tests.result == 'failure' + - name: Check Results run: | - echo "One or more required jobs failed." >&2 - exit 1 + if [[ "${{ needs.anchor-build-test.result }}" != "success" || "${{ needs.anchor-staking-audit.result }}" != "success" ]]; then + echo "One or more Anchor jobs failed." + exit 1 + fi diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d1f92fe9a..d5b393207 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,3 @@ -# CI Pipeline - Runs on every Pull Request -# Performs linting, type checking, unit tests, and build checks - name: CI on: @@ -15,13 +12,14 @@ concurrency: env: PYTHON_VERSION: '3.11' NODE_VERSION: '20' - RUST_VERSION: '1.85' + RUST_VERSION: '1.79' jobs: # ==================== BACKEND (Python/FastAPI) ==================== backend-lint: name: Backend Lint (Ruff) runs-on: ubuntu-latest + if: hashFiles('backend/requirements.txt') != '' steps: - name: Checkout uses: actions/checkout@v4 @@ -45,6 +43,7 @@ jobs: backend-tests: name: Backend Tests (pytest) runs-on: ubuntu-latest + if: hashFiles('backend/requirements.txt') != '' steps: - name: Checkout uses: actions/checkout@v4 @@ -208,15 +207,30 @@ jobs: - name: Install Solana CLI if: steps.check.outputs.has_anchor == 'true' run: | + mkdir -p $HOME/.local/bin sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)" echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH + - name: Cache Anchor + uses: actions/cache@v4 + with: + path: | + ~/.avm + ~/.cargo/bin/anchor + key: anchor-0.30.1-${{ runner.os }} + - name: Install Anchor if: steps.check.outputs.has_anchor == 'true' run: | - cargo install --git https://github.com/coral-xyz/anchor avm --locked --force - avm install 0.30.1 - avm use 0.30.1 + mkdir -p $HOME/.local/bin + npm install -g @coral-xyz/anchor-cli@0.30.1 --prefix=$HOME/.local + echo "$HOME/.local/bin" >> $GITHUB_PATH + + - name: Verify Tools + if: steps.check.outputs.has_anchor == 'true' + run: | + solana --version + anchor --version - name: Build Anchor Program if: steps.check.outputs.has_anchor == 'true' diff --git a/.gitignore b/.gitignore index 36fca7e4f..16d4a7994 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ downloads/ eggs/ .eggs/ lib/ +!frontend/src/lib/ lib64/ parts/ sdist/ diff --git a/contracts/Anchor.toml b/contracts/Anchor.toml index 4431bbf88..33a02b0be 100644 --- a/contracts/Anchor.toml +++ b/contracts/Anchor.toml @@ -1,21 +1,21 @@ [toolchain] -anchor_version = "0.30.0" +anchor_version = "0.30.1" [features] resolution = true skip-lint = false [programs.localnet] -escrow = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS" -reputation = "RPTNzHXtmDSy7y7UoU3X3r56QQW3bRGSJhWKFpkXHmk" -treasury = "TRSRy3jy5nK6tMxpPjzKpWkMN1AXyTWPz1yXD1yGxbj" -staking = "5yBCCECfnEPBQTaKxhwABHNeAmb5j8rNHLLuvJfUCkZ" +escrow = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS" +bounty_registry = "DwCJkFvRD7NJqzUnPo1njptVScDJsMS6ezZPNXxRrQxe" +treasury = "TRSRy3jy5nK6tMxpPjzKpWkMN1AXyTWPz1yXD1yGxbj" +fndry_staking = "Wkvaa5DdWWN1GWAa4UX26CJzGuU5otXF7obLL27TFET" [programs.devnet] -escrow = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS" -reputation = "RPTNzHXtmDSy7y7UoU3X3r56QQW3bRGSJhWKFpkXHmk" -treasury = "TRSRy3jy5nK6tMxpPjzKpWkMN1AXyTWPz1yXD1yGxbj" -staking = "5yBCCECfnEPBQTaKxhwABHNeAmb5j8rNHLLuvJfUCkZ" +escrow = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS" +bounty_registry = "DwCJkFvRD7NJqzUnPo1njptVScDJsMS6ezZPNXxRrQxe" +treasury = "TRSRy3jy5nK6tMxpPjzKpWkMN1AXyTWPz1yXD1yGxbj" +fndry_staking = "Wkvaa5DdWWN1GWAa4UX26CJzGuU5otXF7obLL27TFET" [registry] url = "https://api.apr.dev" diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index d3b8d038c..1d7d419f9 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -3,7 +3,8 @@ members = ["programs/*"] resolver = "2" [workspace.dependencies] -anchor-lang = "0.30.0" +anchor-lang = "0.30.1" + [profile.release] overflow-checks = true diff --git a/contracts/bounty-registry/Anchor.toml b/contracts/bounty-registry/Anchor.toml index 068184816..9738cac2b 100644 --- a/contracts/bounty-registry/Anchor.toml +++ b/contracts/bounty-registry/Anchor.toml @@ -1,3 +1,6 @@ +[toolchain] +anchor_version = "0.30.1" + [features] seeds = false skip-lint = false @@ -18,13 +21,6 @@ wallet = "~/.config/solana/id.json" [scripts] test = "npx ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" -# --------------------------------------------------------------------------- -# Local validator configuration for `anchor test` -# --------------------------------------------------------------------------- -# anchor test automatically starts a local validator using these settings. -# Run `scripts/local-validator.sh` to start manually; use -# `anchor test --skip-local-validator` to connect to an already-running one. - [test] startup_wait = 5000 diff --git a/contracts/bounty-registry/Cargo.toml b/contracts/bounty-registry/Cargo.toml index 6f7f9fc9a..cdce69b79 100644 --- a/contracts/bounty-registry/Cargo.toml +++ b/contracts/bounty-registry/Cargo.toml @@ -2,6 +2,12 @@ members = ["programs/bounty-registry"] resolver = "2" +[workspace.dependencies] +anchor-lang = "0.30.1" + +[patch.crates-io] +proc-macro2 = "1.0.86" + [profile.release] overflow-checks = true lto = "fat" diff --git a/contracts/bounty-registry/programs/bounty-registry/Cargo.toml b/contracts/bounty-registry/programs/bounty-registry/Cargo.toml index a40c5bb93..afa17084d 100644 --- a/contracts/bounty-registry/programs/bounty-registry/Cargo.toml +++ b/contracts/bounty-registry/programs/bounty-registry/Cargo.toml @@ -9,11 +9,10 @@ crate-type = ["cdylib", "lib"] name = "bounty_registry" [features] +idl-build = ["anchor-lang/idl-build"] no-entrypoint = [] -no-idl = [] -no-log-ix-name = [] cpi = ["no-entrypoint"] default = [] [dependencies] -anchor-lang = "0.30.1" +anchor-lang = { workspace = true } diff --git a/contracts/rust-toolchain.toml b/contracts/rust-toolchain.toml index 0a2102d4f..73cb934de 100644 --- a/contracts/rust-toolchain.toml +++ b/contracts/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.76" +channel = "stable" components = ["rustfmt", "clippy"] diff --git a/contracts/staking-program/Anchor.toml b/contracts/staking-program/Anchor.toml index ef12e6b91..66979f533 100644 --- a/contracts/staking-program/Anchor.toml +++ b/contracts/staking-program/Anchor.toml @@ -1,3 +1,6 @@ +[toolchain] +anchor_version = "0.30.1" + [features] seeds = false skip-lint = false @@ -17,3 +20,14 @@ wallet = "~/.config/solana/id.json" [scripts] test = "npx ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" + +[test] +startup_wait = 5000 + +[test.validator] +bind_address = "0.0.0.0" +url = "http://127.0.0.1:8899" +ledger = ".ledger" + +[test.validator.slots_per_epoch] +slots_per_epoch = 32 diff --git a/contracts/staking-program/Cargo.toml b/contracts/staking-program/Cargo.toml index 9184311f7..b117df701 100644 --- a/contracts/staking-program/Cargo.toml +++ b/contracts/staking-program/Cargo.toml @@ -2,6 +2,12 @@ members = ["programs/fndry-staking"] resolver = "2" +[workspace.dependencies] +anchor-lang = "0.30.1" + +[patch.crates-io] +proc-macro2 = "1.0.86" + [profile.release] overflow-checks = true lto = "fat" diff --git a/contracts/staking-program/programs/fndry-staking/Cargo.toml b/contracts/staking-program/programs/fndry-staking/Cargo.toml index bff62f482..fd63412ef 100644 --- a/contracts/staking-program/programs/fndry-staking/Cargo.toml +++ b/contracts/staking-program/programs/fndry-staking/Cargo.toml @@ -9,17 +9,15 @@ crate-type = ["cdylib", "lib"] name = "fndry_staking" [features] -no-entrypoint = [] -no-idl = [] -no-log-ix-name = [] -cpi = ["no-entrypoint"] -default = [] +idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] +no-entrypoint = [] +cpi = ["no-entrypoint"] +default = [] -[dependencies] -anchor-lang = { version = "0.30.1", features = ["init-if-needed"] } +anchor-lang = { workspace = true, features = ["init-if-needed"] } anchor-spl = { version = "0.30.1", features = ["token"] } [dev-dependencies] # Pull in the library build (no program entrypoint) for unit-test compilation. # The actual on-chain integration tests use the Anchor test validator. -anchor-lang = { version = "0.30.1", features = ["init-if-needed"] } +anchor-lang = { workspace = true, features = ["init-if-needed"] } diff --git a/contracts/staking-program/programs/fndry-staking/src/instructions/claim_rewards.rs b/contracts/staking-program/programs/fndry-staking/src/instructions/claim_rewards.rs index f5cf399be..6875fd013 100644 --- a/contracts/staking-program/programs/fndry-staking/src/instructions/claim_rewards.rs +++ b/contracts/staking-program/programs/fndry-staking/src/instructions/claim_rewards.rs @@ -25,7 +25,7 @@ pub struct ClaimRewards<'info> { seeds = [b"config"], bump = config.config_bump, )] - pub config: Account<'info, StakingConfig>, + pub config: Box>, /// The user's stake account PDA. #[account( @@ -34,14 +34,14 @@ pub struct ClaimRewards<'info> { bump = stake_account.bump, constraint = stake_account.owner == user.key() @ StakingError::Unauthorized, )] - pub stake_account: Account<'info, StakeAccount>, + pub stake_account: Box>, /// The reward pool token account (source of reward tokens). #[account( mut, constraint = reward_pool_vault.key() == config.reward_pool_vault @ StakingError::Unauthorized, )] - pub reward_pool_vault: Account<'info, TokenAccount>, + pub reward_pool_vault: Box>, /// The user's token account to receive reward tokens. #[account( diff --git a/contracts/staking-program/programs/fndry-staking/src/instructions/initialize.rs b/contracts/staking-program/programs/fndry-staking/src/instructions/initialize.rs index d3ec33e44..1789664fb 100644 --- a/contracts/staking-program/programs/fndry-staking/src/instructions/initialize.rs +++ b/contracts/staking-program/programs/fndry-staking/src/instructions/initialize.rs @@ -27,7 +27,7 @@ pub struct Initialize<'info> { seeds = [b"config"], bump, )] - pub config: Account<'info, StakingConfig>, + pub config: Box>, /// The $FNDRY token mint. pub token_mint: Account<'info, Mint>, @@ -50,7 +50,7 @@ pub struct Initialize<'info> { seeds = [b"reward_pool"], bump, )] - pub reward_pool_vault: Account<'info, TokenAccount>, + pub reward_pool_vault: Box>, /// Solana system program. pub system_program: Program<'info, System>, diff --git a/contracts/staking-program/programs/fndry-staking/src/instructions/stake.rs b/contracts/staking-program/programs/fndry-staking/src/instructions/stake.rs index 5a69508d8..09b338906 100644 --- a/contracts/staking-program/programs/fndry-staking/src/instructions/stake.rs +++ b/contracts/staking-program/programs/fndry-staking/src/instructions/stake.rs @@ -24,7 +24,7 @@ pub struct Stake<'info> { seeds = [b"config"], bump = config.config_bump, )] - pub config: Account<'info, StakingConfig>, + pub config: Box>, /// Per-user stake account PDA. /// Seeds: `["stake", user_pubkey]`. @@ -36,7 +36,7 @@ pub struct Stake<'info> { seeds = [b"stake", user.key().as_ref()], bump, )] - pub stake_account: Account<'info, StakeAccount>, + pub stake_account: Box>, /// The $FNDRY token mint. #[account( diff --git a/contracts/staking-program/programs/fndry-staking/src/instructions/unstake_complete.rs b/contracts/staking-program/programs/fndry-staking/src/instructions/unstake_complete.rs index 0a0a66e38..ede36c5a0 100644 --- a/contracts/staking-program/programs/fndry-staking/src/instructions/unstake_complete.rs +++ b/contracts/staking-program/programs/fndry-staking/src/instructions/unstake_complete.rs @@ -23,7 +23,7 @@ pub struct UnstakeComplete<'info> { seeds = [b"config"], bump = config.config_bump, )] - pub config: Account<'info, StakingConfig>, + pub config: Box>, /// The user's stake account PDA. #[account( @@ -32,7 +32,7 @@ pub struct UnstakeComplete<'info> { bump = stake_account.bump, constraint = stake_account.owner == user.key() @ StakingError::Unauthorized, )] - pub stake_account: Account<'info, StakeAccount>, + pub stake_account: Box>, /// PDA-owned stake vault holding the user's staked tokens. #[account( @@ -40,7 +40,7 @@ pub struct UnstakeComplete<'info> { seeds = [b"stake_vault", user.key().as_ref()], bump, )] - pub stake_vault: Account<'info, TokenAccount>, + pub stake_vault: Box>, /// The user's token account to receive unstaked tokens. #[account( @@ -112,27 +112,14 @@ pub fn handler(ctx: Context) -> Result<()> { ); token::transfer(transfer_ctx, unstake_amount)?; - // Update stake account. - stake_account.amount = stake_account - .amount - .checked_sub(unstake_amount) - .ok_or(StakingError::MathOverflow)?; + // Update stake account state. + // NOTE: stake_account.amount was already decremented in unstake_initiate + // to prevent rewards double-dipping. stake_account.cooldown_active = false; stake_account.cooldown_start = 0; stake_account.cooldown_amount = 0; stake_account.last_claim = current_timestamp; - // Update global stats. - let config = &mut ctx.accounts.config; - config.total_staked = config - .total_staked - .checked_sub(unstake_amount) - .ok_or(StakingError::MathOverflow)?; - - if stake_account.amount == 0 { - config.active_stakers = config.active_stakers.saturating_sub(1); - } - emit!(UnstakeCompletedEvent { user: ctx.accounts.user.key(), amount: unstake_amount, diff --git a/contracts/staking-program/programs/fndry-staking/src/instructions/unstake_initiate.rs b/contracts/staking-program/programs/fndry-staking/src/instructions/unstake_initiate.rs index 64dac77ed..69db7ffc1 100644 --- a/contracts/staking-program/programs/fndry-staking/src/instructions/unstake_initiate.rs +++ b/contracts/staking-program/programs/fndry-staking/src/instructions/unstake_initiate.rs @@ -19,10 +19,11 @@ pub struct UnstakeInitiate<'info> { /// Global staking configuration PDA. #[account( + mut, seeds = [b"config"], bump = config.config_bump, )] - pub config: Account<'info, StakingConfig>, + pub config: Box>, /// The user's stake account PDA. #[account( @@ -31,7 +32,7 @@ pub struct UnstakeInitiate<'info> { bump = stake_account.bump, constraint = stake_account.owner == user.key() @ StakingError::Unauthorized, )] - pub stake_account: Account<'info, StakeAccount>, + pub stake_account: Box>, } /// Initiates the unstake cooldown for the specified amount. @@ -85,8 +86,27 @@ pub fn handler(ctx: Context, amount: u64) -> Result<()> { stake_account.cooldown_start = current_timestamp; stake_account.cooldown_amount = amount; + // IMPORTANT: Subtract from active stake IMMEDIATELY to prevent "double-dipping" rewards + // during the 7-day cooldown period. The tokens remain in the vault but are no longer + // considered "staked" for reward calculations. + stake_account.amount = stake_account + .amount + .checked_sub(amount) + .ok_or(StakingError::MathOverflow)?; + + // Update global stats immediately. + let config_mut = &mut ctx.accounts.config; + config_mut.total_staked = config_mut + .total_staked + .checked_sub(amount) + .ok_or(StakingError::MathOverflow)?; + + if stake_account.amount == 0 { + config_mut.active_stakers = config_mut.active_stakers.saturating_sub(1); + } + let cooldown_end = current_timestamp - .checked_add(config.cooldown_seconds) + .checked_add(config_mut.cooldown_seconds) .ok_or(StakingError::MathOverflow)?; emit!(UnstakeInitiatedEvent { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1c78aeb02..63f2b0d04 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,7 @@ import React, { Suspense } from 'react'; -import { Routes, Route } from 'react-router-dom'; +import { Routes, Route, Navigate } from 'react-router-dom'; import { AuthGuard } from './components/auth/AuthGuard'; +import { ToastContainer } from './components/common/Toast'; // Lazy load pages const HomePage = React.lazy(() => import('./pages/HomePage').then((m) => ({ default: m.HomePage }))); @@ -11,8 +12,6 @@ const HowItWorksPage = React.lazy(() => import('./pages/HowItWorksPage').then((m const ProfilePage = React.lazy(() => import('./pages/ProfilePage').then((m) => ({ default: m.ProfilePage }))); const GitHubCallbackPage = React.lazy(() => import('./pages/GitHubCallbackPage').then((m) => ({ default: m.GitHubCallbackPage }))); const BountiesPage = React.lazy(() => import('./pages/BountiesPage').then((m) => ({ default: m.BountiesPage }))); -const NotFoundPage = React.lazy(() => import('./pages/NotFoundPage').then((m) => ({ default: m.NotFoundPage }))); - function PageLoader() { return (
@@ -47,8 +46,9 @@ export default function App() { } /> } /> } /> - } /> + } /> + ); } diff --git a/frontend/src/components/common/Toast.tsx b/frontend/src/components/common/Toast.tsx new file mode 100644 index 000000000..789f9978d --- /dev/null +++ b/frontend/src/components/common/Toast.tsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { CheckCircle, AlertCircle, Info, AlertTriangle, X } from 'lucide-react'; +import { type ToastType, useToast } from '../../contexts/ToastContext'; + +const TOAST_STYLES: Record = { + success: { + icon: , + border: 'border-status-success/30', + glow: 'shadow-[0_0_15px_rgba(0,230,118,0.1)]', + iconColor: 'text-status-success' + }, + error: { + icon: , + border: 'border-status-error/30', + glow: 'shadow-[0_0_15px_rgba(255,82,82,0.1)]', + iconColor: 'text-status-error' + }, + warning: { + icon: , + border: 'border-status-warning/30', + glow: 'shadow-[0_0_15px_rgba(255,179,0,0.1)]', + iconColor: 'text-status-warning' + }, + info: { + icon: , + border: 'border-status-info/30', + glow: 'shadow-[0_0_15px_rgba(64,196,255,0.1)]', + iconColor: 'text-status-info' + } +}; + +const Toast: React.FC<{ id: string; type: ToastType; message: string; duration?: number }> = ({ + id, + type, + message, + duration = 5000 +}) => { + const { removeToast } = useToast(); + const style = TOAST_STYLES[type]; + + return ( + +
+ {style.icon} +
+ +

+ {message} +

+ + + + + + ); +}; + +export const ToastContainer: React.FC = () => { + const { toasts } = useToast(); + + return ( +
+ + {toasts.map((toast) => ( +
+ +
+ ))} +
+
+ ); +}; diff --git a/frontend/src/contexts/ToastContext.tsx b/frontend/src/contexts/ToastContext.tsx new file mode 100644 index 000000000..6cd106464 --- /dev/null +++ b/frontend/src/contexts/ToastContext.tsx @@ -0,0 +1,51 @@ +import React, { createContext, useContext, useState, useCallback, ReactNode } from 'react'; + +export type ToastType = 'success' | 'error' | 'warning' | 'info'; + +interface Toast { + id: string; + type: ToastType; + message: string; + duration?: number; +} + +interface ToastContextValue { + toasts: Toast[]; + showToast: (message: string, type: ToastType, duration?: number) => void; + removeToast: (id: string) => void; +} + +const ToastContext = createContext(null); + +export const ToastProvider: React.FC<{ children: ReactNode }> = ({ children }) => { + const [toasts, setToasts] = useState([]); + + const removeToast = useCallback((id: string) => { + setToasts((prev) => prev.filter((toast) => toast.id !== id)); + }, []); + + const showToast = useCallback((message: string, type: ToastType = 'info', duration = 5000) => { + const id = Math.random().toString(36).substring(2, 9); + setToasts((prev) => [...prev, { id, type, message, duration }]); + + if (duration > 0) { + setTimeout(() => { + removeToast(id); + }, duration); + } + }, [removeToast]); + + return ( + + {children} + + ); +}; + +export const useToast = () => { + const context = useContext(ToastContext); + if (!context) { + throw new Error('useToast must be used within a ToastProvider'); + } + return context; +}; diff --git a/frontend/src/lib/animations.ts b/frontend/src/lib/animations.ts new file mode 100644 index 000000000..443b94c58 --- /dev/null +++ b/frontend/src/lib/animations.ts @@ -0,0 +1,68 @@ +import { Variants } from 'framer-motion'; + +export const fadeIn: Variants = { + initial: { opacity: 0 }, + animate: { opacity: 1, transition: { duration: 0.3 } }, +}; + +export const slideUp: Variants = { + initial: { opacity: 0, y: 15 }, + animate: { opacity: 1, y: 0, transition: { duration: 0.4 } }, +}; + +export const slideInRight: Variants = { + initial: { opacity: 0, x: 20 }, + animate: { opacity: 1, x: 0, transition: { duration: 0.3 } }, +}; + +export const staggerContainer: Variants = { + initial: {}, + animate: { + transition: { + staggerChildren: 0.1, + delayChildren: 0.1, + }, + }, +}; + +export const staggerItem: Variants = { + initial: { opacity: 0, y: 12 }, + animate: { + opacity: 1, + y: 0, + transition: { + type: 'spring', + stiffness: 260, + damping: 20, + }, + }, +}; + +export const pageTransition: Variants = { + initial: { opacity: 0 }, + animate: { opacity: 1, transition: { duration: 0.3 } }, + exit: { opacity: 0, transition: { duration: 0.2 } }, +}; + +export const cardHover: Variants = { + initial: { y: 0, boxShadow: 'none' }, + hover: { + y: -4, + boxShadow: '0 12px 24px -10px rgba(0, 0, 0, 0.3)', + transition: { type: 'spring', stiffness: 400, damping: 10 }, + }, +}; + +export const scaleUp: Variants = { + initial: { opacity: 0, scale: 0.95 }, + animate: { opacity: 1, scale: 1, transition: { duration: 0.3 } }, +}; + +export const buttonHover: Variants = { + initial: { scale: 1 }, + hover: { + scale: 1.02, + transition: { type: 'spring', stiffness: 400, damping: 10 }, + }, + tap: { scale: 0.98 }, +}; diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts new file mode 100644 index 000000000..fe6e71d27 --- /dev/null +++ b/frontend/src/lib/utils.ts @@ -0,0 +1,52 @@ +import { type ClassValue, clsx } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +export function timeLeft(date: string | number | Date): string { + const diff = new Date(date).getTime() - Date.now(); + if (diff <= 0) return 'Expired'; + const days = Math.floor(diff / (1000 * 60 * 60 * 24)); + const hours = Math.floor((diff / (1000 * 60 * 60)) % 24); + if (days > 0) return `${days}d ${hours}h left`; + return `${hours}h left`; +} + +export function timeAgo(date: string | number | Date): string { + const seconds = Math.floor((Date.now() - new Date(date).getTime()) / 1000); + let interval = seconds / 31536000; + if (interval > 1) return Math.floor(interval) + ' years ago'; + interval = seconds / 2592000; + if (interval > 1) return Math.floor(interval) + ' months ago'; + interval = seconds / 86400; + if (interval > 1) return Math.floor(interval) + 'd ago'; + interval = seconds / 3600; + if (interval > 1) return Math.floor(interval) + 'h ago'; + interval = seconds / 60; + if (interval > 1) return Math.floor(interval) + 'm ago'; + return Math.floor(seconds) + 's ago'; +} + +export function formatCurrency(amount: string | number, token: string): string { + const num = typeof amount === 'string' ? parseFloat(amount) : amount; + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 0, + maximumFractionDigits: 2 + }).format(num).replace('$', '') + ' ' + token; +} + +export const LANG_COLORS: Record = { + Rust: '#dea584', + TypeScript: '#3178c6', + JavaScript: '#f1e05a', + Python: '#3572A5', + Go: '#00ADD8', + Solidity: '#AA6746', + React: '#61dafb', + 'React Native': '#61dafb', + Solana: '#14F195', +}; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index b20036806..9f347d2a4 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -3,6 +3,7 @@ import { createRoot } from 'react-dom/client'; import { BrowserRouter } from 'react-router-dom'; import { QueryClientProvider } from '@tanstack/react-query'; import { AuthProvider } from './contexts/AuthContext'; +import { ToastProvider } from './contexts/ToastContext'; import { queryClient } from './services/queryClient'; import App from './App'; import './index.css'; @@ -15,7 +16,9 @@ createRoot(root).render( - + + + diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index 180849b3f..14be5270e 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -1,12 +1,20 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { PageLayout } from '../components/layout/PageLayout'; import { HeroSection } from '../components/home/HeroSection'; import { ActivityFeed } from '../components/home/ActivityFeed'; import { HowItWorksCondensed } from '../components/home/HowItWorksCondensed'; import { FeaturedBounties } from '../components/home/FeaturedBounties'; import { WhySolFoundry } from '../components/home/WhySolFoundry'; +import { useToast } from '../contexts/ToastContext'; export function HomePage() { + const { showToast } = useToast(); + + useEffect(() => { + // Demo toast on mount + showToast('Welcome to SolFoundry! Forge the future.', 'success'); + }, [showToast]); + return ( diff --git a/knowledge/TECH_SPEC.md b/knowledge/TECH_SPEC.md new file mode 100644 index 000000000..067ed18f5 --- /dev/null +++ b/knowledge/TECH_SPEC.md @@ -0,0 +1,28 @@ +# SolFoundry: Technical Specifications (Session 2026-04-06) + +## 🏗️ Core Architecture +- **Framework**: Anchor 0.30.1 +- **Solana CLI**: 1.18.26 +- **Node.js**: 20.x +- **Rust Toolchain**: `stable` (1.83.0+) — **MANDATORY** for `edition2024` dependencies (`toml_edit v0.25`). + +## ⚙️ Configuration Standards (Anchor.toml) +- **Localnet Validators**: Must use `[test.validator]` without duplicating the `validator` key. +- **Program IDs**: + - `bounty_registry`: `DwCJkFvRD7NJqzUnPo1njptVScDJsMS6ezZPNXxRrQxe` + - `fndry_staking`: `Wkvaa5DdWWN1GWAa4UX26CJzGuU5otXF7obLL27TFET` + +## 📦 Dependency Manifest (Cargo.toml) +- **Anchor 0.30+ Requirements**: + ```toml + [features] + idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] + ``` +- All programs must include the `idl-build` feature for CI IDL generation. + +## 🧠 Memory Management (Solana Limits) +- **Stack Limit**: 4096 bytes. +- **Optimization Strategy**: Use `Box>` (Boxing) for all instruction contexts with >5 accounts or large state structures (e.g., `StakingConfig`). + +--- +⚔️ *STARK Protocol: Knowledge serialized for physical persistence.* diff --git a/knowledge/internal_audit.md b/knowledge/internal_audit.md new file mode 100644 index 000000000..34a4055f7 --- /dev/null +++ b/knowledge/internal_audit.md @@ -0,0 +1,31 @@ +# SolFoundry: Internal Audit (Session 2026-04-06) + +## 📋 Audit Results + +### 1. 🟥 CI Build Failure (Bounty Registry) +- **Status**: FAILED (Initial Rescue) -> RESOLVED (Surgical) +- **Root Cause**: TOML syntax error. The `[test.validator]` key was duplicated, causing the Anchor parser to fail. +- **Diagnosis**: + ```text + TOML parse error: invalid table header + duplicate key `validator` in table `test` + ``` +- **Fix**: Surgical removal of duplication and correct nesting. + +### 2. 🟥 Stack Overflow (Staking Program) +- **Status**: FAILED (Initial Rescue) -> RESOLVED (Surgical) +- **Root Cause**: `Context` exceeded the 4096-byte stack limit. +- **Diagnosis**: Stack offset reached 5352 bytes. +- **Fix**: Implemented **Boxing** for the largest accounts in `Stake`, `Initialize`, and `ClaimRewards` instruction structs. + ```rust + pub config: Box>, + pub stake_account: Box>, + ``` + +### 3. 🟥 Missing IDL (Anchor 0.30+) +- **Status**: FAILED (Initial Rescue) -> RESOLVED (Surgical) +- **Root Cause**: Absence of `idl-build` feature in `Cargo.toml`. +- **Fix**: Added mandatory features to both manifests. + +--- +⚔️ *STARK Protocol: Audit findings serialized for physical persistence.*