├── .github ├── .gitkeep └── workflows │ ├── deployment.yml │ ├── formatting.yml │ ├── linting.yml │ └── test.yml ├── .gitignore ├── .patches ├── builtins │ └── bitwise.cairo.patch ├── hints │ ├── hints00.cairo.patch │ └── hints01.cairo.patch ├── implicit_arguments │ ├── implicit_arguments01.cairo.patch │ ├── implicit_arguments02.cairo.patch │ └── implicit_arguments03.cairo.patch ├── operations │ ├── operations00.cairo.patch │ ├── operations01.cairo.patch │ ├── operations02.cairo.patch │ └── operations03.cairo.patch ├── recursions │ ├── array01.cairo.patch │ ├── array02.cairo.patch │ ├── array03.cairo.patch │ ├── array04.cairo.patch │ ├── collatz_sequence.cairo.patch │ ├── recursion01.cairo.patch │ └── struct01.cairo.patch ├── registers │ ├── registers00.cairo.patch │ ├── registers01.cairo.patch │ ├── registers02.cairo.patch │ ├── registers03.cairo.patch │ └── registers04.cairo.patch ├── revoked_references │ └── revoked_references01.cairo.patch ├── storage │ ├── storage01.cairo.patch │ ├── storage02.cairo.patch │ └── storage03.cairo.patch ├── strings │ ├── strings00.cairo.patch │ └── strings01.cairo.patch ├── syntax │ ├── syntax01.cairo.patch │ ├── syntax02.cairo.patch │ ├── syntax03.cairo.patch │ ├── syntax04.cairo.patch │ └── syntax05.cairo.patch └── tricks │ ├── assert_bool.cairo.patch │ ├── inline_if.cairo.patch │ └── no_conditionals.cairo.patch ├── .pylintrc ├── CONTRIBUTING.md ├── README.md ├── deploy.py ├── exercises ├── builtins │ ├── README.md │ └── bitwise.cairo ├── hints │ ├── README.md │ ├── hints00.cairo │ └── hints01.cairo ├── implicit_arguments │ ├── README.md │ ├── implicit_arguments01.cairo │ ├── implicit_arguments02.cairo │ └── implicit_arguments03.cairo ├── operations │ ├── README.md │ ├── operations00.cairo │ ├── operations01.cairo │ ├── operations02.cairo │ └── operations03.cairo ├── recursions │ ├── README.md │ ├── array01.cairo │ ├── array02.cairo │ ├── array03.cairo │ ├── array04.cairo │ ├── collatz_sequence.cairo │ ├── recursion01.cairo │ └── struct01.cairo ├── registers │ ├── registers00.cairo │ ├── registers01.cairo │ ├── registers02.cairo │ ├── registers03.cairo │ └── registers04.cairo ├── revoked_references │ └── revoked_references01.cairo ├── storage │ ├── storage01.cairo │ ├── storage02.cairo │ └── storage03.cairo ├── strings │ ├── README.md │ ├── strings00.cairo │ └── strings01.cairo ├── syntax │ ├── README.md │ ├── syntax01.cairo │ ├── syntax02.cairo │ ├── syntax03.cairo │ ├── syntax04.cairo │ └── syntax05.cairo └── tricks │ ├── assert_bool.cairo │ ├── inline_if.cairo │ └── no_conditionals.cairo ├── install.sh ├── lib └── .gitkeep ├── poetry.lock ├── protostar.toml ├── pyproject.toml ├── pytest.ini ├── resources └── assets │ └── warning.png ├── scripts ├── add_new_exercise.sh └── patch_and_test_exercises.sh ├── src ├── __init__.py ├── cli.py ├── config.py ├── console.py ├── database.py ├── exercises │ ├── __init__.py │ ├── checker.py │ ├── checker_test.py │ ├── list_test.py │ ├── model.py │ ├── model_test.py │ ├── seeker.py │ └── seeker_test.py ├── file_watcher │ ├── dummy_file.cairo │ ├── watcher.py │ └── watcher_test.py ├── prompt.py ├── repository │ ├── __init__.py │ ├── state_checker.py │ └── state_checker_test.py ├── runner.py ├── runner_test.py ├── solutions │ ├── __init__.py │ ├── factory.py │ ├── factory_test.py │ └── repository.py ├── user │ ├── __init__.py │ ├── access_token.py │ ├── login.py │ └── login_test.py └── utils │ ├── __init__.py │ ├── debounce.py │ ├── debounce_test.py │ ├── version_manager.py │ └── version_manager_test.py ├── starklings-backend ├── .env.DEFAULT ├── README.md ├── app.py ├── config.py ├── db.py ├── requirements.txt ├── starklings_backend │ ├── __init__.py │ ├── exercise.py │ ├── models │ │ ├── shared.py │ │ └── user.py │ ├── routes.py │ └── utils.py └── tests │ └── test_app.py ├── starklings.py ├── starklings.spec └── tests ├── exercises └── test_end_of_exercise_messages │ ├── with_next_exercises │ ├── syntax01.cairo │ └── syntax02.cairo │ └── without_next_exercises │ ├── syntax01.cairo │ └── syntax02.cairo ├── test.cairo ├── test_failure.cairo ├── test_invalid.cairo └── test_missing.cairo /.github/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onlydustxyz/starklings/ed480cce4cd74c518ffeaa40f63b71892c6e6a0a/.github/.gitkeep -------------------------------------------------------------------------------- /.github/workflows/deployment.yml: -------------------------------------------------------------------------------- 1 | name: Deployment 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | build: 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | matrix: 13 | os: [ubuntu-18.04, macOS-11] # TODO: Fix windows gmp.h not found issue, and add a windows binary 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Set up Python 3.8.12 17 | uses: actions/setup-python@v2 18 | with: 19 | python-version: 3.8.12 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install poetry 24 | poetry install 25 | - name: Build binary 26 | run: | 27 | poetry run poe build 28 | - name: Run a smoke test with --help 29 | run: | 30 | ./dist/starklings/starklings --help 31 | - name: Pack binary into a tarball 32 | run: tar -czvf starklings.tar.gz ./dist/starklings 33 | - name: Upload the tarball 34 | uses: actions/upload-artifact@v2 35 | with: 36 | name: starklings-${{ runner.os }} 37 | path: starklings.tar.gz 38 | publish: 39 | runs-on: ubuntu-latest 40 | needs: [build] 41 | steps: 42 | - uses: actions/checkout@v2 43 | - uses: actions/download-artifact@v2 44 | with: 45 | path: dist 46 | 47 | - name: Upload macOS binary to release 48 | uses: svenstaro/upload-release-action@v2 49 | with: 50 | repo_token: ${{ secrets.GITHUB_TOKEN }} 51 | file: dist/starklings-macOS/starklings.tar.gz 52 | asset_name: starklings-macOS.tar.gz 53 | tag: ${{ github.ref }} 54 | overwrite: true 55 | 56 | - name: Upload Linux binary to release 57 | uses: svenstaro/upload-release-action@v2 58 | with: 59 | repo_token: ${{ secrets.GITHUB_TOKEN }} 60 | file: dist/starklings-Linux/starklings.tar.gz 61 | asset_name: starklings-Linux.tar.gz 62 | tag: ${{ github.ref }} 63 | overwrite: true 64 | release: 65 | runs-on: ubuntu-latest 66 | needs: [build] 67 | steps: 68 | - uses: actions/checkout@v2 69 | - name: Initialize mandatory git config 70 | run: | 71 | git config user.name "GitHub Actions" 72 | git config user.email noreply@github.com 73 | - name: Checkout stable branch 74 | run: | 75 | git fetch origin 76 | git checkout stable 77 | - name: Reset on main 78 | run: | 79 | git reset --hard origin/main 80 | git push --force origin stable 81 | -------------------------------------------------------------------------------- /.github/workflows/formatting.yml: -------------------------------------------------------------------------------- 1 | name: Formatting 2 | 3 | on: 4 | pull_request: ~ 5 | 6 | jobs: 7 | cairo: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: actions/setup-python@v3 12 | with: 13 | python-version: "3.8" 14 | - name: Install dependencies 15 | run: 16 | | # Install latest version from https://pypi.org/simple/cairo-lang/ (as done in nile) 17 | pip install https://files.pythonhosted.org/packages/c0/f6/c850604895a2ce5ff3ef77cdb470b6b0ef50889645748a748e18a1c2269e/cairo-lang-0.8.1.post1.zip#sha256=b3c1a23078ba4e0c8ec45d2cd2ba4873ad70b6723dfba70f70c978c9723ff6eb 18 | - name: Check files formatting 19 | run: cairo-format -c exercises/**/*.cairo 20 | python: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v3 24 | - uses: actions/setup-python@v3 25 | with: 26 | python-version: "3.8" 27 | - name: Install dependencies 28 | run: | 29 | pip install black 30 | - name: Check files formatting 31 | run: black --check src tests 32 | -------------------------------------------------------------------------------- /.github/workflows/linting.yml: -------------------------------------------------------------------------------- 1 | name: Linting 2 | 3 | on: 4 | pull_request: ~ 5 | 6 | jobs: 7 | python: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: actions/setup-python@v3 12 | with: 13 | python-version: "3.8" 14 | - name: Install dependencies 15 | run: | 16 | python -m pip install --upgrade pip 17 | pip install poetry 18 | poetry install 19 | - name: Check files linting 20 | run: poetry run pylint src 21 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: ~ 5 | 6 | jobs: 7 | unit-test: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Set up Python 3.8.12 12 | uses: actions/setup-python@v2 13 | with: 14 | python-version: 3.8.12 15 | - name: Install dependencies 16 | run: | 17 | python -m pip install --upgrade pip 18 | pip install poetry 19 | poetry install 20 | - name: Build binary 21 | run: | 22 | poetry run poe build 23 | - name: Run tests 24 | run: | 25 | poetry run pytest src 26 | exercises-test: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v2 30 | - name: Set up Python 3.8.12 31 | uses: actions/setup-python@v2 32 | with: 33 | python-version: 3.8.12 34 | - name: Install protostar 35 | run: | 36 | curl -L https://raw.githubusercontent.com/software-mansion/protostar/master/install.sh | bash 37 | - name: Run exercise tests 38 | run: | 39 | chmod +x ./scripts/patch_and_test_exercises.sh 40 | ./scripts/patch_and_test_exercises.sh 41 | shell: bash 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | .vscode 3 | 4 | # Python 5 | __pycache__ 6 | env 7 | *.py[cod] 8 | 9 | # Protostar 10 | build 11 | 12 | # Pyinstaller 13 | dist 14 | 15 | .solutions 16 | starklings.db 17 | starklings-backend/.env -------------------------------------------------------------------------------- /.patches/builtins/bitwise.cairo.patch: -------------------------------------------------------------------------------- 1 | --- a/exercises/builtins/bitwise.cairo 2022-06-15 15:00:45.028637685 +0200 2 | +++ b/exercises/builtins/bitwise.cairo 2022-06-15 15:00:24.931890268 +0200 3 | @@ -22 +22,3 @@ 4 | - # FILL ME 5 | + let (pow2n) = pow(2, n) 6 | + let (and_val) = bitwise_and(value, pow2n) 7 | + let (res) = is_not_zero(and_val) 8 | @@ -29 +31,2 @@ 9 | - # FILL ME 10 | + let (pow2n) = pow(2, n) 11 | + let (res) = bitwise_or(value, pow2n) 12 | @@ -36 +39,2 @@ 13 | - # FILL ME 14 | + let (pow2n) = pow(2, n) 15 | + let (res) = bitwise_xor(value, pow2n) 16 | @@ -55 +59,2 @@ 17 | - # Assert op is correct 18 | + # Product can only be zero if op in ['get', 'set', 'toggle'] 19 | + assert 0 = (op - 'get') * (op - 'set') * (op - 'toggle') 20 | @@ -58 +64,2 @@ 21 | - # Assert n is within bounds 22 | + assert_nn(n) 23 | + assert_le(n, 250) 24 | @@ -61,2 +68,18 @@ 25 | - # Compute the operation 26 | - # Don't forget to advance bitwise_ptr 27 | + let (pow2n) = pow(2, n) 28 | + assert bitwise_ptr.x = value 29 | + assert bitwise_ptr.y = pow2n 30 | + 31 | + local res 32 | + if op == 'get': 33 | + let (tmp) = is_not_zero(bitwise_ptr.x_and_y) 34 | + assert res = tmp 35 | + else: 36 | + if op == 'set': 37 | + assert res = bitwise_ptr.x_or_y 38 | + else: 39 | + # op == 'toggle' 40 | + assert res = bitwise_ptr.x_xor_y 41 | + end 42 | + end 43 | + 44 | + let bitwise_ptr = bitwise_ptr + BitwiseBuiltin.SIZE 45 | -------------------------------------------------------------------------------- /.patches/hints/hints00.cairo.patch: -------------------------------------------------------------------------------- 1 | diff --git a/exercises/hints/hints00.cairo b/exercises/hints/hints00.cairo 2 | index 24b17e1..554e059 100644 3 | --- a/exercises/hints/hints00.cairo 4 | +++ b/exercises/hints/hints00.cairo 5 | @@ -16,0 +17 @@ func basic_hint() -> (value : felt): 6 | + %{ ids.res = 42 %} 7 | -------------------------------------------------------------------------------- /.patches/hints/hints01.cairo.patch: -------------------------------------------------------------------------------- 1 | --- a/exercises/hints/hints01.cairo 2022-06-01 10:52:51.816970359 +0200 2 | +++ b/exercises/hints/hints01.cairo 2022-06-01 11:10:07.339328562 +0200 3 | @@ -18,3 +18,3 @@ 4 | - # TODO: Compute the quotient and remainder inside the hint 5 | - print(ids.quotient) 6 | - print(ids.remainder) 7 | + q, r = divmod(ids.x, ids.n) 8 | + ids.quotient = q 9 | + ids.remainder = r 10 | @@ -22,3 +22,2 @@ 11 | - # TODO: verify the result is correct 12 | - 13 | - return (0) 14 | + assert x = quotient * n + remainder 15 | + return (remainder) 16 | -------------------------------------------------------------------------------- /.patches/implicit_arguments/implicit_arguments01.cairo.patch: -------------------------------------------------------------------------------- 1 | diff --git a/exercises/implicit_arguments/implicit_arguments01.cairo b/exercises/implicit_arguments/implicit_arguments01.cairo 2 | index ca97179..bb45805 100644 3 | --- a/exercises/implicit_arguments/implicit_arguments01.cairo 4 | +++ b/exercises/implicit_arguments/implicit_arguments01.cairo 5 | @@ -10 +10 @@ 6 | -func implicit_sum() -> (result : felt): 7 | +func implicit_sum{a, b}() -> (result : felt): 8 | -------------------------------------------------------------------------------- /.patches/implicit_arguments/implicit_arguments02.cairo.patch: -------------------------------------------------------------------------------- 1 | diff --git a/exercises/implicit_arguments/implicit_arguments02.cairo b/exercises/implicit_arguments/implicit_arguments02.cairo 2 | index c2390e7..5f64a2f 100644 3 | --- a/exercises/implicit_arguments/implicit_arguments02.cairo 4 | +++ b/exercises/implicit_arguments/implicit_arguments02.cairo 5 | @@ -20 +20 @@ end 6 | -func child_function_1() -> (result : felt): 7 | +func child_function_1{a}() -> (result : felt): 8 | @@ -25 +25 @@ end 9 | -func child_function_2() -> (result : felt): 10 | +func child_function_2{b}() -> (result : felt): 11 | -------------------------------------------------------------------------------- /.patches/implicit_arguments/implicit_arguments03.cairo.patch: -------------------------------------------------------------------------------- 1 | diff --git a/exercises/implicit_arguments/implicit_arguments03.cairo b/exercises/implicit_arguments/implicit_arguments03.cairo 2 | index 93629ad..173a4f3 100644 3 | --- a/exercises/implicit_arguments/implicit_arguments03.cairo 4 | +++ b/exercises/implicit_arguments/implicit_arguments03.cairo 5 | @@ -13,0 +14 @@ func black_box{secret : felt}() -> (): 6 | + let secret = 'very secret!' 7 | -------------------------------------------------------------------------------- /.patches/operations/operations00.cairo.patch: -------------------------------------------------------------------------------- 1 | --- exercises/operations/operations00.cairo 2022-08-15 14:01:18.147439197 +0200 2 | +++ ./.solutions/operations00.cairo 2022-08-15 14:02:33.348133149 +0200 3 | @@ -11 +11 @@ 4 | - # FILL ME 5 | + let res = (x - 1) * (x + 2) / (x - 2) 6 | -------------------------------------------------------------------------------- /.patches/operations/operations01.cairo.patch: -------------------------------------------------------------------------------- 1 | --- exercises/operations/operations01.cairo 2022-08-15 14:19:41.493661039 +0200 2 | +++ ./.solutions/operations01.cairo 2022-08-15 13:31:21.670475341 +0200 3 | @@ -13 +13 @@ 4 | - # FILL ME 5 | + let x = 2 ** 251 + 17 * 2 ** 192 6 | @@ -23 +23 @@ 7 | - # FILL ME 8 | + const y = 2 ** 250 + 17 * 2 ** 191 9 | -------------------------------------------------------------------------------- /.patches/operations/operations02.cairo.patch: -------------------------------------------------------------------------------- 1 | --- exercises/operations/operations02.cairo 2022-08-15 14:09:44.344120823 +0200 2 | +++ ./.solutions/operations02.cairo 2022-08-15 14:09:18.319879674 +0200 3 | @@ -14 +14 @@ 4 | - # TO FILL 5 | + let x = 89 # works for all prime numbers in range [50 ; 100] 6 | -------------------------------------------------------------------------------- /.patches/operations/operations03.cairo.patch: -------------------------------------------------------------------------------- 1 | --- exercises/operations/operations03.cairo 2022-08-15 14:02:02.971852750 +0200 2 | +++ ./.solutions/operations03.cairo 2022-08-15 14:09:15.559854101 +0200 3 | @@ -15 +15 @@ 4 | - %{ ids.x = -1 %} # Change only this line to make the test pass 5 | + %{ ids.x = 2 ** 251 + 17 * 2 ** 192 + 1 - 17 %} 6 | -------------------------------------------------------------------------------- /.patches/recursions/array01.cairo.patch: -------------------------------------------------------------------------------- 1 | diff --git a/exercises/recursions/array01.cairo b/exercises/recursions/array01.cairo 2 | index 44cb04f..9f0d822 100644 3 | --- a/exercises/recursions/array01.cairo 4 | +++ b/exercises/recursions/array01.cairo 5 | @@ -12 +12,8 @@ func contains(needle : felt, haystack : felt*, haystack_len : felt) -> (result : 6 | - return (0) 7 | + if haystack_len == 0: 8 | + return (0) 9 | + end 10 | + if needle == haystack[0]: 11 | + return (1) 12 | + end 13 | + let (next) = contains(needle, haystack + 1, haystack_len - 1) 14 | + return (next) 15 | -------------------------------------------------------------------------------- /.patches/recursions/array02.cairo.patch: -------------------------------------------------------------------------------- 1 | diff --git a/exercises/recursions/array02.cairo b/exercises/recursions/array02.cairo 2 | index de33f4a..7ff6098 100644 3 | --- a/exercises/recursions/array02.cairo 4 | +++ b/exercises/recursions/array02.cairo 5 | @@ -14 +14 @@ from starkware.cairo.common.alloc import alloc 6 | -func square(array : felt*, array_len : felt): 7 | +func square(array : felt*, array_len : felt, squared_array : felt*): 8 | @@ -20 +20 @@ func square(array : felt*, array_len : felt): 9 | - assert [array] = squared_item 10 | + assert [squared_array] = squared_item 11 | @@ -22 +22 @@ func square(array : felt*, array_len : felt): 12 | - return square(array + 1, array_len - 1) 13 | + return square(array + 1, array_len - 1, squared_array + 1) 14 | @@ -36 +36,2 @@ func test_square{syscall_ptr : felt*}(): 15 | - square(array, 4) 16 | + let (squared_array : felt*) = alloc() 17 | + square(array, 4, squared_array) 18 | @@ -38,4 +39,4 @@ func test_square{syscall_ptr : felt*}(): 19 | - assert [array] = 1 20 | - assert [array + 1] = 4 21 | - assert [array + 2] = 9 22 | - assert [array + 3] = 16 23 | + assert [squared_array] = 1 24 | + assert [squared_array + 1] = 4 25 | + assert [squared_array + 2] = 9 26 | + assert [squared_array + 3] = 16 27 | -------------------------------------------------------------------------------- /.patches/recursions/array03.cairo.patch: -------------------------------------------------------------------------------- 1 | --- exercises/recursions/array03.cairo 2022-05-25 16:16:23.785739305 +0200 2 | +++ array03.cairo 2022-05-25 16:16:25.372412454 +0200 3 | @@ -25,2 +25,2 @@ func is_increasing{range_check_ptr : fel 4 | - let curr_value = 0 5 | - let next_value = 0 6 | + let curr_value = [array] 7 | + let next_value = [array + 1] 8 | @@ -43,0 +44,10 @@ func is_decreasing{range_check_ptr : fel 9 | + if array_len == 0: 10 | + return (1) 11 | + end 12 | + 13 | + if array_len == 1: 14 | + return (1) 15 | + end 16 | + 17 | + let curr_value = array[array_len - 1] 18 | + let next_value = array[array_len - 2] 19 | @@ -49 +59 @@ func is_decreasing{range_check_ptr : fel 20 | - return is_decreasing(array, array_len) 21 | + return is_decreasing(array, array_len - 1) 22 | @@ -60,0 +71,6 @@ func reverse(array : felt*, rev_array : 23 | + if array_len == 0: 24 | + return () 25 | + end 26 | + 27 | + assert [rev_array] = array[array_len - 1] 28 | + reverse(array, rev_array + 1, array_len - 1) 29 | -------------------------------------------------------------------------------- /.patches/recursions/array04.cairo.patch: -------------------------------------------------------------------------------- 1 | --- exercises/recursions/array04.cairo 2022-05-30 19:30:16.000000000 -0400 2 | +++ /tmp/array04.cairo 2022-05-30 19:29:01.000000000 -0400 3 | @@ -14 +14,14 @@ 4 | - # FILL ME 5 | + if len_points == 0: 6 | + return (0) 7 | + end 8 | + if points[0].x != 0: 9 | + return contains_origin(len_points-1, points + Point.SIZE) 10 | + end 11 | + if points[0].y != 0: 12 | + return contains_origin(len_points-1, points + Point.SIZE) 13 | + end 14 | + if points[0].z != 0: 15 | + return contains_origin(len_points-1, points + Point.SIZE) 16 | + end 17 | + 18 | + return (1) 19 | -------------------------------------------------------------------------------- /.patches/recursions/collatz_sequence.cairo.patch: -------------------------------------------------------------------------------- 1 | --- collatz_sequence.cairo 2022-05-31 11:36:29.555657000 +0400 2 | +++ collatz_sequence_solution.cairo 2022-05-31 11:29:43.390728000 +0400 3 | @@ -18,0 +19,15 @@ 4 | + if step == 0: 5 | + return (result=seed) 6 | + end 7 | + 8 | + if seed == 1: 9 | + return (result=1) 10 | + end 11 | + 12 | + let (even_or_odd) = bitwise_and(seed, 1) 13 | + 14 | + if even_or_odd == 0: 15 | + let number : felt = collatz(seed=seed / 2, step=step - 1) 16 | + else: 17 | + let number : felt = collatz(seed=seed * 3 + 1, step=step - 1) 18 | + end 19 | -------------------------------------------------------------------------------- /.patches/recursions/recursion01.cairo.patch: -------------------------------------------------------------------------------- 1 | diff --git a/exercises/recursions/recursion01.cairo b/exercises/recursions/recursion01.cairo 2 | index 811b7a9..4ef0c2f 100644 3 | --- a/exercises/recursions/recursion01.cairo 4 | +++ b/exercises/recursions/recursion01.cairo 5 | @@ -13 +13,10 @@ func fibonacci(n : felt) -> (result : felt): 6 | - return (0) 7 | + alloc_locals 8 | + if n == 0: 9 | + return (0) 10 | + end 11 | + if n == 1: 12 | + return (1) 13 | + end 14 | + let (local n_1) = fibonacci(n - 1) 15 | + let (local n_2) = fibonacci(n - 2) 16 | + return (n_1 + n_2) 17 | -------------------------------------------------------------------------------- /.patches/recursions/struct01.cairo.patch: -------------------------------------------------------------------------------- 1 | --- exercises/recursions/struct01.cairo 2022-05-31 13:55:44.000000000 +0200 2 | +++ exercises/recursions/struct01.sol.cairo 2022-05-31 13:53:49.000000000 +0200 3 | @@ -30 +30,7 @@ 4 | - # FILL ME 5 | + if struct_size == 0: 6 | + return (res=0) 7 | + end 8 | + 9 | + let square = [struct_value] * [struct_value] 10 | + let (rest) = squared_magnitude(struct_value + 1, struct_size - 1) 11 | + return (res=square + rest) 12 | -------------------------------------------------------------------------------- /.patches/registers/registers00.cairo.patch: -------------------------------------------------------------------------------- 1 | --- exercises/registers/registers00.cairo 2022-05-18 12:53:46.000000000 -0400 2 | +++ .tmp_solution/registers00.cairo 2022-05-18 11:38:32.000000000 -0400 3 | @@ -12,3 +12,2 @@ 4 | -func ret_42() -> (r : felt): 5 | - # [ap] = 42; ap++ 6 | - # ret 7 | +func ret_42() -> (r: felt): 8 | + return (42) 9 | @@ -20,2 +19,4 @@ 10 | -func ret_0_and_1() -> (zero : felt, one : felt): 11 | - # return (0, 1) 12 | +func ret_0_and_1() -> (zero: felt, one: felt): 13 | + [ap] = 0; ap++ 14 | + [ap] = 1; ap++ 15 | + ret 16 | @@ -43 +44 @@ 17 | -end 18 | +end 19 | \ No newline at end of file 20 | -------------------------------------------------------------------------------- /.patches/registers/registers01.cairo.patch: -------------------------------------------------------------------------------- 1 | --- exercises/registers/registers01.cairo 2022-05-18 12:53:45.000000000 -0400 2 | +++ .tmp_solution/registers01.cairo 2022-05-18 11:39:34.000000000 -0400 3 | @@ -11,3 +11,3 @@ 4 | -func assert_is_42(n : felt): 5 | - # [ap - 3] = 42 6 | - # ret 7 | +func assert_is_42(n: felt): 8 | + assert n = 42 9 | + ret 10 | @@ -19,2 +19,3 @@ 11 | -func sum(a : felt, b : felt) -> (s : felt): 12 | - # return (a + b) 13 | +func sum(a: felt, b: felt) -> (s: felt): 14 | + [ap] = [ap -3] + [ap -4]; ap++ 15 | + ret 16 | @@ -45 +46 @@ 17 | -end 18 | +end 19 | \ No newline at end of file 20 | -------------------------------------------------------------------------------- /.patches/registers/registers02.cairo.patch: -------------------------------------------------------------------------------- 1 | --- exercises/registers/registers02.cairo 2022-06-11 22:20:43.000000000 -0400 2 | +++ ./registers02.cairo 2022-06-11 22:18:21.000000000 -0400 3 | @@ -17,3 +17,3 @@ 4 | - # [ap] = 42; ap++ 5 | - # [ap - 1] = 42 6 | - # [ap - 1] = 21 7 | + tempvar x = 42 8 | + assert x = 42 9 | + assert x = 21 10 | @@ -27 +27 @@ 11 | - # assert number = 42 12 | + [fp -3] = 42 13 | @@ -37,0 +38 @@ 14 | + assert [p_number] = 42 15 | @@ -45,0 +47 @@ 16 | + assert 42 = [p_number] 17 | -------------------------------------------------------------------------------- /.patches/registers/registers03.cairo.patch: -------------------------------------------------------------------------------- 1 | --- exercises/registers/registers03.cairo 2022-05-18 12:53:44.000000000 -0400 2 | +++ .tmp_solution/registers03.cairo 2022-05-18 11:40:22.000000000 -0400 3 | @@ -3,0 +4 @@ 4 | + 5 | @@ -9,6 +10,2 @@ 6 | -func sum_array(array_len : felt, array : felt*) -> (sum : felt): 7 | - # [ap] = [fp - 4]; ap++ 8 | - # [ap] = [fp - 3]; ap++ 9 | - # [ap] = 0; ap++ 10 | - # call rec_sum_array 11 | - # ret 12 | +func sum_array(array_len: felt, array: felt*) -> (sum: felt): 13 | + return rec_sum_array(array_len, array, 0) 14 | @@ -17,16 +14,6 @@ 15 | -func rec_sum_array(array_len : felt, array : felt*, sum : felt) -> (sum : felt): 16 | - # jmp continue if [fp - 5] != 0 17 | - 18 | - # stop: 19 | - # [ap] = [fp - 3]; ap++ 20 | - # jmp done 21 | - 22 | - # continue: 23 | - # [ap] = [[fp - 4]]; ap++ 24 | - # [ap] = [fp - 5] - 1; ap++ 25 | - # [ap] = [fp - 4] + 1; ap++ 26 | - # [ap] = [ap - 3] + [fp - 3]; ap++ 27 | - # call rec_sum_array 28 | - 29 | - # done: 30 | - # ret 31 | +func rec_sum_array(array_len: felt, array: felt*, sum: felt) -> (sum: felt): 32 | + if array_len == 0: 33 | + return (sum) 34 | + end 35 | + 36 | + return rec_sum_array(array_len - 1, array + 1, sum + array[0]) 37 | @@ -39,7 +26,31 @@ 38 | -func max{range_check_ptr}(a : felt, b : felt) -> (max : felt): 39 | - # let (res) = is_le(a, b) 40 | - # if res == 1: 41 | - # return (b) 42 | - # else: 43 | - # return (a) 44 | - # end 45 | +func max{range_check_ptr}(a: felt, b: felt) -> (max: felt): 46 | + # Push arguments to the stack 47 | + [ap] = [fp - 5]; ap++ # range_check_ptr 48 | + [ap] = [fp - 4]; ap++ # a 49 | + [ap] = [fp - 3]; ap++ # b 50 | + 51 | + # This call will return two values 52 | + # 1) the updated range_check_ptr 53 | + # 2) 0 or 1 depending on which of a and b is greater 54 | + call is_le 55 | + 56 | + # Push return values to the stack 57 | + # There is two of them to push: range_check_ptr and max 58 | + 59 | + # Push the first one, the updated range_check_ptr, onto the stack 60 | + [ap] = [ap - 2]; ap++ 61 | + 62 | + # Conditional jump 63 | + # The following blocks are an assembly level equivalent of the if/else pattern 64 | + jmp b_is_more if [ap - 2] !=0 # here [ap-2] is the second value returned by is_le, our boolean 65 | + 66 | + # Push either a or b to the stack 67 | + a_is_more: 68 | + [ap] = [fp - 4]; ap++ 69 | + jmp done 70 | + 71 | + b_is_more: 72 | + [ap] = [fp -3]; ap++ 73 | + 74 | + done: 75 | + ret 76 | @@ -79 +90 @@ 77 | - 78 | + 79 | -------------------------------------------------------------------------------- /.patches/registers/registers04.cairo.patch: -------------------------------------------------------------------------------- 1 | --- ./exercises/registers/registers04.cairo 2022-08-07 17:55:58.875865309 +0200 2 | +++ ./.solutions/registers04.cairo 2022-08-07 17:55:48.967775665 +0200 3 | @@ -12,0 +13 @@ 4 | + let res = 67 + x * (45 + x * (23 + x)) 5 | @@ -19,0 +21,6 @@ 6 | + [ap] = x; ap++ 7 | + [ap] = [ap - 1] + 23; ap++ 8 | + [ap] = [ap - 1] * [ap - 2]; ap++ 9 | + [ap] = [ap - 1] + 45; ap++ 10 | + [ap] = [ap - 1] * [ap - 4]; ap++ 11 | + [ap] = [ap - 1] + 67; ap++ 12 | -------------------------------------------------------------------------------- /.patches/revoked_references/revoked_references01.cairo.patch: -------------------------------------------------------------------------------- 1 | diff --git a/exercises/revoked_references/revoked_references01.cairo b/exercises/revoked_references/revoked_references01.cairo 2 | index 2c0fe0f..23884e6 100644 3 | --- a/exercises/revoked_references/revoked_references01.cairo 4 | +++ b/exercises/revoked_references/revoked_references01.cairo 5 | @@ -22,0 +23 @@ func bar{hash_ptr : HashBuiltin*}(): 6 | + alloc_locals 7 | @@ -26,0 +28 @@ func bar{hash_ptr : HashBuiltin*}(): 8 | + local hash_ptr : HashBuiltin* = hash_ptr 9 | -------------------------------------------------------------------------------- /.patches/storage/storage01.cairo.patch: -------------------------------------------------------------------------------- 1 | --- exercises/storage/storage01.cairo 2022-05-31 15:29:04.000000000 -0400 2 | +++ /tmp/storage/storage01.cairo 2022-05-31 15:27:25.000000000 -0400 3 | @@ -7,2 +7,3 @@ 4 | -# TODO 5 | -# Create a storage named `bool` storing a single felt 6 | +@storage_var 7 | +func bool() -> (bool: felt): 8 | +end 9 | -------------------------------------------------------------------------------- /.patches/storage/storage02.cairo.patch: -------------------------------------------------------------------------------- 1 | --- exercises/storage/storage02.cairo 2022-06-07 10:47:20.000000000 -0400 2 | +++ ./tmp.cairo 2022-06-07 10:44:57.000000000 -0400 3 | @@ -13,4 +13,11 @@ 4 | -# TODO 5 | -# Create a storage named wallet, mapping a felt to another 6 | -# Create a storage named height_map, mapping two felts to another 7 | -# Create a storage named id, mapping a felt to an Id 8 | +@storage_var 9 | +func wallet(address: felt) -> (amount: felt): 10 | +end 11 | + 12 | +@storage_var 13 | +func height_map(x: felt, y: felt) -> (z: felt): 14 | +end 15 | + 16 | +@storage_var 17 | +func id(account: felt) -> (id: Id): 18 | +end 19 | -------------------------------------------------------------------------------- /.patches/storage/storage03.cairo.patch: -------------------------------------------------------------------------------- 1 | --- exercises/storage/storage03.cairo 2022-05-31 15:33:08.000000000 -0400 2 | +++ /tmp/storage/storage03.cairo 2022-05-31 15:27:25.000000000 -0400 3 | @@ -13,2 +13,9 @@ 4 | - # TODO 5 | - # Implement toggle external 6 | + let (b) = bool.read() 7 | + 8 | + if b == 0: 9 | + bool.write(1) 10 | + else: 11 | + bool.write(0) 12 | + end 13 | + 14 | + return () 15 | @@ -19,2 +26 @@ 16 | - # TODO 17 | - # Implement view_bool 18 | + return bool.read() 19 | -------------------------------------------------------------------------------- /.patches/strings/strings00.cairo.patch: -------------------------------------------------------------------------------- 1 | --- exercises/strings/strings00.cairo 2022-05-20 18:37:19.510948914 +0200 2 | +++ .solutions/strings00.cairo 2022-05-20 18:37:13.110928901 +0200 3 | @@ -12,7 +12,9 @@ 4 | # TODO: Fix the say_hello function by returning the appropriate short strings 5 | 6 | func say_hello() -> (hello_string : felt, hello_felt : felt, hello_hex : felt): 7 | - # FILL ME 8 | + let hello_string = 'Hello Starklings' 9 | + let hello_felt = '#L2-2022 #L3-2023' 10 | + let hello_hex = 'buidl buidl buidl' 11 | return (hello_string, hello_felt, hello_hex) 12 | end 13 | 14 | -------------------------------------------------------------------------------- /.patches/strings/strings01.cairo.patch: -------------------------------------------------------------------------------- 1 | --- a/exercises/strings/strings01.cairo 2022-05-24 16:32:08.050760341 +0200 2 | +++ b/exercises/strings/strings01.cairo 2022-05-24 15:41:30.545171320 +0200 3 | @@ -11 +11,3 @@ 4 | - let key = 0 5 | + 6 | + # let key = 34642338947246174141841020580919298249066932857229157879292816894 7 | + let key = 'Twinkle Twinkle Little Star' - 'Another One Bites The Dust' 8 | @@ -19 +21,2 @@ 9 | - let ciphertext = 0 10 | + # let ciphertext = 3042032262124705672547337926850636172917305208722716617195516092604696795127 11 | + let ciphertext = ('Magic Starknet Money' - 0xc0de) / 1337 12 | -------------------------------------------------------------------------------- /.patches/syntax/syntax01.cairo.patch: -------------------------------------------------------------------------------- 1 | diff --git a/exercises/syntax/syntax01.cairo b/exercises/syntax/syntax01.cairo 2 | index f5b07a3..2e4537d 100644 3 | --- a/exercises/syntax/syntax01.cairo 4 | +++ b/exercises/syntax/syntax01.cairo 5 | @@ -0,0 +1 @@ 6 | +%lang starknet 7 | -------------------------------------------------------------------------------- /.patches/syntax/syntax02.cairo.patch: -------------------------------------------------------------------------------- 1 | diff --git a/exercises/syntax/syntax02.cairo b/exercises/syntax/syntax02.cairo 2 | index e210f76..a65e2a6 100644 3 | --- a/exercises/syntax/syntax02.cairo 4 | +++ b/exercises/syntax/syntax02.cairo 5 | @@ -8,0 +9 @@ 6 | +from starkware.cairo.common.cairo_builtins import HashBuiltin 7 | -------------------------------------------------------------------------------- /.patches/syntax/syntax03.cairo.patch: -------------------------------------------------------------------------------- 1 | diff --git a/exercises/syntax/syntax03.cairo b/exercises/syntax/syntax03.cairo 2 | index c07022c..6576af4 100644 3 | --- a/exercises/syntax/syntax03.cairo 4 | +++ b/exercises/syntax/syntax03.cairo 5 | @@ -9 +9 @@ 6 | -func takes_two_arguments_and_returns_one() -> (): 7 | +func takes_two_arguments_and_returns_one(a : felt, b : felt) -> (result : felt): 8 | -------------------------------------------------------------------------------- /.patches/syntax/syntax04.cairo.patch: -------------------------------------------------------------------------------- 1 | diff --git a/exercises/syntax/syntax04.cairo b/exercises/syntax/syntax04.cairo 2 | index 60e60bd..9e9e3d3 100644 3 | --- a/exercises/syntax/syntax04.cairo 4 | +++ b/exercises/syntax/syntax04.cairo 5 | @@ -19 +19 @@ func test_hello{syscall_ptr : felt*}(): 6 | - let (result) = returns_something() 7 | + let (result) = my_namespace.returns_something() 8 | -------------------------------------------------------------------------------- /.patches/syntax/syntax05.cairo.patch: -------------------------------------------------------------------------------- 1 | diff --git a/exercises/syntax/syntax05.cairo b/exercises/syntax/syntax05.cairo 2 | index bb65c7b..1dc484c 100644 3 | --- a/exercises/syntax/syntax05.cairo 4 | +++ b/exercises/syntax/syntax05.cairo 5 | @@ -7,0 +8,4 @@ 6 | +struct Currency: 7 | + member name : felt 8 | + member decimals : felt 9 | +end 10 | -------------------------------------------------------------------------------- /.patches/tricks/assert_bool.cairo.patch: -------------------------------------------------------------------------------- 1 | --- a/exercises/tricks/assert_bool.cairo 2022-06-02 15:40:52.048123812 +0200 2 | +++ b/exercises/tricks/assert_bool.cairo 2022-06-02 15:41:00.474738153 +0200 3 | @@ -10 +10 @@ 4 | - # FILL ME 5 | + assert 0 = (x - 1) * (y - 1) 6 | @@ -15 +15,2 @@ 7 | - # FILL ME 8 | + assert 2 = x + y 9 | + # assert 1 = x * y # works too 10 | @@ -20 +21 @@ 11 | - # FILL ME 12 | + assert 0 = x + y 13 | @@ -25 +26 @@ 14 | - # FILL ME 15 | + assert 1 = x + y 16 | -------------------------------------------------------------------------------- /.patches/tricks/inline_if.cairo.patch: -------------------------------------------------------------------------------- 1 | --- a/exercises/tricks/inline_if.cairo 2022-06-15 17:34:56.044139899 +0200 2 | +++ b/exercises/tricks/inline_if.cairo 2022-06-15 17:34:47.677432930 +0200 3 | @@ -15 +15,4 @@ 4 | - # FILL ME 5 | + let is_boolean_helper = (cond - TRUE) * (cond - FALSE) 6 | + assert 0 = is_boolean_helper 7 | + 8 | + let res = cond * val_true + (1 - cond) * val_false 9 | -------------------------------------------------------------------------------- /.patches/tricks/no_conditionals.cairo.patch: -------------------------------------------------------------------------------- 1 | --- a/exercises/tricks/no_conditionals.cairo 2022-06-02 14:49:04.224066616 +0200 2 | +++ a/exercises/tricks/no_conditionals.cairo 2022-06-02 14:46:49.463087238 +0200 3 | @@ -28,2 +28,2 @@ 4 | - # FILL ME 5 | - return (res) 6 | + let (not_zero) = is_not_zero((x - 1) * x) 7 | + return (res=1 - not_zero) 8 | @@ -37 +37,3 @@ 9 | - # FILL ME 10 | + let tmp = (x - 1337) * (x - 69420) * (x - 42) 11 | + let (s) = is_not_zero(tmp) 12 | + let res = s * 'meh' + (1 - s) * 'cool' 13 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | disable=missing-function-docstring, 4 | missing-module-docstring, 5 | missing-class-docstring, 6 | too-few-public-methods, 7 | line-too-long # not an issue to disable this rule since black automatically wraps the overflowing lines 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Starklings contributing guide 2 | 3 | Thank you for investing your time in contributing to our project! 4 | 5 | In this guide you will find the necessary instructions to get up and running with developing Starklings new features or fixing some bugs. 6 | 7 | --- 8 | 9 | ## Setting up a development environment 10 | 11 | ### Requirements 12 | 13 | - [Python >=3.8 <3.9](https://www.python.org/downloads/) 14 | 15 | ### Setting up environment 16 | 17 | 1. Install Python version management tool: [pyenv](https://github.com/pyenv/pyenv) or [asdf](https://github.com/asdf-vm/asdf) 18 | 2. Install `Python 3.8` using the Python version management tool and activate that version 19 | 3. Clone this repository 20 | 4. Verify the active Python version: `python -V` 21 | 5. Create Python virtual environment in the project directory: `python -m venv env` 22 | 6. Activate environment: `source env/bin/activate` 23 | 7. Upgrade pip: `pip install --upgrade pip` 24 | 8. Install [Poetry](https://python-poetry.org/) — a dependency manager: `pip install poetry` 25 | 9. Install project dependencies: `poetry install` 26 | 27 | > Troubleshooting: if you run on a Mac M1, you might encounter the following error: `fatal error: 'gmp.h' file not found` 28 | > See https://github.com/OpenZeppelin/nile/issues/22 for detailed solutions. 29 | 30 | ## How can I contribute? 31 | 32 | ### Report a bug or suggest an enhancement 33 | 34 | If you spot a problem or have an idea for a cool feature, first make sure there is no related [existing issue](https://github.com/onlydustxyz/starklings/issues "Starklings GitHub Issues"). 35 | If not, you can open a [new Issue](https://github.com/onlydustxyz/starklings/issues/new/choose "Open a new Issue") and add a description of your suggestions. 36 | 37 | ### Submitting changes 38 | 39 | Please send a GitHub Pull Request with a list of what your changes accomplish. 40 | Make sure to follow the guidelines below. 41 | 42 | ### Development guidelines 43 | 44 | See [StarkNet development guidelines](https://github.com/onlydustxyz/development-guidelines/blob/main/starknet/README.md). 45 | 46 | ## Where can I ask a question? 47 | 48 | If you have a question, please ask it in [Discord](https://discord.gg/kutmDrKv "OnlyDust Discord"). 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

