feat(distribution): exact O(1) payouts in token units, and fix the O(n^2) basis-point path #213
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: PR CI | |
| on: | |
| pull_request: | |
| types: [opened, synchronize, reopened, ready_for_review] | |
| concurrency: | |
| group: pr-ci-${{ github.event.pull_request.number }} | |
| cancel-in-progress: true | |
| jobs: | |
| changes: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| outputs: | |
| packages: ${{ steps.filter.outputs.packages }} | |
| has_tests: ${{ steps.compute-matrix.outputs.has_tests }} | |
| need_egs: ${{ steps.compute-matrix.outputs.need_egs }} | |
| need_metagame: ${{ steps.compute-matrix.outputs.need_metagame }} | |
| need_economy: ${{ steps.compute-matrix.outputs.need_economy }} | |
| need_utilities: ${{ steps.compute-matrix.outputs.need_utilities }} | |
| need_presets: ${{ steps.compute-matrix.outputs.need_presets }} | |
| infra_ci: ${{ steps.filter.outputs.infra_ci }} | |
| general_ci: ${{ steps.filter.outputs.general_ci }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: dorny/paths-filter@v3 | |
| id: filter | |
| with: | |
| filters: | | |
| packages: | |
| - "packages/**" | |
| - "Scarb.toml" | |
| - "Scarb.lock" | |
| - ".tool-versions" | |
| general_ci: | |
| - "codecov.yml" | |
| - ".tool-versions" | |
| infra_ci: | |
| - ".github/workflows/**" | |
| - ".github/prompts/**" | |
| - ".github/pull_request_template.md" | |
| embeddable_game_standard: | |
| - "packages/embeddable_game_standard/**" | |
| metagame_pkg: | |
| - "packages/metagame/**" | |
| economy: | |
| - "packages/economy/**" | |
| utilities: | |
| - "packages/utilities/**" | |
| interfaces: | |
| - "packages/interfaces/**" | |
| presets: | |
| - "packages/presets/**" | |
| testing: | |
| - "packages/testing/**" | |
| test_common: | |
| - "packages/test_common/**" | |
| tool_versions: | |
| - ".tool-versions" | |
| workspace_manifests: | |
| - "Scarb.toml" | |
| - "Scarb.lock" | |
| - name: Compute test matrix from changed packages | |
| id: compute-matrix | |
| env: | |
| FILTER_INTERFACES: ${{ steps.filter.outputs.interfaces }} | |
| FILTER_TESTING: ${{ steps.filter.outputs.testing }} | |
| FILTER_UTILITIES: ${{ steps.filter.outputs.utilities }} | |
| FILTER_METAGAME_PKG: ${{ steps.filter.outputs.metagame_pkg }} | |
| FILTER_ECONOMY: ${{ steps.filter.outputs.economy }} | |
| FILTER_EGS: ${{ steps.filter.outputs.embeddable_game_standard }} | |
| FILTER_TEST_COMMON: ${{ steps.filter.outputs.test_common }} | |
| FILTER_PRESETS: ${{ steps.filter.outputs.presets }} | |
| FILTER_TOOL_VERSIONS: ${{ steps.filter.outputs.tool_versions }} | |
| FILTER_WORKSPACE_MANIFESTS: ${{ steps.filter.outputs.workspace_manifests }} | |
| run: | | |
| # Determine which group packages need testing based on transitive deps | |
| # interfaces & testing & .tool-versions → ALL | |
| # utilities → utilities + metagame + embeddable_game_standard + presets | |
| # metagame_pkg → metagame + embeddable_game_standard + presets | |
| # economy → economy + presets | |
| # embeddable_game_standard → embeddable_game_standard + presets | |
| # test_common → embeddable_game_standard + metagame + presets | |
| # presets → presets | |
| RUN_ALL="false" | |
| NEED_EGS="false" | |
| NEED_METAGAME="false" | |
| NEED_ECONOMY="false" | |
| NEED_UTILITIES="false" | |
| NEED_PRESETS="false" | |
| if [ "$FILTER_INTERFACES" = "true" ] || [ "$FILTER_TESTING" = "true" ] || [ "$FILTER_TOOL_VERSIONS" = "true" ] || [ "$FILTER_WORKSPACE_MANIFESTS" = "true" ]; then | |
| RUN_ALL="true" | |
| fi | |
| if [ "$RUN_ALL" != "true" ]; then | |
| if [ "$FILTER_UTILITIES" = "true" ]; then | |
| NEED_UTILITIES="true" | |
| NEED_METAGAME="true" | |
| NEED_EGS="true" | |
| NEED_PRESETS="true" | |
| fi | |
| if [ "$FILTER_METAGAME_PKG" = "true" ]; then | |
| NEED_METAGAME="true" | |
| NEED_EGS="true" | |
| NEED_PRESETS="true" | |
| fi | |
| if [ "$FILTER_ECONOMY" = "true" ]; then | |
| NEED_ECONOMY="true" | |
| NEED_PRESETS="true" | |
| fi | |
| if [ "$FILTER_EGS" = "true" ]; then | |
| NEED_EGS="true" | |
| NEED_PRESETS="true" | |
| fi | |
| if [ "$FILTER_TEST_COMMON" = "true" ]; then | |
| NEED_EGS="true" | |
| NEED_METAGAME="true" | |
| NEED_PRESETS="true" | |
| fi | |
| if [ "$FILTER_PRESETS" = "true" ]; then | |
| NEED_PRESETS="true" | |
| fi | |
| fi | |
| if [ "$RUN_ALL" = "true" ]; then | |
| NEED_EGS="true" | |
| NEED_METAGAME="true" | |
| NEED_ECONOMY="true" | |
| NEED_UTILITIES="true" | |
| NEED_PRESETS="true" | |
| fi | |
| HAS_TESTS="false" | |
| for flag in "$NEED_EGS" "$NEED_METAGAME" "$NEED_ECONOMY" "$NEED_UTILITIES" "$NEED_PRESETS"; do | |
| if [ "$flag" = "true" ]; then | |
| HAS_TESTS="true" | |
| break | |
| fi | |
| done | |
| echo "has_tests=$HAS_TESTS" >> "$GITHUB_OUTPUT" | |
| echo "need_egs=$NEED_EGS" >> "$GITHUB_OUTPUT" | |
| echo "need_metagame=$NEED_METAGAME" >> "$GITHUB_OUTPUT" | |
| echo "need_economy=$NEED_ECONOMY" >> "$GITHUB_OUTPUT" | |
| echo "need_utilities=$NEED_UTILITIES" >> "$GITHUB_OUTPUT" | |
| echo "need_presets=$NEED_PRESETS" >> "$GITHUB_OUTPUT" | |
| echo "Packages to test: egs=$NEED_EGS metagame=$NEED_METAGAME economy=$NEED_ECONOMY utilities=$NEED_UTILITIES presets=$NEED_PRESETS" | |
| infra-validate: | |
| needs: changes | |
| if: needs.changes.outputs.infra_ci == 'true' || needs.changes.outputs.general_ci == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Validate workflow YAML | |
| run: | | |
| YAML_CLI="yaml@2.8.1" | |
| for file in .github/workflows/*.yml .github/workflows/*.yaml; do | |
| [ -f "$file" ] || continue | |
| npx -y "$YAML_CLI" valid < "$file" | |
| done | |
| if [ -f codecov.yml ]; then | |
| npx -y "$YAML_CLI" valid < codecov.yml | |
| fi | |
| - name: Validate package count matches codecov config | |
| run: | | |
| matrix_count=$(grep -cE '^\s+module:' .github/workflows/main-ci.yml) | |
| codecov_count=$(grep 'after_n_builds:' codecov.yml | grep -oE '[0-9]+') | |
| echo "Matrix modules: $matrix_count" | |
| echo "Codecov after_n_builds: $codecov_count" | |
| if [ "$matrix_count" != "$codecov_count" ]; then | |
| echo "::error::Matrix has $matrix_count modules but codecov.yml expects $codecov_count builds" | |
| echo "::error::Update codecov.yml after_n_builds to match module count" | |
| exit 1 | |
| fi | |
| echo "Configuration validated: $matrix_count modules" | |
| lint: | |
| needs: changes | |
| if: needs.changes.outputs.packages == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Setup Scarb | |
| uses: software-mansion/setup-scarb@v1 | |
| with: | |
| tool-versions: .tool-versions | |
| cache: false | |
| - name: Scarb fmt | |
| run: scarb fmt --check --workspace | |
| setup: | |
| needs: [changes, lint] | |
| if: needs.changes.outputs.has_tests == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| outputs: | |
| test_matrix: ${{ steps.build-matrix.outputs.test_matrix }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Build test matrix from package flags | |
| id: build-matrix | |
| env: | |
| NEED_EGS: ${{ needs.changes.outputs.need_egs }} | |
| NEED_METAGAME: ${{ needs.changes.outputs.need_metagame }} | |
| NEED_ECONOMY: ${{ needs.changes.outputs.need_economy }} | |
| NEED_UTILITIES: ${{ needs.changes.outputs.need_utilities }} | |
| NEED_PRESETS: ${{ needs.changes.outputs.need_presets }} | |
| run: | | |
| # Build JSON matrix from per-package boolean flags | |
| # Module catalog must match main-ci.yml | |
| INCLUDES="[]" | |
| add() { INCLUDES=$(echo "$INCLUDES" | jq -c --arg p "$1" --arg m "$2" --arg r "$3" --argjson f "$4" '. + [{package:$p,module:$m,runner:$r,fuzzer_runs:$f}]'); } | |
| if [ "$NEED_EGS" = "true" ]; then | |
| add game_components_embeddable_game_standard token ubuntu-latest-32 32 | |
| add game_components_embeddable_game_standard minigame ubuntu-latest-8 32 | |
| add game_components_embeddable_game_standard metagame ubuntu-latest-8 32 | |
| add game_components_embeddable_game_standard registry ubuntu-latest-8 32 | |
| fi | |
| if [ "$NEED_METAGAME" = "true" ]; then | |
| add game_components_metagame leaderboard ubuntu-latest-4 256 | |
| add game_components_metagame registration ubuntu-latest-4 256 | |
| add game_components_metagame entry_requirement ubuntu-latest-4 256 | |
| add game_components_metagame entry_fee ubuntu-latest-4 256 | |
| add game_components_metagame prize ubuntu-latest-4 256 | |
| add game_components_metagame ticket_booth ubuntu-latest-4 256 | |
| add game_components_metagame merkledrop ubuntu-latest-4 256 | |
| fi | |
| if [ "$NEED_ECONOMY" = "true" ]; then | |
| add game_components_economy tokenomics ubuntu-latest-4 256 | |
| fi | |
| if [ "$NEED_UTILITIES" = "true" ]; then | |
| add game_components_utilities math ubuntu-latest-4 256 | |
| add game_components_utilities distribution ubuntu-latest-4 256 | |
| add game_components_utilities utils ubuntu-latest-4 256 | |
| add game_components_utilities renderer ubuntu-latest-4 256 | |
| fi | |
| if [ "$NEED_PRESETS" = "true" ]; then | |
| add game_components_presets presets ubuntu-latest-4 256 | |
| fi | |
| MATRIX=$(echo "$INCLUDES" | jq -c '{include:.}') | |
| echo "test_matrix=$MATRIX" >> "$GITHUB_OUTPUT" | |
| echo "Matrix: $(echo "$INCLUDES" | jq 'length') modules" | |
| echo "$MATRIX" | jq . | |
| - name: Extract tool versions | |
| run: | | |
| echo "SCARB_VERSION=$(grep '^scarb ' .tool-versions | awk '{print $2}')" >> "$GITHUB_ENV" | |
| echo "FOUNDRY_VERSION=$(grep '^starknet-foundry ' .tool-versions | awk '{print $2}')" >> "$GITHUB_ENV" | |
| - uses: software-mansion/setup-scarb@v1 | |
| with: | |
| scarb-version: ${{ env.SCARB_VERSION }} | |
| cache: false | |
| - uses: foundry-rs/setup-snfoundry@v4 | |
| with: | |
| starknet-foundry-version: ${{ env.FOUNDRY_VERSION }} | |
| - name: Cache Scarb dependencies | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.cache/scarb | |
| key: ${{ runner.os }}-scarb-deps-${{ hashFiles('**/Scarb.toml', '**/Scarb.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-scarb-deps- | |
| - name: Cache Cargo (snforge plugin + Scarb Rust deps) | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.cargo | |
| key: ${{ runner.os }}-cargo-snforge-${{ env.FOUNDRY_VERSION }} | |
| restore-keys: | | |
| ${{ runner.os }}-cargo-snforge- | |
| - name: Fetch dependencies | |
| run: scarb fetch | |
| - name: Warm snforge plugin cache | |
| run: snforge --version | |
| - name: Verify workspace compiles (no warnings) | |
| run: | | |
| scarb build --workspace 2>&1 | tee /tmp/build.log | |
| if grep -qE '^ --> .*\.cairo:[0-9]+:[0-9]+' /tmp/build.log; then | |
| echo "::error::Build produced compiler warnings. Fix all warnings before merging." | |
| grep -B1 -E '^ --> .*\.cairo:[0-9]+:[0-9]+' /tmp/build.log | |
| exit 1 | |
| fi | |
| test: | |
| name: test (${{ matrix.module }}) | |
| needs: [changes, setup] | |
| if: needs.changes.outputs.has_tests == 'true' | |
| runs-on: ${{ matrix.runner }} | |
| timeout-minutes: 10 | |
| strategy: | |
| fail-fast: true | |
| matrix: ${{ fromJson(needs.setup.outputs.test_matrix) }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Extract tool versions | |
| run: | | |
| echo "SCARB_VERSION=$(grep '^scarb ' .tool-versions | awk '{print $2}')" >> "$GITHUB_ENV" | |
| echo "FOUNDRY_VERSION=$(grep '^starknet-foundry ' .tool-versions | awk '{print $2}')" >> "$GITHUB_ENV" | |
| - uses: software-mansion/setup-scarb@v1 | |
| with: | |
| scarb-version: ${{ env.SCARB_VERSION }} | |
| cache: false | |
| - uses: foundry-rs/setup-snfoundry@v4 | |
| with: | |
| starknet-foundry-version: ${{ env.FOUNDRY_VERSION }} | |
| - name: Cache Scarb dependencies | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.cache/scarb | |
| key: ${{ runner.os }}-scarb-deps-${{ hashFiles('**/Scarb.toml', '**/Scarb.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-scarb-deps- | |
| - name: Cache Cargo (snforge plugin) | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.cargo | |
| key: ${{ runner.os }}-cargo-snforge-${{ env.FOUNDRY_VERSION }} | |
| restore-keys: | | |
| ${{ runner.os }}-cargo-snforge- | |
| - name: Cache tools | |
| uses: actions/cache@v4 | |
| id: cache-tools | |
| with: | |
| path: | | |
| ~/.local/bin/cairo-coverage | |
| ~/.local/bin/universal-sierra-compiler | |
| key: ${{ runner.os }}-tools-cairo-coverage-universal-sierra-compiler-v1 | |
| - name: Install tools | |
| if: steps.cache-tools.outputs.cache-hit != 'true' | |
| run: | | |
| (curl -L https://raw.githubusercontent.com/software-mansion/cairo-coverage/main/scripts/install.sh | sh) & | |
| (curl -L https://raw.githubusercontent.com/software-mansion/universal-sierra-compiler/master/scripts/install.sh | sh) & | |
| wait | |
| - name: Cache compiled packages | |
| uses: actions/cache@v4 | |
| with: | |
| path: target/ | |
| key: ${{ runner.os }}-target-${{ matrix.package }}-${{ matrix.module }}-${{ env.SCARB_VERSION }}-${{ hashFiles('packages/**/*.cairo', '**/Scarb.toml', '**/Scarb.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-target-${{ matrix.package }}-${{ matrix.module }}-${{ env.SCARB_VERSION }}- | |
| - name: Run tests with coverage | |
| run: | | |
| if [ "${{ matrix.package }}" = "game_components_${{ matrix.module }}" ]; then | |
| snforge test -p ${{ matrix.package }} --fuzzer-runs ${{ matrix.fuzzer_runs }} --coverage 2>&1 | tee /tmp/test.log | |
| else | |
| snforge test -p ${{ matrix.package }} "::${{ matrix.module }}::" --fuzzer-runs ${{ matrix.fuzzer_runs }} --coverage 2>&1 | tee /tmp/test.log | |
| fi | |
| - name: Check for compiler warnings | |
| if: always() && steps.cache-tools.outcome != 'failure' | |
| run: | | |
| if [ -f /tmp/test.log ] && grep -qE '^ --> .*\.cairo:[0-9]+:[0-9]+' /tmp/test.log; then | |
| echo "::error::Test compilation produced warnings. Fix all warnings before merging." | |
| grep -B1 -E '^ --> .*\.cairo:[0-9]+:[0-9]+' /tmp/test.log | |
| exit 1 | |
| fi | |
| - name: Upload coverage to Codecov | |
| uses: codecov/codecov-action@v5 | |
| with: | |
| files: ./coverage.lcov | |
| flags: ${{ matrix.module }} | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| # AI code review (Claude + Codex) is delegated to the org-shared reusable | |
| # workflow so the review logic and model config live in one place. This repo | |
| # supplies only .github/review-agents.json + .github/prompts/*.md. | |
| # | |
| # Deliberately has no `needs:` — a skipped dependency skips its dependents, | |
| # and `lint` is conditional on packages/ changing, so gating on it would | |
| # silently drop reviews on docs-only PRs. | |
| review: | |
| uses: Provable-Games/.github/.github/workflows/code-review.yml@v1 | |
| with: | |
| # Preserves the merge gate the inline review jobs had: a [CRITICAL] or | |
| # [HIGH] finding fails the job rather than only posting a comment. | |
| block_on_severity: true | |
| # Repo default workflow token is read-only; a reusable workflow's job | |
| # permissions can't exceed the caller's, so grant the write scopes the | |
| # review jobs need (PR comments + Claude OIDC) here. | |
| # | |
| # @v1 is a mutable tag on purpose — it is the org convention every caller | |
| # uses, and it is what lets a model deprecation be fixed once upstream | |
| # rather than in seven repos. Pinning a SHA here would defeat the reason | |
| # this workflow is shared. `secrets: inherit` likewise: the callee is | |
| # first-party and declares exactly CLAUDE_CODE_OAUTH_TOKEN and | |
| # CODEX_AUTH_DOT_JSON, and inheriting means a new upstream secret does not | |
| # need a PR in every caller. | |
| permissions: | |
| contents: read | |
| issues: write | |
| pull-requests: write | |
| id-token: write | |
| secrets: inherit | |
| pr-ci: | |
| needs: | |
| - changes | |
| - infra-validate | |
| - lint | |
| - setup | |
| - test | |
| - review | |
| if: always() | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Evaluate results | |
| env: | |
| NEEDS_JSON: ${{ toJson(needs) }} | |
| run: | | |
| FAILED=$(echo "$NEEDS_JSON" | jq -r ' | |
| to_entries[] | |
| | select(.value.result == "failure" or .value.result == "cancelled") | |
| | .key | |
| ') | |
| if [ -n "$FAILED" ]; then | |
| echo "::error::Failing jobs:" | |
| echo "$FAILED" | |
| exit 1 | |
| fi | |
| echo "All required jobs passed or were skipped" |