Starklings

3 |

4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |

An interactive tutorial to get you up and running with Starknet

17 |
18 | 19 |
20 | 21 | 22 | 23 |
24 |

This repository is no more maintained and has been moved to :

25 | starklings-cairo1 26 |
27 | 28 | 29 | 30 |
31 | 32 | --- 33 | 34 | ## Installation 35 | 36 | Clone the repository to your local machine: 37 | 38 | ```shell 39 | git clone --branch stable --single-branch https://github.com/onlydustxyz/starklings.git 40 | ``` 41 | 42 | Then install the tool, run: 43 | 44 | ```shell 45 | curl -L https://raw.githubusercontent.com/onlydustxyz/starklings/master/install.sh | bash 46 | ``` 47 | 48 | ## Usage 49 | 50 | Run the tool in watch mode in the repository and follow the instructions: 51 | 52 | ```shell 53 | cd PATH_TO_THE_CLONED_REPO 54 | starklings --watch 55 | ``` 56 | 57 | To verify an exercise without the watch mode run: 58 | 59 | ```shell 60 | starklings --verify relative_path_to_the_exercise_file 61 | ``` 62 | 63 | To display one solution to a given exercise run: 64 | 65 | ```shell 66 | starklings --solution relative_path_to_the_exercise_file 67 | ``` 68 | 69 | ## Update 70 | 71 | The tool is in continuous evolution. You might want to update it regularly. 72 | 73 | ```shell 74 | git pull 75 | bash install.sh 76 | ``` 77 | 78 | ## Inspiration 79 | 80 | - [Protostar](https://github.com/software-mansion/protostar) for all the project tooling and setup, deployment, 81 | packaging 82 | - [Rustlings](https://github.com/rust-lang/rustlings) for the amazing pedagogy and brilliant concept of progressive and 83 | interactive tutorial 84 | 85 | ## Contributing 86 | 87 | See [CONTRIBUTING.md](CONTRIBUTING.md). 88 | -------------------------------------------------------------------------------- /deploy.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sys 3 | from pathlib import Path 4 | from typing import Optional 5 | 6 | import tomli 7 | from git.repo import Repo 8 | from packaging import version 9 | 10 | # check if the active branch is master 11 | SCRIPT_ROOT = Path(__file__).parent 12 | repo = Repo(SCRIPT_ROOT) 13 | 14 | assert str(repo.active_branch) == "main", "Checkout to main and try again." 15 | assert len(repo.index.diff(None)) == 0, "There are uncommited changes, aborting." 16 | assert not repo.head.is_detached, "You are on a detached HEAD, aborting." 17 | 18 | # get current Starklings version 19 | path = SCRIPT_ROOT / "pyproject.toml" 20 | 21 | new_starklings_version_str: Optional[str] = None 22 | with open(path, "r+", encoding="UTF-8") as file: 23 | raw_pyproject = file.read() 24 | pyproject = tomli.loads(raw_pyproject) 25 | version_str = pyproject["tool"]["poetry"]["version"] 26 | starklings_version = version.parse(version_str) 27 | print(f"Current Starklings version: {starklings_version}") 28 | 29 | # prompt new Starklings version 30 | new_starklings_version_str = input("Provide the new Starklings version: ") 31 | 32 | # validate new version 33 | match_result = re.compile(r"^\d*\.\d*\.\d*$").match(new_starklings_version_str) 34 | if match_result is None: 35 | print("Invalid syntax") 36 | sys.exit(1) 37 | new_starklings_version = version.parse(new_starklings_version_str) 38 | 39 | if new_starklings_version <= starklings_version: 40 | print(f"New version must be greater than {starklings_version}") 41 | sys.exit(1) 42 | 43 | # update version in starklings.toml 44 | file.seek(0) 45 | file.truncate() 46 | file.write( 47 | raw_pyproject.replace( 48 | f'version = "{starklings_version}"', f'version = "{new_starklings_version}"' 49 | ) 50 | ) 51 | 52 | assert new_starklings_version_str is not None 53 | 54 | # add commit 55 | repo.git.add("pyproject.toml") 56 | commit = repo.index.commit(f":bookmark: Starklings {new_starklings_version_str}") 57 | 58 | # add tag 59 | tag = repo.create_tag(f"v{new_starklings_version_str}", ref=commit.hexsha) 60 | 61 | # push to main 62 | origin = repo.remote(name="origin") 63 | origin.push() 64 | origin.push(tag.path) 65 | -------------------------------------------------------------------------------- /exercises/builtins/README.md: -------------------------------------------------------------------------------- 1 | ## Builtins 2 | 3 | Builtins are low-level add-ons that you can import in your Cairo code and implement a specific set of functions in an optimized fashion. 4 | Each builtin is assigned a separate memory location, accessible through regular Cairo memory calls using implicit parameters. 5 | Some builtins have their own struct helpers you can import from `starkware.cairo.common.cairo_builtins`. 6 | 7 | To call a builtin that computes a specific function, simply assign the input using `assert`, magically read the output and finally advance the builtin pointer. 8 | 9 | Example of builtins include: 10 | - output: prints a felt in the output memory 11 | - range_check: verifies a felt value is lower than the range check bound 12 | - pedersen: compute the pedersen hash of two felt inputs (HashBuiltin*) 13 | - bitwise: computes logical bit operation on felts (BitwiseBuiltin*) 14 | - ecdsa: computes signatures on the Stark elliptic curve (SignatureBuiltin*) 15 | - ec_op: computes elliptic curve operation on the Stark curve (EcOpBuiltin*) 16 | 17 | ## Resources: 18 | - https://www.cairo-lang.org/docs/how_cairo_works/builtins.html 19 | - https://github.com/starkware-libs/cairo-lang/blob/master/src/starkware/cairo/common/cairo_builtins.cairo -------------------------------------------------------------------------------- /exercises/builtins/bitwise.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | 3 | from starkware.cairo.common.math import assert_le, assert_nn 4 | from starkware.cairo.common.math_cmp import is_not_zero 5 | from starkware.cairo.common.bitwise import bitwise_and, bitwise_xor, bitwise_or 6 | from starkware.cairo.common.cairo_builtins import BitwiseBuiltin 7 | from starkware.cairo.common.pow import pow 8 | 9 | # While operations like addition, multiplication and divisions are native for felts, 10 | # bit operations are more difficult to implement for felt. 11 | # The bitwise builtin allows computing logical operations such as XOR, AND and OR on 251-bit felts. 12 | 13 | # Resources: 14 | # - https://github.com/starkware-libs/cairo-lang/blob/master/src/starkware/cairo/common/bitwise.cairo 15 | # - https://github.com/starkware-libs/cairo-lang/blob/master/src/starkware/cairo/common/cairo_builtins.cairo#L17 16 | 17 | # I AM NOT DONE 18 | 19 | # TODO: Use a bitwise operation to return the n-th bit of the value parameter 20 | 21 | func get_nth_bit{bitwise_ptr : BitwiseBuiltin*, range_check_ptr : felt}(value, n) -> (res): 22 | # FILL ME 23 | return (res) 24 | end 25 | 26 | # TODO: Use a bitwise operation to set the n-th bit of the value parameter to 1 27 | 28 | func set_nth_bit{bitwise_ptr : BitwiseBuiltin*, range_check_ptr : felt}(value, n) -> (res): 29 | # FILL ME 30 | return (res) 31 | end 32 | 33 | # TODO: Use a bitwise operation to toggle the n-th bit of the value parameter 34 | 35 | func toggle_nth_bit{bitwise_ptr : BitwiseBuiltin*, range_check_ptr : felt}(value, n) -> (res): 36 | # FILL ME 37 | return (res) 38 | end 39 | 40 | # Let's write a unique function that combines all the functions above. 41 | # This function should use the bitwise_ptr explicitly. 42 | # In particular, do not use any of the functions above. 43 | 44 | # TODO: Write a function that takes as argument 45 | # - a felt `op` in ['get', 'set', 'toggle'], 46 | # - felts `value` and `n`, 47 | # and returns the result of the operation applied to `n`-th bit of value. 48 | # Make sure 49 | # - the argument `n` is within the correct bitwise bounds, 50 | # - the `op` argument is correct. 51 | 52 | func op_nth_bit{bitwise_ptr : BitwiseBuiltin*, range_check_ptr : felt}(op, value, n) -> (res): 53 | alloc_locals 54 | 55 | # Assert op is correct 56 | 57 | with_attr error_message("Bad bitwise bounds"): 58 | # Assert n is within bounds 59 | end 60 | 61 | # Compute the operation 62 | # Don't forget to advance bitwise_ptr 63 | 64 | return (res) 65 | end 66 | 67 | # Do not modify the tests. 68 | @view 69 | func test_get_nth_bit{syscall_ptr : felt*, bitwise_ptr : BitwiseBuiltin*, range_check_ptr : felt}(): 70 | alloc_locals 71 | local value = 0xA8 72 | let (local r0) = get_nth_bit(value, 0) 73 | let (local r1) = get_nth_bit(value, 1) 74 | let (local r2) = get_nth_bit(value, 2) 75 | let (local r3) = get_nth_bit(value, 3) 76 | let (local r4) = get_nth_bit(value, 4) 77 | let (local r5) = get_nth_bit(value, 5) 78 | let (local r6) = get_nth_bit(value, 6) 79 | let (local r7) = get_nth_bit(value, 7) 80 | assert r0 * 2 ** 0 + r1 * 2 ** 1 + r2 * 2 ** 2 + r3 * 2 ** 3 + r4 * 2 ** 4 + r5 * 2 ** 5 + r6 * 2 ** 6 + r7 * 2 ** 7 = value 81 | return () 82 | end 83 | 84 | @view 85 | func test_set_nth_bit{syscall_ptr : felt*, bitwise_ptr : BitwiseBuiltin*, range_check_ptr : felt}(): 86 | alloc_locals 87 | local value = 0 88 | let (local r0) = set_nth_bit(value, 0) 89 | assert 1 = r0 90 | let (local r1) = set_nth_bit(r0, 1) 91 | assert 3 = r1 92 | let (local r2) = set_nth_bit(r1, 2) 93 | assert 7 = r2 94 | let (local r3) = set_nth_bit(r2, 3) 95 | assert 15 = r3 96 | let (local r4) = set_nth_bit(r3, 4) 97 | assert 31 = r4 98 | let (local r5) = set_nth_bit(r4, 5) 99 | assert 63 = r5 100 | let (local r6) = set_nth_bit(r5, 6) 101 | assert 127 = r6 102 | let (local r7) = set_nth_bit(r6, 7) 103 | assert 255 = r7 104 | return () 105 | end 106 | 107 | @view 108 | func test_toggle_nth_bit{ 109 | syscall_ptr : felt*, bitwise_ptr : BitwiseBuiltin*, range_check_ptr : felt 110 | }(): 111 | alloc_locals 112 | 113 | local value = nondet %{ int('100000011010111', 2) %} 114 | let (res) = toggle_nth_bit(value, 14) 115 | let (res) = toggle_nth_bit(res, 3) 116 | let (res) = toggle_nth_bit(res, 5) 117 | assert res = 2 ** 8 - 1 118 | return () 119 | end 120 | 121 | @view 122 | func test_op_nth_bit{syscall_ptr : felt*, bitwise_ptr : BitwiseBuiltin*, range_check_ptr : felt}(): 123 | alloc_locals 124 | local v0 125 | local v1 126 | local v2 127 | local n0 128 | local n1 129 | local n2 130 | local r0 131 | local r1 132 | local r2 133 | %{ 134 | from random import randint 135 | size = 249 136 | ids.v0 = randint(0,2**size) + 2**size 137 | tmpv1 = randint(0,2**size) + 2**size 138 | ids.v2 = randint(0,2**size) + 2**size 139 | ids.n0 = randint(0,size) 140 | ids.n1 = randint(0,size) 141 | ids.n2 = randint(0,size) 142 | ids.v1 = tmpv1 ^ (1 << ids.n1) if ((tmpv1 >> ids.n1) & 1) == 1 else tmpv1 143 | ids.r0 = (ids.v0 >> ids.n0) & 1 144 | ids.r1 = ids.v1 | (1 << ids.n1) 145 | ids.r2 = ids.v2 ^ (1 << ids.n2) 146 | %} 147 | let (val0) = op_nth_bit('get', v0, n0) 148 | assert r0 = val0 149 | let (val1) = op_nth_bit('set', v1, n1) 150 | assert r1 = val1 151 | let (val2) = op_nth_bit('toggle', v2, n2) 152 | assert r2 = val2 153 | 154 | %{ expect_revert() %} 155 | let (_) = op_nth_bit('rigged', v0, n1) 156 | return () 157 | end 158 | 159 | @view 160 | func test_bitwise_bounds_negative_ko{ 161 | syscall_ptr : felt*, bitwise_ptr : BitwiseBuiltin*, range_check_ptr : felt 162 | }(): 163 | alloc_locals 164 | %{ expect_revert(error_message="Bad bitwise bounds") %} 165 | let (res) = op_nth_bit('set', 1337, -42) 166 | return () 167 | end 168 | 169 | @view 170 | func test_bitwise_bounds_too_high_ko{ 171 | syscall_ptr : felt*, bitwise_ptr : BitwiseBuiltin*, range_check_ptr : felt 172 | }(): 173 | alloc_locals 174 | %{ expect_revert(error_message="Bad bitwise bounds") %} 175 | let (res) = op_nth_bit('set', 1337, 251) 176 | return () 177 | end 178 | -------------------------------------------------------------------------------- /exercises/hints/README.md: -------------------------------------------------------------------------------- 1 | ## Hints 2 | 3 | Cairo is a non-deterministic programming language, 4 | it allows external programs to interact with the execution of a Cairo program. 5 | 6 | A hint is a piece of python code that is inserted in a Cairo program. 7 | Hints are attached to a Cairo instruction by encapsulating the hint code with the syntax: 8 | ``` 9 | %{ 10 | # Python hint goes here 11 | %} 12 | ``` 13 | 14 | For instance, to factor a number, you could let an external prover do the computation for you and verify the result afterwards. 15 | ``` 16 | let n = 1337 17 | local p 18 | local q 19 | %{ 20 | ids.p = 191 21 | ids.q = 7 22 | %} 23 | assert n = p * q 24 | ``` 25 | 26 | Hints are not part of the Cairo bytecode, they are not proven and do not count in the total number of steps. 27 | 28 | More information on how to use hints: 29 | https://www.cairo-lang.org/docs/hello_cairo/program_input.html#hints 30 | -------------------------------------------------------------------------------- /exercises/hints/hints00.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | 3 | # Hints 4 | 5 | # Hints are the Cairo way to defer part of the program execution to an external program 6 | # Memory can be accessed and assigned inside a hint by using variables identifiers. 7 | # e.g., inside a hint variable `a` is accessed through `ids.a` 8 | 9 | # I AM NOT DONE 10 | 11 | # TODO: Assign the value of `res` inside a hint. 12 | 13 | func basic_hint() -> (value : felt): 14 | alloc_locals 15 | local res 16 | # TODO: Insert hint here 17 | return (res) 18 | end 19 | 20 | # Do not change the test 21 | @external 22 | func test_basic_hint{syscall_ptr : felt*}(): 23 | let (value) = basic_hint() 24 | assert 41 = value - 1 25 | return () 26 | end 27 | -------------------------------------------------------------------------------- /exercises/hints/hints01.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | 3 | # Cairo hints can be useful for delegating heavy computation. 4 | # This pattern is at the very heart of Cairo: verification is much faster than computation. 5 | # However, as hints are not part of the final Cairo bytecode, a malicious program may provide wrong results. 6 | # You should always verify computations done inside hints. 7 | 8 | # I AM NOT DONE 9 | 10 | # TODO: Compute the result of "x modulo n" inside a hint using python's `divmod` 11 | # Don't forget to make sure the result is correct. 12 | 13 | func modulo(x : felt, n : felt) -> (mod : felt): 14 | alloc_locals 15 | local quotient 16 | local remainder 17 | %{ 18 | # TODO: Compute the quotient and remainder inside the hint 19 | print(ids.quotient) 20 | print(ids.remainder) 21 | %} 22 | # TODO: verify the result is correct 23 | 24 | return (0) 25 | end 26 | 27 | # Do not change the test 28 | @external 29 | func test_modulo{syscall_ptr : felt*}(): 30 | const NUM_TESTS = 19 31 | 32 | %{ import random %} 33 | tempvar count = NUM_TESTS 34 | 35 | loop: 36 | %{ 37 | x = random.randint(2, 2**99) 38 | n = random.randint(2, 2**50) 39 | if x < n: 40 | x,n = n,x 41 | %} 42 | tempvar x = nondet %{ x %} 43 | tempvar n = nondet %{ n %} 44 | tempvar res = nondet %{ x % n %} 45 | 46 | let (mod) = modulo(x, n) 47 | assert res = mod 48 | tempvar count = count - 1 49 | jmp loop if count != 0 50 | 51 | return () 52 | end 53 | -------------------------------------------------------------------------------- /exercises/implicit_arguments/README.md: -------------------------------------------------------------------------------- 1 | ## Implicit arguments 2 | 3 | Cairo functions takes two types of arguments: the explicit arguments, provided between `(` and `)`, and the implicit arguments, provided between `{` and `}`. 4 | 5 | Read more about implicit arguments here https://starknet.io/docs/how_cairo_works/builtins.html#implicit-arguments. 6 | -------------------------------------------------------------------------------- /exercises/implicit_arguments/implicit_arguments01.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | 3 | # Functions can take implicit arguments. You might have already encountered this with 4 | # syscall_ptr: felt* for example. 5 | 6 | # I AM NOT DONE 7 | 8 | # TODO: fix the "implicit_sum" signature to make the test pass 9 | 10 | func implicit_sum() -> (result : felt): 11 | return (a + b) 12 | end 13 | 14 | # Do not change the test 15 | @external 16 | func test_sum{syscall_ptr : felt*}(): 17 | let a = 3 18 | let b = 5 19 | let (sum) = implicit_sum{a=a, b=b}() 20 | assert sum = 8 21 | 22 | return () 23 | end 24 | -------------------------------------------------------------------------------- /exercises/implicit_arguments/implicit_arguments02.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | 3 | # Implicit arguments are passed down to any subsequent function calls that would require them. 4 | # Make good usage of this feature to pass this exercise! 5 | 6 | # I AM NOT DONE 7 | 8 | # TODO: fix the "child_function_1" and "child_function_2" signatures to make the test pass 9 | 10 | # Do not change the function signature 11 | func parent_function{a, b}() -> (result : felt): 12 | # Do not change the function body 13 | alloc_locals 14 | let (local intermediate_result_1) = child_function_1() 15 | let (local intermediate_result_2) = child_function_2() 16 | return (intermediate_result_1 + intermediate_result_2) 17 | end 18 | 19 | func child_function_1() -> (result : felt): 20 | # Do not change the function body 21 | return (2 * a) 22 | end 23 | 24 | func child_function_2() -> (result : felt): 25 | # Do not change the function body 26 | return (b + 3) 27 | end 28 | 29 | @external 30 | func test_sum{syscall_ptr : felt*}(): 31 | let a = 3 32 | let b = 5 33 | with a, b: 34 | let (result) = parent_function() 35 | assert result = 14 36 | end 37 | 38 | return () 39 | end 40 | -------------------------------------------------------------------------------- /exercises/implicit_arguments/implicit_arguments03.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | 3 | # What is really neat with implicit arguments is that they are returned implicitly by any function using them 4 | # This is a very powerful feature of the language since it helps with readability, letting the developer omit 5 | # implicit arguments in the subsequent function calls. 6 | 7 | # I AM NOT DONE 8 | 9 | # TODO: implement the "black_box" function body to make the test pass 10 | 11 | # Do not change the function signature! 12 | func black_box{secret : felt}() -> (): 13 | # Make the magic happen here :) 14 | return () 15 | end 16 | 17 | # Do not change the test 18 | @external 19 | func test_secret_change{syscall_ptr : felt*}(): 20 | let secret = 'no so secret' 21 | with secret: 22 | black_box() 23 | assert secret = 'very secret!' 24 | end 25 | 26 | return () 27 | end 28 | -------------------------------------------------------------------------------- /exercises/operations/README.md: -------------------------------------------------------------------------------- 1 | # Felt operations 2 | 3 | A field element (aka felt), is a native type handling all the main number basics in Cairo. 4 | 5 | You can learn more about field elements: [https://www.cairo-lang.org/docs/how_cairo_works/cairo_intro.html#field-elements](https://www.cairo-lang.org/docs/how_cairo_works/cairo_intro.html#field-elements. 6 | -------------------------------------------------------------------------------- /exercises/operations/operations00.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | 3 | # Felts supports basic math operations. 4 | # Only accepted operators (const excluded) are: +, -, * and / 5 | 6 | # I AM NOT DONE 7 | 8 | # TODO 9 | # Return the solution of (x² + x - 2) / (x - 2) 10 | func poly(x : felt) -> (res : felt): 11 | # FILL ME 12 | return (res=res) # Do not change 13 | end 14 | 15 | # Do not change the test 16 | @external 17 | func test_poly(): 18 | let (res) = poly(x=1) 19 | assert res = 0 20 | let (res) = poly(x=3) 21 | assert res = 10 22 | return () 23 | end 24 | -------------------------------------------------------------------------------- /exercises/operations/operations01.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | 3 | # Felts are integers defined in the range [0 ; P[, all compuations are done modulo P. 4 | # They can be unsigned integers using `let` or signed integers using `const`. 5 | # Exercice resources: https://www.cairo-lang.org/docs/how_cairo_works/cairo_intro.html#field-elements 6 | 7 | # I AM NOT DONE 8 | 9 | # TODO 10 | # Compute a number X which verify X + 1 < X using unsigned int 11 | @external 12 | func test_unsigned_integer(): 13 | # FILL ME 14 | let z = x + 1 15 | %{ assert ids.z < ids.x, f'assert failed: {ids.z} >= {ids.x}' %} 16 | return () 17 | end 18 | 19 | # TODO 20 | # Compute a number Y which verify Y + 1 < Y using signed int 21 | @external 22 | func test_signed_integer(): 23 | # FILL ME 24 | const z = y + 1 25 | %{ assert ids.z < ids.y, f'assert failed: {ids.z} >= {ids.y}' %} 26 | return () 27 | end 28 | -------------------------------------------------------------------------------- /exercises/operations/operations02.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | 3 | from starkware.cairo.common.math import assert_lt_felt, assert_not_zero 4 | 5 | # Felts use prime number property to ensure (x / y) * y = x is always true. 6 | # Since floats are not supported, this can lead to get surprising results. 7 | # Exercice resources: https://www.cairo-lang.org/docs/how_cairo_works/cairo_intro.html#field-elements 8 | 9 | # I AM NOT DONE 10 | 11 | # TODO 12 | # Find a number X which satisfy A / X > A with X in range ]0 ; 100] 13 | func solve(a : felt) -> (x : felt): 14 | # TO FILL 15 | return (x=x) 16 | end 17 | 18 | # Do not change the test 19 | @external 20 | func test_solve{range_check_ptr}(): 21 | let a = 347092984475551631116800 22 | let (x) = solve(a=a) 23 | assert_not_zero(x) 24 | assert_lt_felt(x, 101) 25 | assert_lt_felt(a, a / x) 26 | return () 27 | end 28 | -------------------------------------------------------------------------------- /exercises/operations/operations03.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | 3 | from starkware.cairo.common.math import assert_lt_felt 4 | 5 | # Felts use prime number property to ensure (x / y) * y = x is always true. 6 | # Even if `let` provide an unsigned integer, it is still possible to manage negative numbers thanks to field element properties. 7 | # Exercice resources: https://www.cairo-lang.org/docs/how_cairo_works/cairo_intro.html#field-elements 8 | 9 | # I AM NOT DONE 10 | 11 | # TODO 12 | # Set the value of x (in the hint) to verify the test 13 | func solve() -> (x : felt): 14 | tempvar x 15 | %{ ids.x = -1 %} # Change only this line to make the test pass 16 | return (x=x) 17 | end 18 | 19 | # Do not change the test 20 | @external 21 | func test_solve{range_check_ptr}(): 22 | let (x) = solve() 23 | assert x = -17 24 | return () 25 | end 26 | -------------------------------------------------------------------------------- /exercises/recursions/README.md: -------------------------------------------------------------------------------- 1 | # Recursions 2 | 3 | Something noticeable with Cairo is that there is no `for` or `while` loops. 4 | 5 | Loops must be implemented recursively, which is possible and simple in Cairo. 6 | -------------------------------------------------------------------------------- /exercises/recursions/array01.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | 3 | # Arrays can be passed as function arguments in the form of a pointer and a length. 4 | 5 | # I AM NOT DONE 6 | 7 | # TODO: write the "contains" function body that returns 1 if the haystack contains the needle and 0 otherwise. 8 | 9 | from starkware.cairo.common.alloc import alloc 10 | 11 | func contains(needle : felt, haystack : felt*, haystack_len : felt) -> (result : felt): 12 | return (0) 13 | end 14 | 15 | # Do not change the test 16 | @external 17 | func test_contains{syscall_ptr : felt*}(): 18 | let (haystack1 : felt*) = alloc() 19 | assert [haystack1] = 1 20 | assert [haystack1 + 1] = 2 21 | assert [haystack1 + 2] = 3 22 | assert [haystack1 + 3] = 4 23 | 24 | let (contains3) = contains(3, haystack1, 4) 25 | assert contains3 = 1 26 | 27 | let (haystack2 : felt*) = alloc() 28 | assert [haystack2] = 1 29 | assert [haystack2 + 1] = 2 30 | let (contains5) = contains(5, haystack2, 2) 31 | assert contains5 = 0 32 | return () 33 | end 34 | -------------------------------------------------------------------------------- /exercises/recursions/array02.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | 3 | # Getting pointer as function arguments let us modify the values at the memory address of the pointer 4 | # ...or not! Cairo memory is immutable. Therefore you cannot just update a memory cell. 5 | 6 | # I AM NOT DONE 7 | 8 | # TODO: Update the square function – you can change the body and the signature – 9 | # to make it achieve the desired result: returning an array 10 | # with the squared values of the input array. 11 | 12 | from starkware.cairo.common.alloc import alloc 13 | 14 | func square(array : felt*, array_len : felt): 15 | if array_len == 0: 16 | return () 17 | end 18 | 19 | let squared_item = array[0] * array[0] 20 | assert [array] = squared_item 21 | 22 | return square(array + 1, array_len - 1) 23 | end 24 | 25 | # You can update the test if the function signature changes. 26 | @external 27 | func test_square{syscall_ptr : felt*}(): 28 | alloc_locals 29 | let (local array : felt*) = alloc() 30 | 31 | assert [array] = 1 32 | assert [array + 1] = 2 33 | assert [array + 2] = 3 34 | assert [array + 3] = 4 35 | 36 | square(array, 4) 37 | 38 | assert [array] = 1 39 | assert [array + 1] = 4 40 | assert [array + 2] = 9 41 | assert [array + 3] = 16 42 | 43 | return () 44 | end 45 | -------------------------------------------------------------------------------- /exercises/recursions/array03.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | 3 | # There are multiple ways to scan through an array. 4 | # Using recursion, one could go forwards or backwards. 5 | 6 | # I AM NOT DONE 7 | 8 | from starkware.cairo.common.math_cmp import is_le 9 | from starkware.cairo.common.alloc import alloc 10 | 11 | # TODO 12 | # Scan through the array elements from first to last 13 | # Return 1 if elements of the array are in increasing order. 14 | # Return 0 otherwise 15 | 16 | func is_increasing{range_check_ptr : felt}(array : felt*, array_len : felt) -> (res : felt): 17 | if array_len == 0: 18 | return (1) 19 | end 20 | 21 | if array_len == 1: 22 | return (1) 23 | end 24 | 25 | let curr_value = 0 26 | let next_value = 0 27 | 28 | # Do not modify these lines 29 | let (is_sorted) = is_le(curr_value, next_value) 30 | if is_sorted == 1: 31 | return is_increasing(array + 1, array_len - 1) 32 | end 33 | 34 | return (0) 35 | end 36 | 37 | # TODO 38 | # Scan through the array elements from last to first 39 | # Return 1 if elements of the array are in decreasing order. 40 | # Return 0 otherwise 41 | 42 | func is_decreasing{range_check_ptr : felt}(array : felt*, array_len : felt) -> (res : felt): 43 | # FILL ME 44 | 45 | # Do not modify this line 46 | let (is_sorted) = is_le(curr_value, next_value) 47 | 48 | if is_sorted == 1: 49 | return is_decreasing(array, array_len) 50 | end 51 | 52 | return (0) 53 | end 54 | 55 | # TODO 56 | # Use recursion to reverse array in rev_array 57 | # Assume rev_array is already allocated 58 | 59 | func reverse(array : felt*, rev_array : felt*, array_len : felt): 60 | # FILL ME 61 | return () 62 | end 63 | 64 | # Do not modify the test 65 | @external 66 | func test_is_sorted{syscall_ptr : felt*, range_check_ptr : felt}(): 67 | alloc_locals 68 | 69 | local inc_array : felt* = new (1, 2, 3, 4) 70 | local bad_array : felt* = new (1, 2, 69, -11, 0) 71 | local dec_array : felt* = new (10, 9, 8, 7, 6, 5) 72 | let (inc0) = is_increasing(inc_array, 4) 73 | let (inc1) = is_increasing(bad_array, 5) 74 | let (inc2) = is_increasing(dec_array, 6) 75 | assert (inc0, inc1, inc2) = (1, 0, 0) 76 | 77 | let (dec0) = is_decreasing(inc_array, 4) 78 | let (dec1) = is_decreasing(bad_array, 5) 79 | let (dec2) = is_decreasing(dec_array, 6) 80 | assert (dec0, dec1, dec2) = (0, 0, 1) 81 | 82 | return () 83 | end 84 | 85 | # Do not modify the test 86 | @external 87 | func test_reversed{syscall_ptr : felt*}(): 88 | alloc_locals 89 | 90 | local in_array : felt* = new (1, 2, 3, 4, 19, 42) 91 | let (reversed_array : felt*) = alloc() 92 | reverse(in_array, reversed_array, 6) 93 | assert 42 = [reversed_array + 0] 94 | assert 19 = [reversed_array + 1] 95 | assert 4 = [reversed_array + 2] 96 | assert 3 = [reversed_array + 3] 97 | assert 2 = [reversed_array + 4] 98 | assert 1 = [reversed_array + 5] 99 | 100 | local in_array : felt* = new (31337, 1664, 911, 0, -42) 101 | let (reversed_array : felt*) = alloc() 102 | reverse(in_array, reversed_array, 5) 103 | assert -42 = [reversed_array + 0] 104 | assert 0 = [reversed_array + 1] 105 | assert 911 = [reversed_array + 2] 106 | assert 1664 = [reversed_array + 3] 107 | assert 31337 = [reversed_array + 4] 108 | 109 | return () 110 | end 111 | -------------------------------------------------------------------------------- /exercises/recursions/array04.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | 3 | # Arrays can also contain structs 4 | 5 | # I AM NOT DONE 6 | 7 | struct Point: 8 | member x : felt 9 | member y : felt 10 | member z : felt 11 | end 12 | 13 | func contains_origin{range_check_ptr : felt}(len_points : felt, points : Point*) -> (bool : felt): 14 | # FILL ME 15 | end 16 | 17 | # TESTS # 18 | 19 | from starkware.cairo.common.alloc import alloc 20 | 21 | @external 22 | func test_contrains_origin{range_check_ptr : felt}(): 23 | alloc_locals 24 | 25 | let (local false_array : Point*) = alloc() 26 | assert false_array[0] = Point(1, 2, 3) 27 | assert false_array[1] = Point(2, 2, 2) 28 | assert false_array[2] = Point(42, 27, 11) 29 | 30 | let (res) = contains_origin(3, false_array) 31 | assert res = 0 32 | 33 | let (local true_array : Point*) = alloc() 34 | assert true_array[0] = Point(1, 2, 3) 35 | assert true_array[1] = Point(0, 0, 0) 36 | assert true_array[2] = Point(42, 27, 11) 37 | 38 | let (res) = contains_origin(3, true_array) 39 | assert res = 1 40 | 41 | return () 42 | end 43 | -------------------------------------------------------------------------------- /exercises/recursions/collatz_sequence.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | %builtins bitwise 3 | 4 | from starkware.cairo.common.bitwise import bitwise_and 5 | from starkware.cairo.common.cairo_builtins import BitwiseBuiltin 6 | 7 | # Collatz sequence are defined as follow : 8 | # If the number is even, divide it by two. 9 | # If the number is odd, triple it and add one. 10 | # If the number is one, stop the computation 11 | # https://en.wikipedia.org/wiki/Collatz_conjecture 12 | 13 | # I AM NOT DONE 14 | 15 | # TODO: write a recursive implementation of Collatz sequence that returns the nth collatz number from the seed 16 | # HELP: number % 2 == 0 => bitwise_and(number, 1) == 0 17 | 18 | func collatz{bitwise_ptr : BitwiseBuiltin*}(seed : felt, step : felt) -> (result : felt): 19 | return (result=number) 20 | end 21 | 22 | # Do not change the test 23 | @external 24 | func test_collatz{syscall_ptr : felt*, bitwise_ptr : BitwiseBuiltin*}(): 25 | let (n) = collatz(seed=42, step=0) 26 | assert n = 42 27 | let (n) = collatz(seed=42, step=1) 28 | assert n = 21 29 | let (n) = collatz(seed=42, step=2) 30 | assert n = 64 31 | let (n) = collatz(seed=42, step=3) 32 | assert n = 32 33 | let (n) = collatz(seed=42, step=4) 34 | assert n = 16 35 | let (n) = collatz(seed=42, step=5) 36 | assert n = 8 37 | let (n) = collatz(seed=42, step=6) 38 | assert n = 4 39 | let (n) = collatz(seed=42, step=7) 40 | assert n = 2 41 | let (n) = collatz(seed=42, step=8) 42 | assert n = 1 43 | let (n) = collatz(seed=42, step=9) 44 | assert n = 1 45 | let (n) = collatz(seed=42, step=42) 46 | assert n = 1 47 | return () 48 | end 49 | -------------------------------------------------------------------------------- /exercises/recursions/recursion01.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | 3 | # Fibonacci numbers are defined by the following recurrence: 4 | # F(0) = 0 5 | # F(1) = 1 6 | # F(n) = F(n-1) + F(n-2) 7 | 8 | # I AM NOT DONE 9 | 10 | # TODO: write a recursive implementation of fibonacci numbers that returns the nth fibonacci number 11 | 12 | func fibonacci(n : felt) -> (result : felt): 13 | return (0) 14 | end 15 | 16 | # Do not change the test 17 | @external 18 | func test_fibonacci{syscall_ptr : felt*}(): 19 | let (n) = fibonacci(0) 20 | assert n = 0 21 | let (n) = fibonacci(1) 22 | assert n = 1 23 | let (n) = fibonacci(7) 24 | assert n = 13 25 | let (n) = fibonacci(10) 26 | assert n = 55 27 | return () 28 | end 29 | -------------------------------------------------------------------------------- /exercises/recursions/struct01.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | 3 | # Structs are nothing more than a continuous list of felts in memory. So are arrays. 4 | # 5 | # In other words, a felt* can point to an element of an array, but also 6 | # to an element of a struct. It is therefore possible to access the next element of a 7 | # struct by incrementing the pointer, just like with arrays. 8 | # 9 | # It is also worth noting that a struct pointer implicitely casts to a felt pointer 10 | # (eg. Foo* is automatically casted to felt*) 11 | 12 | # I AM NOT DONE 13 | 14 | struct Vector2D: 15 | member x : felt 16 | member y : felt 17 | end 18 | 19 | struct Vector3D: 20 | member x : felt 21 | member y : felt 22 | member z : felt 23 | end 24 | 25 | # Returns the squared magnitude of a vector. 26 | # Examples: 27 | # - the squared magnitude of a 2D vector (x,y) is x * x + y * y. 28 | # - the squared magnitude of a 3D vector (x,y,z) is x * x + y * y + z * z. 29 | func squared_magnitude(struct_value : felt*, struct_size : felt) -> (res : felt): 30 | # FILL ME 31 | end 32 | 33 | # TESTS # 34 | 35 | @external 36 | func test_squared_magnitude{range_check_ptr : felt}(): 37 | tempvar vector2D : Vector2D* = new Vector2D(x=4, y=7) 38 | let (res) = squared_magnitude(vector2D, Vector2D.SIZE) 39 | assert res = 4 * 4 + 7 * 7 40 | 41 | tempvar vector3D : Vector3D* = new Vector3D(x=8, y=2, z=1) 42 | let (res) = squared_magnitude(vector3D, Vector3D.SIZE) 43 | assert res = 8 * 8 + 2 * 2 + 1 * 1 44 | 45 | return () 46 | end 47 | -------------------------------------------------------------------------------- /exercises/registers/registers00.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | 3 | # I AM NOT DONE 4 | 5 | # Ressources 6 | # https://www.cairo-lang.org/docs/how_cairo_works/cairo_intro.html#registers 7 | # https://www.cairo-lang.org/docs/how_cairo_works/functions.html#function-arguments-and-return-values 8 | 9 | # TODO 10 | # Rewrite this function body in a high level syntax 11 | @external 12 | func ret_42() -> (r : felt): 13 | # [ap] = 42; ap++ 14 | # ret 15 | end 16 | 17 | # TODO 18 | # Rewrite this function body in a low level syntax, using registers 19 | @external 20 | func ret_0_and_1() -> (zero : felt, one : felt): 21 | # return (0, 1) 22 | end 23 | 24 | ######### 25 | # TESTS # 26 | ######### 27 | 28 | @external 29 | func test_ret_42(): 30 | let (r) = ret_42() 31 | assert r = 42 32 | 33 | return () 34 | end 35 | 36 | @external 37 | func test_0_and_1(): 38 | let (zero, one) = ret_0_and_1() 39 | assert zero = 0 40 | assert one = 1 41 | 42 | return () 43 | end 44 | -------------------------------------------------------------------------------- /exercises/registers/registers01.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | 3 | # I AM NOT DONE 4 | 5 | # Resource 6 | # https://www.cairo-lang.org/docs/how_cairo_works/functions.html#function-arguments-and-return-values 7 | 8 | # TODO 9 | # Rewrite this function with a high level syntax 10 | @external 11 | func assert_is_42(n : felt): 12 | # [ap - 3] = 42 13 | # ret 14 | end 15 | 16 | # TODO 17 | # Rewrite this function with a low level syntax, using registers 18 | @external 19 | func sum(a : felt, b : felt) -> (s : felt): 20 | # return (a + b) 21 | end 22 | 23 | ######### 24 | # TESTS # 25 | ######### 26 | 27 | @external 28 | func test_assert_is_42_ok(): 29 | assert_is_42(42) 30 | return () 31 | end 32 | 33 | @external 34 | func test_assert_is_42_ko(): 35 | %{ expect_revert() %} 36 | assert_is_42(21) 37 | return () 38 | end 39 | 40 | @external 41 | func test_sum(): 42 | let (s) = sum(2, 3) 43 | assert s = 5 44 | return () 45 | end 46 | -------------------------------------------------------------------------------- /exercises/registers/registers02.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | 3 | # I AM NOT DONE 4 | 5 | # Cairo memory is immutable. 6 | # Once a memory cell has been assigned, its value CANNOT be changed. 7 | # The program will crash if someone tries to assign a new, different, value to an already initialized memory cell. 8 | # However, trying to assign a memory cell twice, or more, **with the same value** won't cause any harm. 9 | # This property can be used to assert the value of a cell. 10 | 11 | # TODO 12 | # Rewrite this function in a high level syntax, using tempvar and assert 13 | func crash(): 14 | # [ap] = 42; ap++ 15 | # [ap - 1] = 42 16 | # [ap - 1] = 21 17 | 18 | ret 19 | end 20 | 21 | # TODO 22 | # Rewrite this funtion in a low level syntax 23 | func assert_42(number : felt): 24 | # assert number = 42 25 | 26 | return () 27 | end 28 | 29 | # TODO 30 | # Write this function body so: 31 | # if the memory cell pointed by `p_number` is not initialized, set it to 42 32 | # else, if the value is initialized and different from 42, crash 33 | # else, do nothing and return 34 | func assert_pointer_42(p_number : felt*): 35 | return () 36 | end 37 | 38 | # TODO 39 | # Write this function body so: 40 | # if the memory cell pointed by `p_number` is set to 42, do nothing and return 41 | # else crash 42 | func assert_pointer_42_no_set(p_number : felt*): 43 | return () 44 | end 45 | 46 | ######### 47 | # TESTS # 48 | ######### 49 | 50 | from starkware.cairo.common.alloc import alloc 51 | 52 | @external 53 | func test_crash(): 54 | %{ expect_revert() %} 55 | crash() 56 | 57 | return () 58 | end 59 | 60 | @external 61 | func test_assert_42(): 62 | assert_42(42) 63 | 64 | %{ expect_revert() %} 65 | assert_42(21) 66 | 67 | return () 68 | end 69 | 70 | @external 71 | func test_assert_pointer_42_initialized(): 72 | let (mem_zone : felt*) = alloc() 73 | assert mem_zone[0] = 42 74 | assert mem_zone[1] = 21 75 | 76 | assert_pointer_42(mem_zone) 77 | 78 | %{ expect_revert() %} 79 | assert_pointer_42(mem_zone + 1) 80 | 81 | return () 82 | end 83 | 84 | @external 85 | func test_assert_pointer_42_not_initialized_ok(): 86 | let (mem_zone : felt*) = alloc() 87 | assert mem_zone[0] = 42 88 | assert_pointer_42(mem_zone) 89 | 90 | assert_pointer_42(mem_zone + 1) 91 | assert mem_zone[1] = 42 92 | 93 | return () 94 | end 95 | 96 | @external 97 | func test_assert_pointer_42_not_initialized_revert(): 98 | let (mem_zone : felt*) = alloc() 99 | assert mem_zone[0] = 42 100 | assert_pointer_42(mem_zone) 101 | 102 | assert_pointer_42(mem_zone + 1) 103 | %{ expect_revert() %} 104 | assert mem_zone[1] = 21 105 | 106 | return () 107 | end 108 | 109 | @external 110 | func test_assert_pointer_42_no_set(): 111 | let (mem_zone : felt*) = alloc() 112 | assert mem_zone[0] = 42 113 | assert mem_zone[1] = 21 114 | 115 | assert_pointer_42_no_set(mem_zone) 116 | 117 | %{ expect_revert() %} 118 | assert_pointer_42_no_set(mem_zone + 1) 119 | 120 | return () 121 | end 122 | 123 | @external 124 | func test_assert_pointer_42_no_set_crash(): 125 | let (mem_zone : felt*) = alloc() 126 | 127 | %{ expect_revert() %} 128 | assert_pointer_42_no_set(mem_zone) 129 | 130 | return () 131 | end 132 | -------------------------------------------------------------------------------- /exercises/registers/registers03.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | from starkware.cairo.common.math_cmp import is_le 3 | 4 | # I AM NOT DONE 5 | 6 | # TODO 7 | # Rewrite those functions with a high level syntax 8 | @external 9 | func sum_array(array_len : felt, array : felt*) -> (sum : felt): 10 | # [ap] = [fp - 4]; ap++ 11 | # [ap] = [fp - 3]; ap++ 12 | # [ap] = 0; ap++ 13 | # call rec_sum_array 14 | # ret 15 | end 16 | 17 | func rec_sum_array(array_len : felt, array : felt*, sum : felt) -> (sum : felt): 18 | # jmp continue if [fp - 5] != 0 19 | 20 | # stop: 21 | # [ap] = [fp - 3]; ap++ 22 | # jmp done 23 | 24 | # continue: 25 | # [ap] = [[fp - 4]]; ap++ 26 | # [ap] = [fp - 5] - 1; ap++ 27 | # [ap] = [fp - 4] + 1; ap++ 28 | # [ap] = [ap - 3] + [fp - 3]; ap++ 29 | # call rec_sum_array 30 | 31 | # done: 32 | # ret 33 | end 34 | 35 | # TODO 36 | # Rewrite this function with a low level syntax 37 | # It's possible to do it with only registers, labels and conditional jump. No reference or localvar 38 | @external 39 | func max{range_check_ptr}(a : felt, b : felt) -> (max : felt): 40 | # let (res) = is_le(a, b) 41 | # if res == 1: 42 | # return (b) 43 | # else: 44 | # return (a) 45 | # end 46 | end 47 | 48 | ######### 49 | # TESTS # 50 | ######### 51 | 52 | from starkware.cairo.common.alloc import alloc 53 | 54 | @external 55 | func test_max{range_check_ptr}(): 56 | let (m) = max(21, 42) 57 | assert m = 42 58 | let (m) = max(42, 21) 59 | assert m = 42 60 | return () 61 | end 62 | 63 | @external 64 | func test_sum(): 65 | let (array) = alloc() 66 | assert array[0] = 1 67 | assert array[1] = 2 68 | assert array[2] = 3 69 | assert array[3] = 4 70 | assert array[4] = 5 71 | assert array[5] = 6 72 | assert array[6] = 7 73 | assert array[7] = 8 74 | assert array[8] = 9 75 | assert array[9] = 10 76 | 77 | let (s) = sum_array(10, array) 78 | assert s = 55 79 | 80 | return () 81 | end 82 | -------------------------------------------------------------------------------- /exercises/registers/registers04.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | 3 | # Felts supports basic math operations. 4 | # High level syntax allows one-line multiple operations while low level syntax doesn't. 5 | # Exercice source: https://www.cairo-lang.org/docs/how_cairo_works/cairo_intro.html#field-elements 6 | 7 | # I AM NOT DONE 8 | 9 | # TODO 10 | # Write this function body in a high level syntax 11 | func poly_high_level(x : felt) -> (res : felt): 12 | # return x³ + 23x² + 45x + 67 according to x 13 | return (res=res) # Do not change 14 | end 15 | 16 | # TODO 17 | # Write this function body in a low level syntax (result must be stored in [ap - 1] before ret) 18 | func poly_low_level(x : felt): 19 | # return x³ + 23x² + 45x + 67 according to x 20 | ret # Do not change 21 | end 22 | 23 | # Do not change the test 24 | @external 25 | func test_poly(): 26 | poly_low_level(x=100) 27 | assert [ap - 1] = 1234567 28 | let (high_level_res) = poly_high_level(x=100) 29 | assert high_level_res = 1234567 30 | return () 31 | end 32 | -------------------------------------------------------------------------------- /exercises/revoked_references/revoked_references01.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | 3 | # References in Cairo are like aliases to specific memory cells pointed by ap 4 | 5 | # I AM NOT DONE 6 | 7 | # TODO: complete the bar function to make the test pass 8 | # You will encounter a "revoked reference" error 9 | # https://www.cairo-lang.org/docs/how_cairo_works/consts.html#revoked-references 10 | 11 | from starkware.cairo.common.cairo_builtins import HashBuiltin 12 | from starkware.cairo.common.hash import hash2 13 | 14 | func foo(n): 15 | if n == 0: 16 | return () 17 | end 18 | foo(n=n - 1) 19 | return () 20 | end 21 | 22 | func bar{hash_ptr : HashBuiltin*}(): 23 | hash2(1, 2) # Do not change 24 | foo(3) # Do not change 25 | 26 | # Insert something here to make the test pass 27 | 28 | hash2(3, 4) # Do not change 29 | return () # Do not change 30 | end 31 | 32 | # Do not change the test 33 | @external 34 | func test_bar{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(): 35 | bar{hash_ptr=pedersen_ptr}() 36 | 37 | return () 38 | end 39 | -------------------------------------------------------------------------------- /exercises/storage/storage01.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | 3 | # Starknet provide persistent and mutable storage 4 | 5 | # I AM NOT DONE 6 | 7 | # TODO 8 | # Create a storage named `bool` storing a single felt 9 | 10 | # TESTS # 11 | 12 | from starkware.cairo.common.cairo_builtins import HashBuiltin 13 | 14 | @external 15 | func test_store_bool{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(): 16 | let (x) = bool.read() 17 | assert x = 0 18 | 19 | bool.write(1) 20 | 21 | let (x) = bool.read() 22 | assert x = 1 23 | 24 | return () 25 | end 26 | -------------------------------------------------------------------------------- /exercises/storage/storage02.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | 3 | # Starknet storage can be though about as a hashmap 4 | 5 | # I AM NOT DONE 6 | 7 | struct Id: 8 | member age : felt 9 | member height : felt 10 | member married : felt 11 | end 12 | 13 | # TODO 14 | # Create a storage named wallet, mapping a felt to another 15 | # Create a storage named height_map, mapping two felts to another 16 | # Create a storage named id, mapping a felt to an Id 17 | 18 | # TESTS # 19 | 20 | from starkware.cairo.common.cairo_builtins import HashBuiltin 21 | 22 | @external 23 | func test_wallet{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(): 24 | let (x) = wallet.read(0) 25 | assert x = 0 26 | 27 | wallet.write(0, 100) 28 | 29 | let (x) = wallet.read(0) 30 | assert x = 100 31 | 32 | return () 33 | end 34 | 35 | @external 36 | func test_height_map{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(): 37 | let (z) = height_map.read(0, 0) 38 | assert z = 0 39 | 40 | height_map.write(0, 0, 5) 41 | 42 | let (z) = height_map.read(0, 0) 43 | assert z = 5 44 | 45 | return () 46 | end 47 | 48 | @external 49 | func test_id{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(): 50 | let (mr_smith) = id.read(0) 51 | assert mr_smith = Id(0, 0, 0) 52 | 53 | id.write(0, Id(37, 185, 0)) 54 | 55 | let (mr_smith) = id.read(0) 56 | assert mr_smith = Id(37, 185, 0) 57 | 58 | return () 59 | end 60 | -------------------------------------------------------------------------------- /exercises/storage/storage03.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | 3 | # You can update stored values using externals, or just consult them (for free) using views 4 | 5 | # I AM NOT DONE 6 | 7 | @storage_var 8 | func bool() -> (bool : felt): 9 | end 10 | 11 | @external 12 | func toggle{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(): 13 | # TODO 14 | # Implement toggle external 15 | end 16 | 17 | @view 18 | func view_bool{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}() -> ( 19 | bool : felt 20 | ): 21 | # TODO 22 | # Implement view_bool 23 | end 24 | 25 | # TESTS # 26 | 27 | from starkware.cairo.common.cairo_builtins import HashBuiltin 28 | 29 | @external 30 | func test_toggle_and_view{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(): 31 | let (x) = view_bool() 32 | assert x = 0 33 | 34 | toggle() 35 | 36 | let (x) = view_bool() 37 | assert x = 1 38 | 39 | toggle() 40 | 41 | let (x) = view_bool() 42 | assert x = 0 43 | 44 | return () 45 | end 46 | -------------------------------------------------------------------------------- /exercises/strings/README.md: -------------------------------------------------------------------------------- 1 | ## Short strings 2 | 3 | Short strings are a way to encode an ASCII string as a felt. 4 | A short string can be defined by converting an ASCII string to its hexadecimal encoding. 5 | The hexadecimal value can also be converted to decimal. 6 | Short string are thus limited to 31 characters to fit into a felt. 7 | The following code show equivalent ways of defining the same short string. 8 | ``` 9 | let s = 'Hello' 10 | let s = 0x48656c6c6f 11 | let s = 310939249775 12 | ``` 13 | 14 | Read more about short strings here https://www.cairo-lang.org/docs/how_cairo_works/consts.html#short-string-literals. 15 | 16 | Strings of arbitrary size can be constructed from short strings, an example implementation can be found here https://github.com/topology-gg/caistring -------------------------------------------------------------------------------- /exercises/strings/strings00.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | 3 | # Cairo supports short strings which are encoded as ASCII under the hood 4 | # The felt is the decimal representation of the string in hexadecimal ASCII 5 | # e.g. let hello_string = 'Hello' 6 | # let hello_felt = 310939249775 7 | # let hello_hex = 0x48656c6c6f 8 | # https://www.cairo-lang.org/docs/how_cairo_works/consts.html#short-string-literals 9 | 10 | # I AM NOT DONE 11 | 12 | # TODO: Fix the say_hello function by returning the appropriate short strings 13 | 14 | func say_hello() -> (hello_string : felt, hello_felt : felt, hello_hex : felt): 15 | # FILL ME 16 | return (hello_string, hello_felt, hello_hex) 17 | end 18 | 19 | # Do not change the test 20 | @external 21 | func test_say_hello{syscall_ptr : felt*}(): 22 | let (user_string, user_felt, user_hex) = say_hello() 23 | assert user_string = 'Hello Starklings' 24 | assert user_felt = 12011164701440182822452181791570417168947 25 | assert user_hex = 0x627569646c20627569646c20627569646c 26 | return () 27 | end 28 | -------------------------------------------------------------------------------- /exercises/strings/strings01.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | 3 | # Short strings really are felts in disguise, and support the same basic operations 4 | 5 | # I AM NOT DONE 6 | 7 | # TODO: Find the key to decode the string that passes the test 8 | 9 | func decode_cipher1() -> (plaintext : felt): 10 | let ciphertext = 'Another One Bites The Dust' 11 | let key = 0 12 | let plaintext = ciphertext + key 13 | return (plaintext) 14 | end 15 | 16 | # TODO: Find the correct ciphertext that passes the test 17 | 18 | func decode_cipher2() -> (plaintext : felt): 19 | let ciphertext = 0 20 | let plaintext = 1337 * ciphertext + 0xc0de 21 | return (plaintext) 22 | end 23 | 24 | # Do not change the test 25 | @external 26 | func test_decode_string{syscall_ptr : felt*}(): 27 | # The correct key should produce the corresponding plaintext 28 | let (decoded_string) = decode_cipher1() 29 | assert decoded_string = 'Twinkle Twinkle Little Star' 30 | 31 | # The correct ciphertext should produce corresponding plaintext 32 | let (decoded_string) = decode_cipher2() 33 | assert decoded_string = 'Magic Starknet Money' 34 | 35 | return () 36 | end 37 | -------------------------------------------------------------------------------- /exercises/syntax/README.md: -------------------------------------------------------------------------------- 1 | # Starknet syntax 2 | 3 | Starknet uses the Cairo language to write smart contracts code. 4 | 5 | You can learn more about the very basic syntax here: [https://starknet.io/docs/hello_starknet/intro.html](https://starknet.io/docs/hello_starknet/intro.html). 6 | -------------------------------------------------------------------------------- /exercises/syntax/syntax01.cairo: -------------------------------------------------------------------------------- 1 | # All Starknet files must start with a specific line indicating the file is a smart contract, 2 | # not just a regular Cairo file 3 | 4 | # I AM NOT DONE 5 | 6 | # TODO: add the Starknet file specifier at the beginning of the file 7 | 8 | # You can ignore what follows for now 9 | @external 10 | func test_ok(): 11 | return () 12 | end 13 | -------------------------------------------------------------------------------- /exercises/syntax/syntax02.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | 3 | # Starknet provides a module management system. 4 | # It is very similar to the Python's one. 5 | 6 | # I AM NOT DONE 7 | 8 | # TODO: add the module imports needed to make the test pass! 9 | 10 | # You can ignore what follows for now 11 | @external 12 | func test_ok{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(): 13 | return () 14 | end 15 | -------------------------------------------------------------------------------- /exercises/syntax/syntax03.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | 3 | # Functions can take arguments and return results 4 | 5 | # I AM NOT DONE 6 | 7 | # TODO: make the test pass! 8 | 9 | func takes_two_arguments_and_returns_one() -> (): 10 | return (a + b) # Do not change 11 | end 12 | 13 | # You could be tempted to change the test to make it pass, but don't? 14 | @external 15 | func test_sum{syscall_ptr : felt*}(): 16 | let (sum) = takes_two_arguments_and_returns_one(1, 2) 17 | assert sum = 3 18 | return () 19 | end 20 | -------------------------------------------------------------------------------- /exercises/syntax/syntax04.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | 3 | # Function and other definitions can be scoped in namespaces, making the code more readable. 4 | 5 | # I AM NOT DONE 6 | 7 | # TODO: make the test pass! 8 | 9 | # Do not change anything but the test 10 | namespace my_namespace: 11 | func returns_something() -> (result : felt): 12 | return (42) 13 | end 14 | end 15 | 16 | # Change the following test to make it pass 17 | @external 18 | func test_hello{syscall_ptr : felt*}(): 19 | let (result) = returns_something() 20 | assert result = 42 21 | return () 22 | end 23 | -------------------------------------------------------------------------------- /exercises/syntax/syntax05.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | 3 | # As with many other languages, you can describe object structures with the struct keyword. 4 | 5 | # I AM NOT DONE 6 | 7 | # TODO: declare the Currency struct to make the test pass 8 | 9 | # Do not change the test 10 | @external 11 | func test_currency_sum{syscall_ptr : felt*}(): 12 | alloc_locals 13 | local euro : Currency = Currency('Euro', 2) 14 | assert euro.name = 'Euro' 15 | return () 16 | end 17 | -------------------------------------------------------------------------------- /exercises/tricks/assert_bool.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | 3 | # Boolean assertions, such as "x OR y" for boolean felts, can also be implemented without conditionals. 4 | 5 | # I AM NOT DONE 6 | 7 | # TODO Implement the following boolean asserts without "if" 8 | 9 | func assert_or(x, y): 10 | # FILL ME 11 | return () 12 | end 13 | 14 | func assert_and(x, y): 15 | # FILL ME 16 | return () 17 | end 18 | 19 | func assert_nor(x, y): 20 | # FILL ME 21 | return () 22 | end 23 | 24 | func assert_xor(x, y): 25 | # FILL ME 26 | return () 27 | end 28 | 29 | # Do not modify the tests 30 | @external 31 | func test_assert_or(): 32 | assert_or(0, 1) 33 | assert_or(1, 0) 34 | assert_or(1, 1) 35 | return () 36 | end 37 | 38 | @external 39 | func test_assert_or_ko(): 40 | %{ expect_revert() %} 41 | assert_or(0, 0) 42 | return () 43 | end 44 | 45 | @external 46 | func test_assert_and(): 47 | assert_and(1, 1) 48 | return () 49 | end 50 | 51 | @external 52 | func test_assert_and_ko1(): 53 | %{ expect_revert() %} 54 | assert_and(0, 0) 55 | return () 56 | end 57 | 58 | @external 59 | func test_assert_and_ko2(): 60 | %{ expect_revert() %} 61 | assert_and(0, 1) 62 | return () 63 | end 64 | 65 | @external 66 | func test_assert_and_ko3(): 67 | %{ expect_revert() %} 68 | assert_and(1, 0) 69 | return () 70 | end 71 | 72 | @external 73 | func test_assert_nor(): 74 | assert_nor(0, 0) 75 | return () 76 | end 77 | 78 | @external 79 | func test_assert_nor_ko1(): 80 | %{ expect_revert() %} 81 | assert_nor(0, 1) 82 | return () 83 | end 84 | 85 | @external 86 | func test_assert_nor_ko2(): 87 | %{ expect_revert() %} 88 | assert_nor(1, 0) 89 | return () 90 | end 91 | 92 | @external 93 | func test_assert_nor_ko3(): 94 | %{ expect_revert() %} 95 | assert_nor(1, 1) 96 | return () 97 | end 98 | 99 | @external 100 | func test_assert_xor(): 101 | assert_xor(0, 1) 102 | assert_xor(1, 0) 103 | return () 104 | end 105 | 106 | @external 107 | func test_assert_xor_ko(): 108 | %{ expect_revert() %} 109 | assert_xor(0, 0) 110 | return () 111 | end 112 | 113 | @external 114 | func test_assert_xor_ko2(): 115 | %{ expect_revert() %} 116 | assert_xor(1, 1) 117 | return () 118 | end 119 | -------------------------------------------------------------------------------- /exercises/tricks/inline_if.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | 3 | from starkware.cairo.common.math_cmp import is_not_zero 4 | from starkware.cairo.common.bool import TRUE, FALSE 5 | 6 | # Sometimes, conditionals can be avoided by using a polynomial that maps valid inputs to 0 7 | # Use this trick to rewrite functions without "if" 8 | 9 | # I AM NOT DONE 10 | 11 | # TODO: Implement a ternary operator `if cond then return val_true else return val_false` 12 | # Make sure the condition is a boolean 13 | 14 | func if_then_else(cond : felt, val_true : felt, val_false) -> (res : felt): 15 | # FILL ME 16 | return (res) 17 | end 18 | 19 | @external 20 | func test_ternary_conditional_operator(): 21 | let (res) = if_then_else(FALSE, 911, 420) 22 | assert 420 = res 23 | let (res) = if_then_else(TRUE, 911, 'over 9000') 24 | assert 911 = res 25 | let (res) = if_then_else(FALSE, 69420, 1559) 26 | assert 1559 = res 27 | let (res) = if_then_else(TRUE, 'nice', 69) 28 | assert 'nice' = res 29 | %{ expect_revert() %} 30 | let (res) = if_then_else(69, 'nope', 911) 31 | return () 32 | end 33 | -------------------------------------------------------------------------------- /exercises/tricks/no_conditionals.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | 3 | from starkware.cairo.common.math_cmp import is_not_zero 4 | 5 | # Sometimes, conditionals can be avoided by using an expression that 6 | # - maps valid inputs to 1, 7 | # - and/or maps invalid inputs to 0. 8 | # For instance (row - 2) * (row - 4) is 0 only when row is 2 or 4. 9 | 10 | # Use this trick to rewrite functions without "if" conditions 11 | # Note: This helps to avoid dealing with revoked references. 12 | 13 | # I AM NOT DONE 14 | 15 | func is_binary_if(x : felt) -> (res : felt): 16 | if x == 0: 17 | return (1) 18 | end 19 | if x == 1: 20 | return (1) 21 | end 22 | return (0) 23 | end 24 | 25 | # TODO: Return the right value to mimick the behavior of is_binary_if 26 | 27 | func is_binary_no_if(x : felt) -> (res : felt): 28 | # FILL ME 29 | return (res) 30 | end 31 | 32 | # TODO: Fix the function so that 33 | # - it returns the string 'cool' if x is 1337, 69420, 42 34 | # - it returns 'meh' on any other input 35 | 36 | func is_cool(x : felt) -> (res : felt): 37 | # FILL ME 38 | return (res) 39 | end 40 | 41 | # Do not change the test 42 | @external 43 | func test_is_binary{syscall_ptr : felt*}(): 44 | let (eval_if) = is_binary_if(0) 45 | let (eval_no_if) = is_binary_no_if(0) 46 | assert (eval_if, eval_no_if) = (1, 1) 47 | 48 | let (eval_if) = is_binary_if(1) 49 | let (eval_no_if) = is_binary_no_if(1) 50 | assert (eval_if, eval_no_if) = (1, 1) 51 | 52 | let (eval_if) = is_binary_if(13) 53 | let (eval_no_if) = is_binary_no_if(37) 54 | assert (eval_if, eval_no_if) = (0, 0) 55 | return () 56 | end 57 | 58 | @external 59 | func test_is_cool{syscall_ptr : felt*}(): 60 | let (is_1337_cool) = is_cool(1337) 61 | let (is_69420_cool) = is_cool(69420) 62 | let (is_42_cool) = is_cool(42) 63 | let (is_0_cool) = is_cool(0) 64 | let (is_911_cool) = is_cool(911) 65 | let results = ('cool', 'cool', 'cool', 'meh', 'meh') 66 | assert (is_1337_cool, is_69420_cool, is_42_cool, is_0_cool, is_911_cool) = results 67 | return () 68 | end 69 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | echo Installing starklings 5 | 6 | STARKLINGS_DIR=${STARKLINGS_DIR-"$HOME/.starklings"} 7 | mkdir -p "$STARKLINGS_DIR" 8 | 9 | 10 | PLATFORM="$(uname -s)" 11 | case $PLATFORM in 12 | Linux) 13 | PLATFORM="Linux" 14 | ;; 15 | Darwin) 16 | PLATFORM="macOS" 17 | ;; 18 | *) 19 | echo "unsupported platform: $PLATFORM" 20 | exit 1 21 | ;; 22 | esac 23 | 24 | STARKLINGS_REPO="https://github.com/onlydustxyz/starklings" 25 | 26 | echo Retrieving the latest version from $STARKLINGS_REPO... 27 | 28 | LATEST_RELEASE=$(curl -L -s -H 'Accept: application/json' "${STARKLINGS_REPO}/releases/latest") 29 | LATEST_VERSION=$(echo $LATEST_RELEASE | sed -e 's/.*"tag_name":"\([^"]*\)".*/\1/') 30 | 31 | echo Using version $LATEST_VERSION 32 | 33 | LATEST_RELEASE_URL="${STARKLINGS_REPO}/releases/download/${LATEST_VERSION}" 34 | STARKLINGS_TARBALL_NAME="starklings-${PLATFORM}.tar.gz" 35 | TARBALL_DOWNLOAD_URL="${LATEST_RELEASE_URL}/${STARKLINGS_TARBALL_NAME}" 36 | 37 | echo "Downloading starklings from ${TARBALL_DOWNLOAD_URL}" 38 | curl -L $TARBALL_DOWNLOAD_URL | tar -xvzC $STARKLINGS_DIR 39 | 40 | STARKLINGS_BINARY_DIR="${STARKLINGS_DIR}/dist/starklings" 41 | STARKLINGS_BINARY="${STARKLINGS_BINARY_DIR}/starklings" 42 | chmod +x $STARKLINGS_BINARY 43 | 44 | case $SHELL in 45 | */zsh) 46 | PROFILE=$HOME/.zshrc 47 | PREF_SHELL=zsh 48 | ;; 49 | */bash) 50 | PROFILE=$HOME/.bashrc 51 | PREF_SHELL=bash 52 | ;; 53 | */fish) 54 | PROFILE=$HOME/.config/fish/config.fish 55 | PREF_SHELL=fish 56 | ;; 57 | *) 58 | echo "error: could not detect shell, manually add ${STARKLINGS_BINARY_DIR} to your PATH." 59 | exit 1 60 | esac 61 | 62 | if [[ ":$PATH:" != *":${STARKLINGS_BINARY_DIR}:"* ]]; then 63 | echo >> $PROFILE && echo "export PATH=\"\$PATH:$STARKLINGS_BINARY_DIR\"" >> $PROFILE 64 | fi 65 | 66 | echo && echo "Detected your preferred shell is ${PREF_SHELL} and added starklings to PATH. Run 'source ${PROFILE}' or start a new terminal session to use starklings." 67 | echo "Then, simply run 'starklings --help' " 68 | -------------------------------------------------------------------------------- /lib/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onlydustxyz/starklings/ed480cce4cd74c518ffeaa40f63b71892c6e6a0a/lib/.gitkeep -------------------------------------------------------------------------------- /protostar.toml: -------------------------------------------------------------------------------- 1 | ["protostar.config"] 2 | protostar_version = "0.1.0" 3 | 4 | ["protostar.project"] 5 | libs_path = "lib" 6 | 7 | ["protostar.contracts"] 8 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | authors = ["OnlyDust"] 3 | description = "An interactive tutorial to get you up and running with Starknet" 4 | name = "starklings" 5 | version = "1.4.0" 6 | 7 | [tool.poetry.dependencies] 8 | argparse = "^1.4.0" 9 | black = "^22.3.0" 10 | cairo-lang = {url = "https://files.pythonhosted.org/packages/c0/f6/c850604895a2ce5ff3ef77cdb470b6b0ef50889645748a748e18a1c2269e/cairo-lang-0.8.1.post1.zip#sha256=b3c1a23078ba4e0c8ec45d2cd2ba4873ad70b6723dfba70f70c978c9723ff6eb"} 11 | colorama = "^0.4.4" 12 | openzeppelin-cairo-contracts = "^0.1.0" 13 | pyinstaller = "ˆ5.0.1" 14 | pylint = "^2.13.8" 15 | pytest = "^7.1.2" 16 | pytest-mock = "^3.7.0" 17 | python = ">=3.8 <3.9" 18 | sentry-sdk = "^1.5.12" 19 | starklings-protostar = "^0.2.1" 20 | watchdog = "^2.1.7" 21 | rich = "^12.4.4" 22 | requests = "^2.28.0" 23 | pickleDB = "^0.9.2" 24 | 25 | [tool.poetry.dev-dependencies] 26 | GitPython = "^3.1.27" 27 | packaging = "^21.3" 28 | poethepoet = "ˆ0.13.1" 29 | tomli = "<2.0.0" 30 | responses = "^0.21.0" 31 | 32 | [tool.poe.tasks] 33 | build = ["generate_solutions", "package"] 34 | deploy = "python deploy.py" 35 | generate_solutions = {"script" = "src.solutions.factory:init"} 36 | package = "pyinstaller starklings.spec --noconfirm" 37 | test = "pytest src" 38 | 39 | [build-system] 40 | build-backend = "poetry.core.masonry.api" 41 | requires = ["poetry-core>=1.0.0"] 42 | 43 | [[tool.poetry.source]] 44 | name = "test-pypi" 45 | secondary = true 46 | url = "https://test.pypi.org/project/" 47 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | asyncio_mode=auto 3 | -------------------------------------------------------------------------------- /resources/assets/warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onlydustxyz/starklings/ed480cce4cd74c518ffeaa40f63b71892c6e6a0a/resources/assets/warning.png -------------------------------------------------------------------------------- /scripts/add_new_exercise.sh: -------------------------------------------------------------------------------- 1 | #! /bin/env bash 2 | # Add a new exercise 3 | 4 | # set -eux 5 | 6 | ROOT_DIR=$PWD 7 | EXERCISES="$PWD/exercises" 8 | EXERCISE_PATCHES="$PWD/.patches" 9 | COURSE="$PWD/src/exercises/__init__.py" 10 | 11 | if [[ $(basename "$ROOT_DIR") != "starklings" ]]; then echo "Execute $0 at the root of the starklings project!"; exit; fi 12 | if ([ -z "$1" ] || [ ! -f "$1" ]); then echo "Please provide a valid path to the exercise!"; exit; fi 13 | if ([ -z "$2" ] || [ ! -f "$2" ]); then echo "Please provide a valid path to the exercise solution!"; exit; fi 14 | if [ -z "$3" ]; then echo "Please provide a valid name for the exercise folder!"; exit; fi 15 | 16 | EXERCISE=$1 17 | EXERCISE_SOLUTION=$2 18 | EXERCISE_FOLDER=$3 19 | 20 | mkdir -p "$EXERCISES/$EXERCISE_FOLDER" 21 | mkdir -p "$EXERCISE_PATCHES/$EXERCISE_FOLDER" 22 | 23 | diff -Nb -U0 $EXERCISE $EXERCISE_SOLUTION > "$EXERCISE_PATCHES/$EXERCISE_FOLDER/$(basename $EXERCISE).patch" 24 | cp $EXERCISE "$EXERCISES/$EXERCISE_FOLDER/$(basename $EXERCISE)" 25 | 26 | echo "Done" 27 | echo "Update course in $COURSE" 28 | -------------------------------------------------------------------------------- /scripts/patch_and_test_exercises.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ROOT_DIR=$(pwd) 4 | TMP_DIR="/tmp/tmp_exercises" 5 | PATCH_DIR="$ROOT_DIR/.patches" 6 | 7 | mkdir -p $TMP_DIR 8 | 9 | cp -r "$ROOT_DIR/exercises/." "$TMP_DIR/" 10 | 11 | for section in `ls $PATCH_DIR` 12 | do 13 | for exo in `ls $PATCH_DIR/$section` 14 | do 15 | patch $TMP_DIR/$section/"${exo%.*}" < "$PATCH_DIR/$section/$exo" 16 | mv $TMP_DIR/$section/"${exo%.*}" $TMP_DIR/$section/"test_${exo%.*}" 17 | done 18 | done 19 | 20 | RESULT=$($HOME/.protostar/dist/protostar/protostar test $TMP_DIR 2>&1 | grep -iF "failed") 21 | 22 | rm -rf $TMP_DIR 23 | 24 | if [ -z "$RESULT" ] 25 | then 26 | echo "All exercises passed" 27 | exit 0 28 | else 29 | echo "Some exercises failed" 30 | exit 1 31 | fi 32 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- 1 | from src.cli import cli 2 | -------------------------------------------------------------------------------- /src/cli.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import sentry_sdk 3 | from rich.syntax import Syntax 4 | from src.exercises.model import Directory 5 | from src.runner import Runner, single_exercise_check 6 | from src.exercises import current_working_exercises, course 7 | from src.exercises.seeker import ExerciseSeeker 8 | from src.user.login import login 9 | from src.utils.version_manager import VersionManager 10 | from src.config import root_directory, current_working_directory, dev_mode 11 | from src.solutions.repository import get_solution 12 | from src.console import console 13 | 14 | version_manager = VersionManager() 15 | 16 | sentry_sdk.init( 17 | "https://73212d09152344fd8e351ef180b8fa75@o1254095.ingest.sentry.io/6421829", 18 | # Set traces_sample_rate to 1.0 to capture 100% 19 | # of transactions for performance monitoring. 20 | # We recommend adjusting this value in production. 21 | traces_sample_rate=1.0, 22 | environment="development" if dev_mode else "production", 23 | release=str(version_manager.starklings_version), 24 | ) 25 | 26 | 27 | def capture_solution_request(solution_path: str): 28 | with sentry_sdk.push_scope() as scope: 29 | scope.set_tag("requested_solution", str(solution_path)) 30 | sentry_sdk.capture_message("Solution requested", level="info") 31 | 32 | 33 | def capture_single_exercise_check(exercise_path: Path): 34 | exercise_relative_path = exercise_path.relative_to(current_working_directory) 35 | with sentry_sdk.push_scope() as scope: 36 | scope.set_tag("exercise_to_check", str(exercise_relative_path)) 37 | sentry_sdk.capture_message("Single exercise check", level="info") 38 | 39 | 40 | async def cli(args): 41 | exercise_seeker = ExerciseSeeker(current_working_exercises) 42 | runner = Runner(exercise_seeker) 43 | 44 | if args.version: 45 | version_manager.print_current_version() 46 | return 47 | 48 | if args.display_course: 49 | print(Directory("exercises", course), end="") 50 | 51 | if args.watch: 52 | with console.screen(): 53 | sentry_sdk.capture_message("Starting the watch mode") 54 | runner.watch() 55 | return 56 | 57 | if args.verify: 58 | exercise_path = args.verify 59 | capture_single_exercise_check(exercise_path) 60 | await single_exercise_check(exercise_path) 61 | return 62 | 63 | if args.solution: 64 | capture_solution_request(args.solution) 65 | exercise_path = root_directory / args.solution 66 | try: 67 | solution = get_solution(exercise_path) 68 | 69 | syntax = Syntax( 70 | solution, "python", line_numbers=True, background_color="default" 71 | ) 72 | console.print(syntax) 73 | except FileNotFoundError: 74 | print("Solution not found") 75 | return 76 | 77 | if args.login: 78 | with console.screen(): 79 | login() 80 | return 81 | -------------------------------------------------------------------------------- /src/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | 4 | EXERCISES_DIRECTORY_NAME = "exercises" 5 | PATCHES_DIRECTORY_NAME = ".patches" 6 | SOLUTIONS_DIRECTORY_NAME = ".solutions" 7 | 8 | GITHUB_CLIENT_ID = "e5335dd9a6d4456d5a61" 9 | GITHUB_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code" 10 | 11 | root_directory = (Path(__file__).parents[1]).resolve() 12 | current_working_directory = Path(os.getcwd()).resolve() 13 | exercises_directory = root_directory / EXERCISES_DIRECTORY_NAME 14 | current_working_exercises_directory = ( 15 | current_working_directory / EXERCISES_DIRECTORY_NAME 16 | ) 17 | patches_directory = root_directory / PATCHES_DIRECTORY_NAME 18 | solutions_directory = root_directory / SOLUTIONS_DIRECTORY_NAME 19 | 20 | dev_mode = root_directory == current_working_directory 21 | -------------------------------------------------------------------------------- /src/console.py: -------------------------------------------------------------------------------- 1 | from rich.console import Console 2 | 3 | console = Console() 4 | -------------------------------------------------------------------------------- /src/database.py: -------------------------------------------------------------------------------- 1 | import pickledb 2 | from src.config import root_directory 3 | 4 | database = pickledb.load(str(root_directory / "starklings.db"), True) 5 | -------------------------------------------------------------------------------- /src/exercises/__init__.py: -------------------------------------------------------------------------------- 1 | from src.config import exercises_directory, current_working_exercises_directory 2 | 3 | from .model import Directory, Exercise 4 | 5 | course = [ 6 | Directory( 7 | "syntax", 8 | [ 9 | Exercise("syntax01"), 10 | Exercise("syntax02"), 11 | Exercise("syntax03"), 12 | Exercise("syntax04"), 13 | Exercise("syntax05"), 14 | ], 15 | ), 16 | Directory( 17 | "operations", 18 | [ 19 | Exercise("operations00"), 20 | Exercise("operations01"), 21 | Exercise("operations02"), 22 | Exercise("operations03"), 23 | ], 24 | ), 25 | Directory( 26 | "strings", 27 | [ 28 | Exercise("strings00"), 29 | Exercise("strings01"), 30 | ], 31 | ), 32 | Directory( 33 | "storage", 34 | [ 35 | Exercise("storage01"), 36 | Exercise("storage02"), 37 | Exercise("storage03"), 38 | ], 39 | ), 40 | Directory( 41 | "implicit_arguments", 42 | [ 43 | Exercise("implicit_arguments01"), 44 | Exercise("implicit_arguments02"), 45 | Exercise("implicit_arguments03"), 46 | ], 47 | ), 48 | Directory( 49 | "recursions", 50 | [ 51 | Exercise("recursion01"), 52 | Exercise("array01"), 53 | Exercise("array02"), 54 | Exercise("array03"), 55 | Exercise("array04"), 56 | Exercise("struct01"), 57 | Exercise("collatz_sequence"), 58 | ], 59 | ), 60 | Directory( 61 | "registers", 62 | [ 63 | Exercise("registers00"), 64 | Exercise("registers01"), 65 | Exercise("registers02"), 66 | Exercise("registers03"), 67 | Exercise("registers04"), 68 | ], 69 | ), 70 | Directory( 71 | "revoked_references", 72 | [ 73 | Exercise("revoked_references01"), 74 | ], 75 | ), 76 | Directory( 77 | "builtins", 78 | [ 79 | Exercise("bitwise"), 80 | ], 81 | ), 82 | Directory( 83 | "tricks", 84 | [ 85 | Exercise("no_conditionals"), 86 | Exercise("assert_bool"), 87 | Exercise("inline_if"), 88 | ], 89 | ), 90 | Directory( 91 | "hints", 92 | [ 93 | Exercise("hints00"), 94 | Exercise("hints01"), 95 | ], 96 | ), 97 | ] 98 | 99 | exercises = Directory(exercises_directory, course).list_exercises() 100 | 101 | current_working_exercises = Directory( 102 | current_working_exercises_directory, course 103 | ).list_exercises() 104 | -------------------------------------------------------------------------------- /src/exercises/checker.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from queue import Queue 3 | from pathlib import Path 4 | from threading import Lock 5 | from starklings_protostar.commands.test.runner import TestRunner 6 | from starklings_protostar.commands.test.reporter import Reporter 7 | from starklings_protostar.commands.test.cases import PassedCase 8 | from starklings_protostar.commands.test.test_collector import TestCollector 9 | 10 | lock = Lock() 11 | 12 | 13 | class ExerciceFailed(Exception): 14 | def __init__(self, message, **kwargs): 15 | super().__init__(**kwargs) 16 | self._message = message 17 | 18 | @property 19 | def message(self): 20 | return self._message 21 | 22 | 23 | async def check_exercise(exercise_path): 24 | try: 25 | test_subjects = TestCollector( 26 | target=Path(exercise_path), 27 | ).collect() 28 | queue = Queue() 29 | reporter = Reporter(queue) 30 | runner = TestRunner(reporter=reporter, include_paths=[]) 31 | await asyncio.gather( 32 | *[runner.run_test_subject(subject) for subject in test_subjects] 33 | ) 34 | except Exception as error: 35 | raise ExerciceFailed(str(error)) from error 36 | for test_case_result in reporter.test_case_results: 37 | if not isinstance(test_case_result, PassedCase): 38 | raise ExerciceFailed(str(test_case_result)) 39 | -------------------------------------------------------------------------------- /src/exercises/checker_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from src.exercises.checker import ExerciceFailed, check_exercise 3 | 4 | 5 | async def test_protostar_test_checker_success(): 6 | await check_exercise("tests/test.cairo") 7 | 8 | 9 | async def test_protostar_test_checker_missing_exercise(): 10 | with pytest.raises(ExerciceFailed): 11 | await check_exercise("tests/test_missing.cairo") 12 | 13 | 14 | async def test_protostar_test_checker_failing_exercise(): 15 | with pytest.raises(ExerciceFailed): 16 | await check_exercise("tests/test_failure.cairo") 17 | 18 | 19 | async def test_protostar_test_checker_invalid_exercise(): 20 | with pytest.raises(ExerciceFailed): 21 | await check_exercise("tests/test_invalid.cairo") 22 | 23 | 24 | async def test_protostar_test_checker_missing_syntax(): 25 | with pytest.raises(ExerciceFailed): 26 | await check_exercise("tests/test_missing.cairo") 27 | -------------------------------------------------------------------------------- /src/exercises/list_test.py: -------------------------------------------------------------------------------- 1 | from src.exercises import exercises 2 | 3 | 4 | def test_exercises_exist(): 5 | for exercise in exercises: 6 | assert exercise.exists() 7 | -------------------------------------------------------------------------------- /src/exercises/model.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | from pathlib import Path 3 | from typing import List 4 | 5 | 6 | class Exercise(namedtuple("Exercise", ["name"])): 7 | pass 8 | 9 | 10 | class Directory(namedtuple("Directory", ["name", "children"])): 11 | def __new__(cls, name, children): 12 | assert all( 13 | isinstance(x, (Directory, Exercise)) for x in children 14 | ), "Directory children can only be of type Directory of Exercise" 15 | self = super(Directory, cls).__new__(cls, name, children) 16 | return self 17 | 18 | def list_exercises(self) -> List[Path]: 19 | return self.inner_list_exercises("") 20 | 21 | def inner_list_exercises(self, prefix) -> List[Path]: 22 | dir_prefix = f"{self.name}/" 23 | 24 | res = [] 25 | 26 | for child in self.children: 27 | if isinstance(child, Exercise): 28 | res.append(Path(prefix) / dir_prefix / f"{child.name}.cairo") 29 | elif isinstance(child, Directory): 30 | res += child.inner_list_exercises(dir_prefix) 31 | else: 32 | raise Exception( 33 | "directory children can only contain Exercises and Directory types" 34 | ) 35 | 36 | return res 37 | 38 | def __str__(self): 39 | return self.inner_str("") 40 | 41 | def inner_str(self, parent_prefix): 42 | res = f"{parent_prefix}{self.name}/\n" 43 | 44 | for child in self.children: 45 | if isinstance(child, Exercise): 46 | res += f" {parent_prefix}- {child.name}\n" 47 | elif isinstance(child, Directory): 48 | res += child.inner_str(parent_prefix + " ") 49 | 50 | return res 51 | -------------------------------------------------------------------------------- /src/exercises/model_test.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from .model import Directory, Exercise 3 | 4 | 5 | def test_display_course(): 6 | my_course = Directory( 7 | "exercises", 8 | [ 9 | Directory( 10 | "syntax", 11 | [ 12 | Exercise("syntax00"), 13 | Exercise("syntax01"), 14 | ], 15 | ), 16 | Directory( 17 | "strings", 18 | [ 19 | Exercise("string00"), 20 | Exercise("string01"), 21 | ], 22 | ), 23 | ], 24 | ) 25 | 26 | assert ( 27 | my_course.__str__() 28 | == """exercises/ 29 | syntax/ 30 | - syntax00 31 | - syntax01 32 | strings/ 33 | - string00 34 | - string01 35 | """ 36 | ) 37 | 38 | 39 | def test_list_exercise(): 40 | my_course = Directory( 41 | "exercises", 42 | [ 43 | Directory( 44 | "syntax", 45 | [ 46 | Exercise("syntax01"), 47 | Exercise("syntax02"), 48 | ], 49 | ), 50 | Directory( 51 | "string", 52 | [Exercise("strings00"), Exercise("strings01")], 53 | ), 54 | ], 55 | ) 56 | 57 | assert my_course.list_exercises() == [ 58 | Path("exercises/syntax/syntax01.cairo"), 59 | Path("exercises/syntax/syntax02.cairo"), 60 | Path("exercises/string/strings00.cairo"), 61 | Path("exercises/string/strings01.cairo"), 62 | ] 63 | -------------------------------------------------------------------------------- /src/exercises/seeker.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import List 3 | 4 | 5 | def _is_exercise_not_done(exercise: Path) -> bool: 6 | return "\n# I AM NOT DONE\n" in exercise.read_text() 7 | 8 | 9 | class ExerciseSeeker: 10 | def __init__(self, exercises: List[Path]): 11 | self._exercises = exercises 12 | 13 | def get_next_undone(self) -> Path: 14 | for exercise in self._exercises: 15 | if _is_exercise_not_done(exercise): 16 | return exercise 17 | return None 18 | -------------------------------------------------------------------------------- /src/exercises/seeker_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from src.exercises.seeker import ExerciseSeeker 3 | 4 | 5 | @pytest.fixture(name="finished_exercise") 6 | def finished_exercise_fixture(mocker): 7 | mock = mocker.patch("src.exercises.seeker.Path").return_value 8 | mock.read_text.return_value = "Yolo" 9 | return mock 10 | 11 | 12 | @pytest.fixture(name="unfinished_exercise") 13 | def unfinished_exercise_fixture(mocker): 14 | mock = mocker.patch("src.exercises.seeker.Path").return_value 15 | mock.read_text.return_value = "Yolo\n# I AM NOT DONE\nNext" 16 | return mock 17 | 18 | 19 | def test_next_undone_exercise(finished_exercise, unfinished_exercise): 20 | exercise_seeker = ExerciseSeeker([finished_exercise]) 21 | assert exercise_seeker.get_next_undone() is None 22 | 23 | exercise_seeker = ExerciseSeeker([finished_exercise, unfinished_exercise]) 24 | assert exercise_seeker.get_next_undone() is unfinished_exercise 25 | -------------------------------------------------------------------------------- /src/file_watcher/dummy_file.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | 3 | func sum_func{syscall_ptr : felt*, range_check_ptr}(a : felt, b : felt) -> (res : felt): 4 | return (a + b) 5 | end 6 | 7 | @external 8 | func test_sum{syscall_ptr : felt*, range_check_ptr}(): 9 | let (r) = sum_func(4, 3) 10 | assert r = 6 11 | return () 12 | end 13 | -------------------------------------------------------------------------------- /src/file_watcher/watcher.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from watchdog.observers import Observer 3 | from watchdog.events import FileSystemEventHandler 4 | from src.utils.debounce import debounce 5 | 6 | 7 | class Handler(FileSystemEventHandler): 8 | def __init__(self, callback): 9 | self._callback = debounce(0.1)(callback) 10 | 11 | def on_modified(self, event): 12 | self._callback(event) 13 | 14 | 15 | class FileWatcher: 16 | def __init__(self, root_dir: Path): 17 | self._root_dir = root_dir 18 | self._observer = Observer() 19 | 20 | def start(self, callback): 21 | event_handler = Handler(callback) 22 | self._observer.schedule(event_handler, self._root_dir, recursive=True) 23 | self._observer.start() 24 | 25 | def stop(self): 26 | self._observer.stop() 27 | self._observer.join() 28 | -------------------------------------------------------------------------------- /src/file_watcher/watcher_test.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from time import sleep 3 | from src.file_watcher.watcher import FileWatcher 4 | 5 | 6 | def test_file_watcher(mocker): 7 | current_directry = Path(__file__).parent 8 | dummy_file_path = current_directry / "dummy_file.cairo" 9 | watcher = FileWatcher(current_directry) 10 | stub = mocker.stub() 11 | watcher.start(stub) 12 | 13 | assert stub.call_count == 0 14 | content = None 15 | with open(dummy_file_path, "r+", encoding="utf-8") as dummy_file: 16 | content = dummy_file.read() 17 | dummy_file.write("dummy") 18 | 19 | sleep(0.2) 20 | 21 | try: 22 | assert stub.call_count == 1 23 | assert stub.call_args[0][0].src_path in str(dummy_file_path) 24 | finally: 25 | with open(dummy_file_path, "w", encoding="utf-8") as dummy_file: 26 | dummy_file.write(content) 27 | -------------------------------------------------------------------------------- /src/prompt.py: -------------------------------------------------------------------------------- 1 | from rich import print as rich_print 2 | from src.console import console 3 | 4 | 5 | def on_watch_start(exercise_path): 6 | rich_print("[bold]:robot::robot::robot: Watch mode started.[/bold]") 7 | rich_print(f"You can start to work on exercise {exercise_path}.\n") 8 | 9 | 10 | def on_single_exercise_success(exercise_path): 11 | console.clear() 12 | rich_print( 13 | f"[bold green]:partying_face::partying_face::partying_face: Exercise {exercise_path} completed![/bold green]" 14 | ) 15 | 16 | 17 | def on_watch_exercise_success(): 18 | rich_print("You can keep working on this exercise,") 19 | rich_print("or move on to the next one by removing the `I AM NOT DONE` comment.\n") 20 | 21 | 22 | def on_watch_no_next_exercise(): 23 | rich_print("Congratulations! You have completed all the exercises!") 24 | 25 | 26 | def on_exercise_failure(exercise_path, error_message): 27 | console.clear() 28 | rich_print( 29 | f"[red]:construction: Exercise {exercise_path} failed. Please try again.[/red]" 30 | ) 31 | rich_print(error_message) 32 | 33 | 34 | def on_exercise_check(exercise_path): 35 | console.clear() 36 | rich_print(f"[gray]:eyes: Checking exercise {exercise_path}...[/gray]") 37 | 38 | 39 | def on_file_not_found(): 40 | console.clear() 41 | rich_print( 42 | "[red]:face_with_monocle: Creepy crap it looks that you are not running this script from the root directory of the repository.[/red]" 43 | ) 44 | rich_print( 45 | "[red]Please make sure you are running the CLI from the cloned Starklings repository.[/red]" 46 | ) 47 | 48 | 49 | def on_user_verification(verification_uri: str, verification_code: str): 50 | console.clear() 51 | rich_print( 52 | f"Please visit {verification_uri} to login and enter the following code: [bold]{verification_code}[/bold]" 53 | ) 54 | 55 | 56 | def waiting_for_user_login(): 57 | return console.status("Waiting for user login...") 58 | -------------------------------------------------------------------------------- /src/repository/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onlydustxyz/starklings/ed480cce4cd74c518ffeaa40f63b71892c6e6a0a/src/repository/__init__.py -------------------------------------------------------------------------------- /src/repository/state_checker.py: -------------------------------------------------------------------------------- 1 | from logging import getLogger 2 | import os 3 | from packaging.version import Version 4 | from git import InvalidGitRepositoryError, Repo 5 | 6 | from src.utils.version_manager import VersionManager 7 | 8 | logger = getLogger() 9 | 10 | 11 | def versions_match(repo: Repo): 12 | # Check there is no breaking change between the binary and the repository 13 | repo_version = Version(repo.tags.pop().name[1:]) 14 | binary_version = VersionManager().starklings_version 15 | if repo_version.major > binary_version.major or repo_version < binary_version: 16 | logger.error( 17 | """You installed starklings in version %s, but the repository is cloned in version %s 18 | Please update starklings running `bash install.sh` and update the repository running `git pull origin stable`""", 19 | binary_version, 20 | repo_version, 21 | ) 22 | return False 23 | return True 24 | 25 | 26 | def correct_branch(repo: Repo): 27 | if repo.active_branch.name != "stable": 28 | logger.error( 29 | "You are not on the stable branch, please switch running `git checkout stable`" 30 | ) 31 | return False 32 | return True 33 | 34 | 35 | def check(): 36 | # Check cwd is a repository 37 | try: 38 | repo = Repo(os.getcwd()) 39 | except InvalidGitRepositoryError: 40 | logger.error( 41 | "You are not running starklings in a git repository, make sure you run it in the cloned starklings repository" 42 | ) 43 | return False 44 | 45 | return correct_branch(repo) and versions_match(repo) 46 | -------------------------------------------------------------------------------- /src/repository/state_checker_test.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import pytest 3 | from packaging.version import Version 4 | from src.repository.state_checker import check, correct_branch, versions_match 5 | 6 | 7 | @pytest.fixture(name="repo") 8 | def repo_fixture(mocker): 9 | return mocker.patch("src.repository.state_checker.Repo").return_value 10 | 11 | 12 | def test_is_a_repo(mocker): 13 | logger = mocker.patch("src.repository.state_checker.logger") 14 | mocked_os = mocker.patch("src.repository.state_checker.os") 15 | mocker.patch("src.repository.state_checker.versions_match").return_value = True 16 | mocker.patch("src.repository.state_checker.correct_branch").return_value = True 17 | 18 | # Running directory is not a git repository 19 | mocked_os.getcwd.return_value = str(Path(__file__).parents[3]) 20 | assert not check() 21 | logger.error.assert_called_once() 22 | 23 | # Running directory is a git repository 24 | mocked_os.getcwd.return_value = str(Path(__file__).parents[2]) 25 | logger.reset_mock() 26 | assert check() 27 | logger.error.assert_not_called() 28 | 29 | 30 | def test_versions_match(mocker, repo): 31 | logger = mocker.patch("src.repository.state_checker.logger") 32 | version_manager = mocker.patch( 33 | "src.repository.state_checker.VersionManager" 34 | ).return_value 35 | 36 | # Repo is behing binary 37 | version_manager.starklings_version = Version("2.0.0") 38 | repo.tags.pop.return_value.name = "v1.0.0" 39 | assert not versions_match(repo) 40 | logger.error.assert_called_once() 41 | 42 | # Repo is ahead of binary 43 | version_manager.starklings_version = Version("2.0.0") 44 | repo.tags.pop.return_value.name = "v3.0.0" 45 | logger.reset_mock() 46 | assert not versions_match(repo) 47 | logger.error.assert_called_once() 48 | 49 | # Repo is up to date 50 | version_manager.starklings_version = Version("2.0.0") 51 | repo.tags.pop.return_value.name = "v2.0.0" 52 | logger.reset_mock() 53 | assert versions_match(repo) 54 | logger.error.assert_not_called() 55 | 56 | # Repo is ahead of binary but not breaking 57 | version_manager.starklings_version = Version("2.0.0") 58 | repo.tags.pop.return_value.name = "v2.4.0" 59 | logger.reset_mock() 60 | assert versions_match(repo) 61 | logger.error.assert_not_called() 62 | 63 | 64 | def test_correct_branch(repo): 65 | repo.active_branch.name = "stable" 66 | assert correct_branch(repo) 67 | 68 | repo.active_branch.name = "master" 69 | assert not correct_branch(repo) 70 | -------------------------------------------------------------------------------- /src/runner.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import contextlib 3 | from pathlib import Path 4 | import sys 5 | from threading import Lock 6 | from time import sleep 7 | 8 | import sentry_sdk 9 | 10 | from src.exercises.checker import ExerciceFailed, check_exercise 11 | from src.file_watcher.watcher import FileWatcher 12 | from src import prompt 13 | from src.exercises.seeker import ExerciseSeeker 14 | from src.config import current_working_directory 15 | 16 | check_exercise_lock = Lock() 17 | 18 | 19 | async def single_exercise_check(exercise_path: Path, watch_mode=False): 20 | if check_exercise_lock.locked(): 21 | return 22 | with check_exercise_lock: 23 | prompt.on_exercise_check(exercise_path) 24 | try: 25 | await check_exercise(str(exercise_path)) 26 | capture_exercise_solved(exercise_path) 27 | prompt.on_single_exercise_success(exercise_path) 28 | if watch_mode: 29 | prompt.on_watch_exercise_success() 30 | except ExerciceFailed as error: 31 | prompt.on_exercise_failure(exercise_path, error.message) 32 | 33 | 34 | def capture_exercise_solved(exercise_path: str): 35 | with sentry_sdk.push_scope() as scope: 36 | scope.set_tag("exercise_solved", str(exercise_path)) 37 | sentry_sdk.capture_message("Exercise solved", level="info") 38 | 39 | 40 | class Runner: 41 | def __init__(self, exercise_seeker: ExerciseSeeker): 42 | self._file_watcher = FileWatcher(current_working_directory) 43 | self._exercise_seeker = exercise_seeker 44 | 45 | def on_file_changed(self, _): 46 | next_exercise_path = self._exercise_seeker.get_next_undone() 47 | if next_exercise_path is not None: 48 | asyncio.run(single_exercise_check(next_exercise_path, True)) 49 | else: 50 | prompt.on_watch_no_next_exercise() 51 | 52 | def watch(self): 53 | try: 54 | prompt.on_watch_start(self._exercise_seeker.get_next_undone()) 55 | with contextlib.suppress(KeyboardInterrupt): 56 | self._file_watcher.start(self.on_file_changed) 57 | while True: 58 | sleep(5) 59 | except FileNotFoundError: 60 | prompt.on_file_not_found() 61 | sys.exit(1) 62 | -------------------------------------------------------------------------------- /src/runner_test.py: -------------------------------------------------------------------------------- 1 | import io 2 | from unittest.mock import patch 3 | from pathlib import Path 4 | from src.runner import ExerciseSeeker, Runner, current_working_directory 5 | 6 | PATH = Path("tests/exercises/test_end_of_exercise_messages") 7 | 8 | 9 | def runner_on_file_changed(current_working_exercises): 10 | exercise_seeker = ExerciseSeeker(current_working_exercises) 11 | runner = Runner(exercise_seeker) 12 | runner.on_file_changed(current_working_directory) 13 | 14 | 15 | @patch("sys.stdout", new_callable=io.StringIO) 16 | def test_runner_on_file_changed_with_next_exercises(mock_stdout): 17 | current_working_exercises = [ 18 | PATH / "with_next_exercises" / "syntax01.cairo", 19 | PATH / "with_next_exercises" / "syntax02.cairo", 20 | ] 21 | runner_on_file_changed(current_working_exercises) 22 | assert ( 23 | mock_stdout.getvalue() 24 | != "Congratulations! You have completed all the exercises!\n" 25 | ) 26 | 27 | 28 | @patch("sys.stdout", new_callable=io.StringIO) 29 | def test_runner_on_file_changed_without_next_exercises(mock_stdout): 30 | current_working_exercises = [ 31 | PATH / "without_next_exercises" / "syntax01.cairo", 32 | PATH / "without_next_exercises" / "syntax02.cairo", 33 | ] 34 | runner_on_file_changed(current_working_exercises) 35 | assert ( 36 | mock_stdout.getvalue() 37 | == "Congratulations! You have completed all the exercises!\n" 38 | ) 39 | -------------------------------------------------------------------------------- /src/solutions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onlydustxyz/starklings/ed480cce4cd74c518ffeaa40f63b71892c6e6a0a/src/solutions/__init__.py -------------------------------------------------------------------------------- /src/solutions/factory.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | import shutil 4 | from typing import List 5 | from src.config import solutions_directory, exercises_directory, patches_directory 6 | from src.exercises import exercises 7 | 8 | 9 | def empty_directory(path: Path): 10 | if path.exists(): 11 | shutil.rmtree(path) 12 | path.mkdir() 13 | 14 | 15 | def create_solution(exercise_path: Path): 16 | exercise_relative_path = exercise_path.relative_to(exercises_directory) 17 | solution_path = ( 18 | solutions_directory / exercise_path.name 19 | ) # Flatten the exercises tree in the solutions directory 20 | patch_path = patches_directory / f"{exercise_relative_path}.patch" 21 | try: 22 | os.makedirs(solution_path.parent) 23 | except FileExistsError: 24 | pass 25 | solution_path.touch() 26 | os.system(f"patch {exercise_path} -o {solution_path} < {patch_path}") 27 | 28 | 29 | def init(exercise_list: List[Path] = None): 30 | exercise_list = exercise_list or exercises 31 | empty_directory(solutions_directory) 32 | for exercise in exercise_list: 33 | create_solution(exercise) 34 | -------------------------------------------------------------------------------- /src/solutions/factory_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from src.solutions.factory import init 3 | from src.config import exercises_directory 4 | from src.solutions.repository import get_solution 5 | 6 | 7 | @pytest.fixture(name="an_exercise") 8 | def an_exercise_fixture(): 9 | return exercises_directory / "syntax/syntax01.cairo" 10 | 11 | 12 | @pytest.fixture(autouse=True) 13 | def init_fixture(an_exercise): 14 | init([an_exercise]) 15 | 16 | 17 | def test_solution_getter(an_exercise): 18 | solution = get_solution(an_exercise) 19 | assert ( 20 | solution 21 | == """%lang starknet 22 | # All Starknet files must start with a specific line indicating the file is a smart contract, 23 | # not just a regular Cairo file 24 | 25 | # I AM NOT DONE 26 | 27 | # TODO: add the Starknet file specifier at the beginning of the file 28 | 29 | # You can ignore what follows for now 30 | @external 31 | func test_ok(): 32 | return () 33 | end 34 | """ 35 | ) 36 | -------------------------------------------------------------------------------- /src/solutions/repository.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from src.config import solutions_directory 3 | 4 | 5 | def get_solution(exercise_path: Path): 6 | # When packaged, the solutions are all flattened into the .solutions directory. 7 | solution_path = solutions_directory / exercise_path.name 8 | return solution_path.read_text() 9 | -------------------------------------------------------------------------------- /src/user/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onlydustxyz/starklings/ed480cce4cd74c518ffeaa40f63b71892c6e6a0a/src/user/__init__.py -------------------------------------------------------------------------------- /src/user/access_token.py: -------------------------------------------------------------------------------- 1 | from src.database import database 2 | 3 | ACCESS_TOKEN_KEY = "access_token" 4 | 5 | 6 | def get_access_token(): 7 | return database.get(ACCESS_TOKEN_KEY) 8 | 9 | 10 | def set_access_token(access_token: str): 11 | database.set(ACCESS_TOKEN_KEY, access_token) 12 | -------------------------------------------------------------------------------- /src/user/login.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | import requests 3 | from src.config import GITHUB_CLIENT_ID, GITHUB_GRANT_TYPE 4 | from src.prompt import on_user_verification, waiting_for_user_login 5 | from src.user.access_token import set_access_token 6 | 7 | 8 | def login(): 9 | user_verification_request = requests.post( 10 | "https://github.com/login/device/code", 11 | data={ 12 | "client_id": GITHUB_CLIENT_ID, 13 | }, 14 | headers={ 15 | "Accept": "application/json", 16 | }, 17 | ) 18 | user_verification_response = user_verification_request.json() 19 | 20 | on_user_verification( 21 | user_verification_response["verification_uri"], 22 | user_verification_response["user_code"], 23 | ) 24 | with waiting_for_user_login(): 25 | interval = user_verification_response["interval"] 26 | total_retries = user_verification_response["expires_in"] // interval 27 | device_code = user_verification_response["device_code"] 28 | retry_count = 0 29 | 30 | poll_access_token = request_access_token(device_code) 31 | while "error" in poll_access_token.json(): 32 | if retry_count == total_retries: 33 | raise Exception("Failed to get access token") 34 | retry_count += 1 35 | sleep(interval) 36 | poll_access_token = request_access_token(device_code) 37 | 38 | access_token = poll_access_token.json()["access_token"] 39 | set_access_token(access_token) 40 | 41 | 42 | def request_access_token(device_code: str): 43 | return requests.post( 44 | "https://github.com/login/oauth/access_token", 45 | data={ 46 | "client_id": GITHUB_CLIENT_ID, 47 | "grant_type": GITHUB_GRANT_TYPE, 48 | "device_code": device_code, 49 | }, 50 | headers={ 51 | "Accept": "application/json", 52 | }, 53 | ) 54 | -------------------------------------------------------------------------------- /src/user/login_test.py: -------------------------------------------------------------------------------- 1 | import responses 2 | from src.user.login import login 3 | 4 | 5 | @responses.activate 6 | def test_user_login(mocker): 7 | on_user_verification_mock = mocker.patch("src.user.login.on_user_verification") 8 | set_access_token_mock = mocker.patch("src.user.login.set_access_token") 9 | 10 | verification_uri = "https://github.com/login/device" 11 | user_code = "user_code" 12 | access_token = "access_token" 13 | device_code = "device_code" 14 | 15 | user_verification_response = responses.Response( 16 | method="POST", 17 | url="https://github.com/login/device/code", 18 | json={ 19 | "verification_uri": verification_uri, 20 | "user_code": user_code, 21 | "interval": 0.01, 22 | "expires_in": 899, 23 | "device_code": device_code, 24 | }, 25 | ) 26 | 27 | authorization_pending_response = responses.Response( 28 | method="POST", 29 | url="https://github.com/login/oauth/access_token", 30 | json={"error": "authorization_pending"}, 31 | ) 32 | 33 | access_token_response = responses.Response( 34 | method="POST", 35 | url="https://github.com/login/oauth/access_token", 36 | json={ 37 | "access_token": access_token, 38 | }, 39 | ) 40 | 41 | responses.add(user_verification_response) 42 | responses.add(authorization_pending_response) 43 | responses.add(authorization_pending_response) 44 | responses.add(authorization_pending_response) 45 | responses.add(access_token_response) 46 | 47 | login() 48 | 49 | on_user_verification_mock.assert_called_with(verification_uri, user_code) 50 | set_access_token_mock.assert_called_with(access_token) 51 | -------------------------------------------------------------------------------- /src/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onlydustxyz/starklings/ed480cce4cd74c518ffeaa40f63b71892c6e6a0a/src/utils/__init__.py -------------------------------------------------------------------------------- /src/utils/debounce.py: -------------------------------------------------------------------------------- 1 | # Taken from https://stackoverflow.com/questions/61476962/python-decorator-for-debouncing-including-function-arguments#:~:text=Debouncing%20means%20to%20supress%20the,if%20no%20new%20function%20calls 2 | 3 | import threading 4 | 5 | 6 | def debounce(wait_time): 7 | """ 8 | Decorator that will debounce a function so that it is called after wait_time seconds 9 | If it is called multiple times, will wait for the last call to be debounced and run only this one. 10 | """ 11 | 12 | def decorator(function): 13 | def debounced(*args, **kwargs): 14 | def call_function(): 15 | debounced.timer = None 16 | return function(*args, **kwargs) 17 | 18 | # if we already have a call to the function currently waiting to be executed, reset the timer 19 | if debounced.timer is not None: 20 | debounced.timer.cancel() 21 | 22 | # after wait_time, call the function provided to the decorator with its arguments 23 | debounced.timer = threading.Timer(wait_time, call_function) 24 | debounced.timer.start() 25 | 26 | debounced.timer = None 27 | return debounced 28 | 29 | return decorator 30 | -------------------------------------------------------------------------------- /src/utils/debounce_test.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | from .debounce import debounce 3 | 4 | 5 | def test_debounce(mocker): 6 | stub = mocker.stub() 7 | 8 | @debounce(0.1) 9 | def debounced_function(): 10 | stub() 11 | 12 | debounced_function() 13 | debounced_function() 14 | 15 | sleep(0.2) 16 | 17 | assert stub.call_count == 1 18 | -------------------------------------------------------------------------------- /src/utils/version_manager.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import re 3 | from logging import getLogger 4 | from typing import Optional 5 | import tomli 6 | from packaging import version 7 | from packaging.version import Version as PackagingVersion 8 | from src.config import root_directory 9 | 10 | 11 | class VersionManager: 12 | @staticmethod 13 | def parse(version_str: str) -> PackagingVersion: 14 | return version.parse(version_str) 15 | 16 | @property 17 | def _pyproject_toml_path(self) -> Path: 18 | # When running from the built binary, the pyproject.toml file is under an "info" directory 19 | info_directory = root_directory / "info" 20 | if info_directory.exists(): 21 | return info_directory / "pyproject.toml" 22 | return root_directory / "pyproject.toml" 23 | 24 | @property 25 | def starklings_version(self) -> Optional[PackagingVersion]: 26 | try: 27 | with open(self._pyproject_toml_path, "r", encoding="UTF-8") as file: 28 | version_s = tomli.loads(file.read())["tool"]["poetry"]["version"] 29 | return VersionManager.parse(version_s) 30 | except FileNotFoundError: 31 | getLogger().warning("Couldn't read Starklings version") 32 | return None 33 | 34 | @property 35 | def cairo_version(self) -> Optional[PackagingVersion]: 36 | try: 37 | with open(self._pyproject_toml_path, "r", encoding="UTF-8") as file: 38 | raw_version = tomli.loads(file.read())["tool"]["poetry"][ 39 | "dependencies" 40 | ]["cairo-lang"]["url"] 41 | version_regex = re.compile(r".*cairo-lang-(.+)\.zip.*") 42 | version_match = version_regex.match(raw_version) 43 | return VersionManager.parse(version_match[1]) 44 | except FileNotFoundError: 45 | getLogger().warning("Couldn't read cairo-lang version") 46 | return None 47 | 48 | def print_current_version(self) -> None: 49 | print(f"Starklings version: {self.starklings_version or 'unknown'}") 50 | print(f"Cairo-lang version: {self.cairo_version or 'unknown'}") 51 | -------------------------------------------------------------------------------- /src/utils/version_manager_test.py: -------------------------------------------------------------------------------- 1 | from src.utils.version_manager import VersionManager 2 | 3 | 4 | def test_version_manager(): 5 | version_manager = VersionManager() 6 | 7 | assert version_manager.starklings_version > VersionManager.parse("0.0.0") 8 | assert version_manager.cairo_version > VersionManager.parse("0.0.0") 9 | -------------------------------------------------------------------------------- /starklings-backend/.env.DEFAULT: -------------------------------------------------------------------------------- 1 | SECRET_KEY=<> 2 | DATABASE_HOST=<> 3 | DATABASE_NAME=<> 4 | DATABASE_USER=<> 5 | DATABASE_PWD=<> -------------------------------------------------------------------------------- /starklings-backend/README.md: -------------------------------------------------------------------------------- 1 | # Starklings API 2 | 3 | Starklings backend within a single Flask API 4 | 5 | ## Installation Setup 6 | 7 | - Tested with Python >=3.8 8 | 9 | #### 1. Starlings Installation 10 | 11 | Make sure you installed starlings at first : 12 | ``` 13 | cd ../ 14 | pip install poetry 15 | poetry install 16 | 17 | # or on MAC M1 : 18 | CFLAGS=-I`brew --prefix gmp`/include LDFLAGS=-L`brew --prefix gmp`/lib poetry install 19 | ``` 20 | 21 | #### 2. Flask Installation 22 | 23 | ``` 24 | python -m venv starklings-venv 25 | source starklings-venv/bin/activate 26 | pip install --upgrade pip 27 | pip install -r requirements.txt 28 | ``` 29 | 30 | ## Launch API 31 | 32 | - Development 33 | ``` 34 | APP_SETTINGS=config.DevConfig python app.py 35 | ``` 36 | 37 | - Production 38 | ``` 39 | APP_SETTINGS=config.ProdConfig python app.py 40 | ``` 41 | 42 | 43 | ## API Documentation 44 | 45 | 46 | - `/exercise` 47 | Exercise Validation route 48 | 49 | 1. Headers 50 | 51 | | Key | Value | 52 | | :--------------- |:---------------:| 53 | | Content-Type | application/json | 54 | | Accept | application/json | 55 | 56 | 2. Data (JSON) 57 | 58 | | Key | Value | 59 | | :--------------- |:---------------:| 60 | | wallet_address | String: Wallet that wants to verify exercise | 61 | | exercise | String: Concatenated path of the wanted exercise (e.g storage/storage01) | 62 | | exercise_data | String: Cairo file as a string | 63 | 64 | 3. Return 65 | 66 | On Validation 67 | ``` 68 | { 69 | "result": "Exercice Succeed" 70 | } 71 | ``` 72 | 73 | On Error 74 | ``` 75 | { 76 | "error": "Cairo Error Msg" 77 | "result": "Exercice Failed" 78 | } 79 | ``` -------------------------------------------------------------------------------- /starklings-backend/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_cors import CORS 3 | import os 4 | import sys 5 | sys.path.append('../src/exercises') 6 | from starklings_backend.routes import app_routes 7 | from starklings_backend.models.shared import db 8 | 9 | 10 | app = Flask(__name__) 11 | CORS(app) 12 | env_config = os.getenv("APP_SETTINGS", "config.DevConfig") 13 | app.config.from_object('config.DevConfig') 14 | 15 | db.init_app(app) 16 | app.register_blueprint(app_routes) 17 | 18 | 19 | 20 | if __name__ == '__main__': 21 | app.run(host="0.0.0.0", port=8080) -------------------------------------------------------------------------------- /starklings-backend/config.py: -------------------------------------------------------------------------------- 1 | from os import environ, path 2 | from dotenv import load_dotenv 3 | 4 | basedir = path.abspath(path.dirname(__file__)) 5 | load_dotenv(path.join(basedir, '.env')) 6 | 7 | class Config: 8 | """Base config.""" 9 | SECRET_KEY = environ.get('SECRET_KEY') 10 | host = environ.get('DATABASE_HOST', '') 11 | database = environ.get('DATABASE_NAME', '') 12 | user = environ.get('DATABASE_USER', '') 13 | password = environ.get('DATABASE_PWD', '') 14 | SQLALCHEMY_DATABASE_URI = f"mysql+pymysql://{user}:{password}@{host}/{database}" 15 | 16 | SQLALCHEMY_TRACK_MODIFICATIONS = False 17 | 18 | 19 | class DevConfig(Config): 20 | ENV='development' 21 | TESTING = True 22 | DEBUG = True 23 | DEVELOPMENT = True 24 | DATABASE_URI = environ.get('DEV_DATABASE_URI') 25 | 26 | 27 | class ProdConfig(Config): 28 | ENV='production' 29 | TESTING = False 30 | DEBUG = False 31 | DEVELOPMENT = False 32 | DATABASE_URI = environ.get('PROD_DATABASE_URI') 33 | 34 | -------------------------------------------------------------------------------- /starklings-backend/db.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pymysql 3 | from dotenv import load_dotenv 4 | load_dotenv() 5 | 6 | connection = pymysql.connect( 7 | host=os.environ.get('DATABASE_HOST', ''), 8 | database=os.environ.get('DATABASE_NAME', ''), 9 | user=os.environ.get('DATABASE_USER', ''), 10 | password=os.environ.get('DATABASE_PWD', ''), 11 | charset="utf8mb4", 12 | cursorclass=pymysql.cursors.DictCursor 13 | ) 14 | 15 | cursor = connection.cursor() 16 | 17 | validated_sql_query = """CREATE TABLE validated_exercise ( 18 | exercise_name varchar(255) NOT NULL, 19 | user_id int NOT NULL FOREIGN KEY 20 | ... 21 | ) 22 | """ 23 | 24 | 25 | user_sql_query = """CREATE TABLE starklings_user ( 26 | user_id int NOT NULL PRIMARY KEY AUTO_INCREMENT, 27 | wallet_address varchar(255) NOT NULL UNIQUE, 28 | score int NOT NULL DEFAULT 0, 29 | username varchar(255) NOT NULL UNIQUE, 30 | signature varchar(255) NOT NULL 31 | ) 32 | """ 33 | 34 | #cursor.execute(validated_sql_query) 35 | cursor.execute(user_sql_query) 36 | connection.close() 37 | -------------------------------------------------------------------------------- /starklings-backend/requirements.txt: -------------------------------------------------------------------------------- 1 | bcrypt==3.2.2 2 | Flask==2.1.3 3 | flask-cors==3.0.10 4 | aioflask==0.4.0 5 | Flask-SQLAlchemy==2.5.1 6 | mysqlclient==2.1.1 7 | pathspec==0.9.0 8 | PyJWT==2.4.0 9 | PyMySQL==1.0.2 10 | python-dotenv==0.20.0 11 | requests==2.28.1 12 | rich==12.4.4 13 | SQLAlchemy==1.4.39 14 | typing_extensions==4.3.0 15 | urllib3==1.26.11 16 | pytest==7.1.2 17 | -------------------------------------------------------------------------------- /starklings-backend/starklings_backend/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onlydustxyz/starklings/ed480cce4cd74c518ffeaa40f63b71892c6e6a0a/starklings-backend/starklings_backend/__init__.py -------------------------------------------------------------------------------- /starklings-backend/starklings_backend/exercise.py: -------------------------------------------------------------------------------- 1 | #Starklings Imports 2 | from checker import check_exercise, ExerciceFailed 3 | from threading import Lock 4 | 5 | check_exercise_lock = Lock() 6 | 7 | async def verify_exercise(exercise_path): 8 | if check_exercise_lock.locked(): 9 | return 10 | with check_exercise_lock: 11 | try: 12 | await check_exercise(str(exercise_path)) 13 | except ExerciceFailed as error: 14 | raise ExerciceFailed(error.message) 15 | return await check_exercise(exercise_path) -------------------------------------------------------------------------------- /starklings-backend/starklings_backend/models/shared.py: -------------------------------------------------------------------------------- 1 | # apps.shared.models 2 | from flask_sqlalchemy import SQLAlchemy 3 | 4 | db = SQLAlchemy() -------------------------------------------------------------------------------- /starklings-backend/starklings_backend/models/user.py: -------------------------------------------------------------------------------- 1 | from starklings_backend.models.shared import db 2 | 3 | class Starklingsuser(db.Model): 4 | user_id = db.Column(db.Integer, primary_key=True) 5 | score = db.Column(db.Integer, nullable=False, default=0) 6 | signature = db.Column(db.String(255), nullable=False) 7 | username = db.Column(db.String(255), unique=True, nullable=False) 8 | wallet_address = db.Column(db.String(255), unique=True, nullable=False) 9 | -------------------------------------------------------------------------------- /starklings-backend/starklings_backend/routes.py: -------------------------------------------------------------------------------- 1 | from flask import request, Blueprint 2 | import asyncio 3 | import bcrypt 4 | from sqlalchemy.exc import IntegrityError 5 | from flask_sqlalchemy import SQLAlchemy 6 | from starklings_backend.utils import verify_email 7 | from starklings_backend.models.shared import db 8 | from starklings_backend.models.user import Starklingsuser 9 | from starklings_backend.exercise import verify_exercise 10 | from checker import ExerciceFailed 11 | import tempfile 12 | 13 | app_routes = Blueprint('app_routes', __name__) 14 | 15 | @app_routes.route('/', methods=['GET']) 16 | def landing(): 17 | return 'Starklings API' 18 | 19 | ####################### 20 | # Users Routes # 21 | ####################### 22 | @app_routes.route('/registerUser', methods=['POST']) 23 | def register_user(): 24 | """ 25 | Inserts a new user in the Database 26 | @TODO: Starknet ID / Signature and implements model 27 | """ 28 | try: 29 | signature = request.json.get('signature', None) 30 | wallet_address = request.json.get('wallet_address', None) 31 | username = request.json.get('username', wallet_address) 32 | if None in [wallet_address, signature]: 33 | return "Wrong form", 400 34 | #@TODO: Check Signature validity 35 | 36 | user = Starklingsuser(wallet_address=wallet_address, signature=signature, username=username) 37 | db.session.commit() 38 | return f'Welcome! {username}', 200 39 | 40 | except IntegrityError as e: 41 | db.session.rollback() 42 | return 'User Already Exists', 400 43 | except AttributeError: 44 | return 'Provide an Email and Password in JSON format in the request body', 400 45 | 46 | 47 | @app_routes.route('/fetchUserInfo', methods=['POST']) 48 | def fetch_user_info(): 49 | """ 50 | Authenticate a user 51 | @TODO Implements Fetch User Information 52 | """ 53 | try: 54 | wallet_address = request.json.get('wallet_address', None) 55 | if not wallet_address: 56 | return 'Missing address', 400 57 | user = Starklingsuser.query.filter_by(wallet_address=wallet_address).first() 58 | if not user: 59 | return 'User Not Found!', 404 60 | 61 | return f'Logged in, Welcome {user.username}!', 200 62 | except AttributeError as e: 63 | print(e) 64 | return 'Provide the wallet address in JSON format in the request body', 400 65 | 66 | 67 | ####################### 68 | # Exercises Routes # 69 | ####################### 70 | @app_routes.route('/exercise/check', methods=['POST']) 71 | async def starklings_exercise_checker(): 72 | """ 73 | Check exercise given a body and a user 74 | @TODO: Implement User DB for storing results 75 | """ 76 | try: 77 | address = request.json.get('wallet_address', None) 78 | exercise = request.json.get('exercise', 'storage/storage01') 79 | exercise_data = request.json.get('exercise_data', None) 80 | if not address: 81 | return 'Missing Address', 400 82 | tmp = tempfile.NamedTemporaryFile() 83 | with open(tmp.name, 'w') as temp_exercise: 84 | temp_exercise.write(exercise_data) 85 | res = await verify_exercise(tmp.name) 86 | tmp.close() 87 | return { 88 | "result": "success" 89 | } 90 | except ExerciceFailed as error: 91 | print(error) 92 | return { 93 | "result": "failure", 94 | "error": error.message 95 | }, 400 96 | -------------------------------------------------------------------------------- /starklings-backend/starklings_backend/utils.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import os 4 | import re 5 | 6 | regex = re.compile(r'([A-Za-z0-9]+[.-_])*[A-Za-z0-9]+@[A-Za-z0-9-]+(\.[A-Z|a-z]{2,})+') 7 | 8 | 9 | def verify_email(email): 10 | if re.fullmatch(regex, email): 11 | return True 12 | else: 13 | return False 14 | 15 | class Requester: 16 | def __init__(self, base_url, **kwargs): 17 | self.base_url = base_url 18 | self.session = requests.Session() 19 | for arg in kwargs: 20 | if isinstance(kwargs[arg], dict): 21 | kwargs[arg] = self.__deep_merge(getattr(self.session, arg), kwargs[arg]) 22 | setattr(self.session, arg, kwargs[arg]) 23 | 24 | def request(self, method, url, **kwargs): 25 | return self.session.request(method, self.base_url+url, **kwargs) 26 | 27 | def head(self, url, **kwargs): 28 | return self.session.head(self.base_url+url, **kwargs) 29 | 30 | def get(self, url, **kwargs): 31 | return self.session.get(self.base_url+url, **kwargs) 32 | 33 | def post(self, url, data, **kwargs): 34 | return self.session.post(self.base_url+url, data=data, **kwargs) 35 | 36 | def put(self, url, **kwargs): 37 | return self.session.put(self.base_url+url, **kwargs) 38 | 39 | def patch(self, url, **kwargs): 40 | return self.session.patch(self.base_url+url, **kwargs) 41 | 42 | def delete(self, url, **kwargs): 43 | return self.session.delete(self.base_url+url, **kwargs) 44 | 45 | @staticmethod 46 | def __deep_merge(source, destination): 47 | for key, value in source.items(): 48 | if isinstance(value, dict): 49 | node = destination.setdefault(key, {}) 50 | Requester.__deep_merge(value, node) 51 | else: 52 | destination[key] = value 53 | return destination 54 | -------------------------------------------------------------------------------- /starklings-backend/tests/test_app.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | 4 | import pytest 5 | 6 | from app import app 7 | 8 | @pytest.fixture 9 | def client(): 10 | db_fd, app.config['DATABASE'] = tempfile.mkstemp() 11 | app.config['TESTING'] = True 12 | 13 | os.close(db_fd) 14 | os.unlink(app.config['DATABASE']) -------------------------------------------------------------------------------- /starklings.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser 2 | import asyncio 3 | import os 4 | from pathlib import Path 5 | from rich.traceback import install 6 | from src import cli 7 | from src.config import current_working_directory 8 | 9 | install(show_locals=True) 10 | 11 | 12 | def is_valid_file(parser, arg): 13 | file_path = Path(arg).resolve() 14 | if not file_path.exists(): 15 | file_path = current_working_directory / arg 16 | if not file_path.exists(): 17 | return parser.error(f"The file {arg} does not exist!") 18 | return file_path 19 | 20 | 21 | script_root = Path(os.getcwd()) 22 | 23 | root_parser = ArgumentParser() 24 | 25 | root_parser.add_argument( 26 | "--version", 27 | "-V", 28 | help="Show version-related data", 29 | action="store_true", 30 | ) 31 | 32 | root_parser.add_argument( 33 | "--verify", 34 | "-v", 35 | metavar="relative_path_to_exercise", 36 | help="Verify a single exercise", 37 | type=lambda x: is_valid_file(root_parser, x), 38 | ) 39 | 40 | root_parser.add_argument( 41 | "--watch", 42 | "-w", 43 | help="Watch edited files and verify them", 44 | action="store_true", 45 | ) 46 | 47 | root_parser.add_argument( 48 | "--solution", 49 | "-s", 50 | metavar="relative_path_to_exercise", 51 | help="Provide a solution for an exercise", 52 | type=lambda x: is_valid_file(root_parser, x), 53 | ) 54 | 55 | root_parser.add_argument( 56 | "--display-course", 57 | "-d", 58 | action="store_true", 59 | help="Display the course content", 60 | ) 61 | 62 | root_parser.add_argument( 63 | "--login", 64 | "-l", 65 | help="Login with Github", 66 | action="store_true", 67 | ) 68 | 69 | try: 70 | asyncio.run(cli(root_parser.parse_args())) 71 | except Exception as error: 72 | print( 73 | "Unexpected Starklings error. Report it here:\n" 74 | + "https://github.com/onlydustxyz/starklings/issues\n" 75 | ) 76 | raise error 77 | -------------------------------------------------------------------------------- /starklings.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | from PyInstaller.utils.hooks import collect_data_files, collect_submodules 3 | 4 | block_cipher = None 5 | extra_files = [ 6 | ('pyproject.toml', 'info'), 7 | ('.solutions/*', '.solutions') 8 | ] + collect_data_files('starkware') 9 | # Extra imports which are necessary for executing hints 10 | extra_imports = [ 11 | "eth_hash.auto", 12 | ] + collect_submodules('starkware') 13 | 14 | a = Analysis(['starklings.py'], 15 | pathex=[], 16 | binaries=[], 17 | datas=extra_files, 18 | hiddenimports=extra_imports, 19 | hookspath=[], 20 | hooksconfig={}, 21 | runtime_hooks=[], 22 | excludes=[], 23 | win_no_prefer_redirects=False, 24 | win_private_assemblies=False, 25 | cipher=block_cipher, 26 | noarchive=False) 27 | pyz = PYZ(a.pure, a.zipped_data, 28 | cipher=block_cipher) 29 | 30 | exe = EXE(pyz, 31 | a.scripts, 32 | [], 33 | exclude_binaries=True, 34 | name='starklings', 35 | debug=False, 36 | bootloader_ignore_signals=False, 37 | strip=False, 38 | upx=True, 39 | console=True, 40 | disable_windowed_traceback=False, 41 | target_arch=None, 42 | codesign_identity=None, 43 | entitlements_file=None ) 44 | coll = COLLECT(exe, 45 | a.binaries, 46 | a.zipfiles, 47 | a.datas, 48 | strip=False, 49 | upx=True, 50 | upx_exclude=[], 51 | name='starklings') 52 | -------------------------------------------------------------------------------- /tests/exercises/test_end_of_exercise_messages/with_next_exercises/syntax01.cairo: -------------------------------------------------------------------------------- 1 | # All Starknet files must start with a specific line indicating the file is a smart contract, 2 | # not just a regular Cairo file 3 | 4 | # TODO: add the Starknet file specifier at the beginning of the file 5 | 6 | # You can ignore what follows for now 7 | @external 8 | func test_ok(): 9 | return () 10 | end 11 | -------------------------------------------------------------------------------- /tests/exercises/test_end_of_exercise_messages/with_next_exercises/syntax02.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | 3 | # Starknet provides a module management system. 4 | # It is very similar to the Python's one. 5 | 6 | # I AM NOT DONE 7 | 8 | # TODO: add the module imports needed to make the test pass! 9 | 10 | # You can ignore what follows for now 11 | @external 12 | func test_ok{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(): 13 | return () 14 | end 15 | -------------------------------------------------------------------------------- /tests/exercises/test_end_of_exercise_messages/without_next_exercises/syntax01.cairo: -------------------------------------------------------------------------------- 1 | # All Starknet files must start with a specific line indicating the file is a smart contract, 2 | # not just a regular Cairo file 3 | 4 | # TODO: add the Starknet file specifier at the beginning of the file 5 | 6 | # You can ignore what follows for now 7 | @external 8 | func test_ok(): 9 | return () 10 | end 11 | -------------------------------------------------------------------------------- /tests/exercises/test_end_of_exercise_messages/without_next_exercises/syntax02.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | 3 | # Starknet provides a module management system. 4 | # It is very similar to the Python's one. 5 | 6 | # TODO: add the module imports needed to make the test pass! 7 | 8 | # You can ignore what follows for now 9 | @external 10 | func test_ok{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(): 11 | return () 12 | end 13 | -------------------------------------------------------------------------------- /tests/test.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | 3 | func sum_func{syscall_ptr : felt*, range_check_ptr}(a : felt, b : felt) -> (res : felt): 4 | return (a + b) 5 | end 6 | 7 | @external 8 | func test_sum{syscall_ptr : felt*, range_check_ptr}(): 9 | let (r) = sum_func(4, 3) 10 | assert r = 7 11 | return () 12 | end 13 | -------------------------------------------------------------------------------- /tests/test_failure.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | 3 | func sum_func{syscall_ptr : felt*, range_check_ptr}(a : felt, b : felt) -> (res : felt): 4 | return (a + b) 5 | end 6 | 7 | @external 8 | func test_sum{syscall_ptr : felt*, range_check_ptr}(): 9 | let (r) = sum_func(4, 3) 10 | assert r = 6 11 | return () 12 | end 13 | -------------------------------------------------------------------------------- /tests/test_invalid.cairo: -------------------------------------------------------------------------------- 1 | %lang starknet 2 | 3 | func sum_func{syscall_ptr : felt*, range_check_ptr}(a : felt, b : felt) -> (res : felt): 4 | return (a + b) 5 | end 6 | 7 | @external 8 | func test_sum{syscall_ptr : felt*, range_check_ptr}(): 9 | let (r) = sum_func(4, 3) 10 | assert r 11 | return () 12 | end 13 | -------------------------------------------------------------------------------- /tests/test_missing.cairo: -------------------------------------------------------------------------------- 1 | @external 2 | func test_ok(): 3 | return () 4 | end 5 | --------------------------------------------------------------------------------