diff --git a/.github/stale.yml b/.github/stale.yml
deleted file mode 100644
index 01cb21d43..000000000
--- a/.github/stale.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-# Number of days of inactivity before an issue becomes stale
-daysUntilStale: 30
-# Number of days of inactivity before a stale issue is closed
-daysUntilClose: 7
-# Issues with these labels will never be considered stale
-exemptLabels:
- - pinned
- - security
-# Label to use when marking an issue as stale
-staleLabel: stale
-# Comment to post when marking an issue as stale. Set to `false` to disable
-markComment: >
- This issue has been automatically marked as stale because it has not had
- recent activity. It will be closed if no further activity occurs. Thank you
- for your contributions.
-# Comment to post when closing a stale issue. Set to `false` to disable
-closeComment: false
\ No newline at end of file
diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml
index 49d5bdeab..34d8802d6 100644
--- a/.github/workflows/checks.yml
+++ b/.github/workflows/checks.yml
@@ -228,7 +228,8 @@ jobs:
- name: Run tests
run: |
- poetry run poe test_ci
+ poetry run poe test_ci_gateway
+ poetry run poe test_ci_full_node
- name: Generate coverage in XML
run: |
@@ -242,7 +243,7 @@ jobs:
# ---------------------------------------------------------- #
run-tests-windows:
- # if: ${{ github.event_name != 'pull_request' }}
+ if: ${{ github.event_name != 'pull_request' }}
name: Tests Windows
needs: setup-tests
runs-on: windows-latest
@@ -278,7 +279,7 @@ jobs:
apt-get install -y python3-pip
sudo apt install -y libgmp3-dev
sudo apt-get install -y git
- pip3 install git+https://github.com/0xSpaceShard/starknet-devnet.git@744e9b3bd5fb9e856287158d87673e090df69d73
+ pip3 install git+https://github.com/0xSpaceShard/starknet-devnet.git@v0.5.4
# ====================== SETUP PYTHON ====================== #
@@ -311,7 +312,8 @@ jobs:
- name: Run tests
run: |
- poetry run poe test_ci
+ poetry run poe test_ci_gateway
+ poetry run poe test_ci_full_node
- name: Generate coverage in XML
run: |
@@ -373,7 +375,8 @@ jobs:
- name: Run tests
run: |
- poetry run poe test_ci_docs
+ poetry run poe test_ci_docs_gateway
+ poetry run poe test_ci_docs_full_node
- name: Generate coverage in XML
run: |
@@ -387,7 +390,7 @@ jobs:
# ---------------------------------------------------------- #
run-docs-tests-windows:
- # if: ${{ github.event_name != 'pull_request' }}
+ if: ${{ github.event_name != 'pull_request' }}
name: Docs Tests Windows
needs: setup-tests
runs-on: windows-latest
@@ -423,7 +426,7 @@ jobs:
apt-get install -y python3-pip
sudo apt install -y libgmp3-dev
sudo apt-get install -y git
- pip3 install git+https://github.com/0xSpaceShard/starknet-devnet.git@744e9b3bd5fb9e856287158d87673e090df69d73
+ pip3 install git+https://github.com/0xSpaceShard/starknet-devnet.git@v0.5.4
# ====================== SETUP PYTHON ====================== #
@@ -452,7 +455,8 @@ jobs:
- name: Run tests
run: |
- poetry run poe test_ci_docs
+ poetry run poe test_ci_docs_gateway
+ poetry run poe test_ci_docs_full_node
- name: Generate coverage in XML
run: |
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
new file mode 100644
index 000000000..022f5fb56
--- /dev/null
+++ b/.github/workflows/stale.yml
@@ -0,0 +1,24 @@
+name: 'Close stale issues and PRs'
+on:
+ schedule:
+ - cron: '30 1 * * *'
+
+jobs:
+ stale:
+ runs-on: ubuntu-latest
+ permissions:
+ issues: write
+ pull-requests: write
+ steps:
+ - uses: actions/stale@v8
+ with:
+ exempt-all-pr-assignees: true
+ exempt-pr-labels: 'pinned'
+ stale-issue-message: 'This issue is stale because it has not received any activity in the last 30 days. Remove stale label or add a comment, otherwise it will be closed in 5 days.'
+ close-issue-message: 'This issue was closed because it has been stale for 5 days with no activity.'
+ days-before-issue-stale: 30
+ days-before-issue-close: 5
+ stale-pr-message: 'This PR is stale because it has not received any activity in the last 30 days. Remove stale label or add a comment, otherwise it will be closed in 5 days.'
+ close-pr-message: 'This PR was closed because it has been stale for 5 days with no activity.'
+ days-before-pr-stale: 30
+ days-before-pr-close: 5
diff --git a/docs/api/models.rst b/docs/api/models.rst
index 344d4d8de..3450c8dc8 100644
--- a/docs/api/models.rst
+++ b/docs/api/models.rst
@@ -27,5 +27,3 @@ Module containing base models and functions to operate on them.
.. autoenum:: StarknetChainId
:members:
-
-.. autofunction:: compute_invoke_hash
diff --git a/docs/guide/deploying_contracts.rst b/docs/guide/deploying_contracts.rst
index cb4bfdea0..b84301bca 100644
--- a/docs/guide/deploying_contracts.rst
+++ b/docs/guide/deploying_contracts.rst
@@ -101,11 +101,6 @@ Here's an example how to declare a Cairo1 contract.
:language: python
:dedent: 4
-.. note::
-
- This is currently the only supported method of declaring a Cairo1 contract to Starknet.
- The support for declaring through :ref:`Contract` interface is planned for a future release.
-
Deploying Cairo1 contracts
##########################
@@ -118,8 +113,22 @@ After declaring a Cairo1 contract, it can be deployed using UDC.
:start-after: docs-deploy: start
:end-before: docs-deploy: end
-.. note::
- Currently only :meth:`~starknet_py.net.udc_deployer.deployer.Deployer.create_contract_deployment_raw` is supported.
- :meth:`~starknet_py.net.udc_deployer.deployer.Deployer.create_contract_deployment` will not work.
+Simple declare and deploy Cairo1 contract example
+#################################################
+.. codesnippet:: ../../starknet_py/tests/e2e/docs/guide/test_simple_declare_and_deploy_cairo1.py
+ :language: python
+ :dedent: 4
+ :start-after: docs: start
+ :end-before: docs: end
+
+
+Simple deploy Cairo1 contract example
+#####################################
+
+.. codesnippet:: ../../starknet_py/tests/e2e/docs/guide/test_simple_deploy_cairo1.py
+ :language: python
+ :dedent: 4
+ :start-after: docs: start
+ :end-before: docs: end
diff --git a/docs/installation.rst b/docs/installation.rst
index bbebf4161..96116dbb5 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -46,5 +46,13 @@ Apple silicon
Windows
-------
-This library is incompatible with Windows devices.
-Use virtual machine with Linux, `Windows Subsystem for Linux 2 `_ (WSL2) or other solution.
+You can install starknet.py on Windows in two ways:
+
+1. Install it just like you would on Linux.
+
+You might encounter problems related to ``libcrypto_c_exports``. Make sure that you have `MinGW `_ installed and up-to-date.
+
+If you encounter any further problems related to installation, you can create an `issue at our GitHub `_
+or ask for help in ``#🐍 | starknet-py`` channel on `Starknet Discord server `_.
+
+2. Use virtual machine with Linux, `Windows Subsystem for Linux 2 `_ (WSL2).
diff --git a/docs/migration_guide.rst b/docs/migration_guide.rst
index dd6950261..f7c3d2b72 100644
--- a/docs/migration_guide.rst
+++ b/docs/migration_guide.rst
@@ -1,6 +1,74 @@
Migration guide
===============
+****************************
+0.17.0-alpha Migration guide
+****************************
+
+.. currentmodule:: starknet_py.net.full_node_client
+
+:class:`FullNodeClient` RPC specification has been updated from `v0.3.0-rc1 `_ to `v0.3.0 `_.
+
+.. currentmodule:: starknet_py.contract
+
+
+:class:`Contract` now *initially* supports contracts written in **Cairo1**.
+
+To create an instance of such contract, a keyword parameter ``cairo_version=1`` in the Contract constructor is required.
+
+
+.. note::
+ Please note that while using the interface with `Cairo1` contracts, it is possible for problems to occur due to some of the types being not yet implemented in the parser.
+
+ In such case, please open an issue at our `GitHub `_ or contract us on `Starknet Discord server `_ in ``#🐍 | starknet-py`` channel.
+
+
+Breaking changes
+----------------
+
+1. Deprecated function ``compute_invoke_hash`` in :mod:`starknet_py.net.models.transaction` has been removed in favor of :func:`starknet_py.hash.transaction.compute_invoke_transaction_hash`.
+
+
+Minor changes
+-------------
+
+1. :meth:`DeclareResult.deploy`, :meth:`PreparedFunctionCall.invoke`, :meth:`PreparedFunctionCall.estimate_fee`, :meth:`ContractFunction.invoke`, :meth:`Contract.declare` and :meth:`Contract.deploy_contract` can now accept custom ``nonce`` parameter.
+
+.. currentmodule:: starknet_py.net.account.account
+
+2. :meth:`Account.sign_invoke_transaction`, :meth:`Account.sign_declare_transaction`, :meth:`Account.sign_declare_v2_transaction`, :meth:`Account.sign_deploy_account_transaction` and :meth:`Account.execute` can now accept custom ``nonce`` parameter.
+3. :meth:`Account.get_nonce` can now be parametrized with ``block_number`` or ``block_hash``.
+4. :meth:`Account.get_balance` can now be parametrized with ``block_number`` or ``block_hash``.
+
+RPC related changes:
+
+.. currentmodule:: starknet_py.net.client_models
+
+5. :class:`L2toL1Message` dataclass now has an additional field: ``from_address``.
+6. :class:`TransactionReceipt` dataclass now has two additional, optional fields: ``type`` and ``contract_address``.
+
+.. currentmodule:: starknet_py.net.full_node_client
+
+7. :meth:`FullNodeClient.get_events` ``keys`` and ``address`` parameters type are now optional.
+8. :meth:`FullNodeClient.get_events` ``keys`` parameter can now also accept integers as felts.
+
+
+Bugfixes
+--------
+
+.. currentmodule:: starknet_py.hash.class_hash
+
+1. Fixed a bug when :func:`compute_class_hash` mutated the ``contract_class`` argument passed to a function.
+
+
+|
+
+.. raw:: html
+
+
+
+|
+
**********************
0.16.1 Migration guide
**********************
@@ -9,23 +77,25 @@ Migration guide
Additionally, this release brings support for `RPC v0.3.0rc1 `_!
-Breaking changes
-----------------
+0.16.1 Breaking changes
+-----------------------
-.. currentmodule:: starknet_py.net
+.. currentmodule:: starknet_py.net.full_node_client
-1. ``FullNodeClient.get_events`` `keys` parameter type is now `List[List[str]]` instead of `List[str]`.
-2. ``FullNodeClient.get_state_update`` return type has been changed from `StateUpdate` to `Union[BlockStateUpdate, PendingBlockStateUpdate]`
+1. :meth:`FullNodeClient.get_events` ``keys`` parameter type is now ``List[List[str]]`` instead of ``List[str]``.
+2. :meth:`FullNodeClient.get_state_update` return type has been changed from ``StateUpdate`` to ``Union[BlockStateUpdate, PendingBlockStateUpdate]``
-.. currentmodule:: starknet_py.net.schemas
+.. currentmodule:: starknet_py.net.client_models
-3. ``StateDiff`` dataclass properties have been changed (more details in RPC specification linked above).
+3. :class:`StateDiff` dataclass properties have been changed (more details in RPC specification linked above).
-Minor changes
--------------
+0.16.1 Minor changes
+--------------------
+
+.. currentmodule:: starknet_py.net.client
-1. ``Client.estimate_fee`` can take a single transaction or a list of transactions to estimate.
+1. :meth:`Client.estimate_fee` can take a single transaction or a list of transactions to estimate.
|
@@ -207,7 +277,7 @@ The only supported Python version is 3.9.
- :class:`starknet_py.net.client_models.InvokeTransaction`
- :class:`starknet_py.net.models.transaction.Invoke`
- - :func:`starknet_py.net.models.transaction.compute_invoke_hash`
+ - ``compute_invoke_hash``
13. Replaced ``BlockStateUpdate.state_diff.declared_contract_hashes`` is now a list of ``DeclaredContractHash`` representing new Cairo classes. Old declared contract classes are still available at ``BlockStateUpdate.state_diff.deprecated_declared_contract_hashes``.
14. Removed ``version`` property from ``PreparedFunctionCall`` class.
15. Removed deprecated ``max_steps`` in :class:`~starknet_py.proxy.contract_abi_resolver.ProxyConfig`.
diff --git a/poetry.lock b/poetry.lock
index 65d6e7e39..d71b8b31a 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -574,63 +574,72 @@ files = [
[[package]]
name = "coverage"
-version = "7.2.6"
+version = "7.2.7"
description = "Code coverage measurement for Python"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
- {file = "coverage-7.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:496b86f1fc9c81a1cd53d8842ef712e950a4611bba0c42d33366a7b91ba969ec"},
- {file = "coverage-7.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fbe6e8c0a9a7193ba10ee52977d4d5e7652957c1f56ccefed0701db8801a2a3b"},
- {file = "coverage-7.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d06b721c2550c01a60e5d3093f417168658fb454e5dfd9a23570e9bffe39a1"},
- {file = "coverage-7.2.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:77a04b84d01f0e12c66f16e69e92616442dc675bbe51b90bfb074b1e5d1c7fbd"},
- {file = "coverage-7.2.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35db06450272473eab4449e9c2ad9bc6a0a68dab8e81a0eae6b50d9c2838767e"},
- {file = "coverage-7.2.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6727a0d929ff0028b1ed8b3e7f8701670b1d7032f219110b55476bb60c390bfb"},
- {file = "coverage-7.2.6-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aac1d5fdc5378f6bac2c0c7ebe7635a6809f5b4376f6cf5d43243c1917a67087"},
- {file = "coverage-7.2.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1c9e4a5eb1bbc3675ee57bc31f8eea4cd7fb0cbcbe4912cf1cb2bf3b754f4a80"},
- {file = "coverage-7.2.6-cp310-cp310-win32.whl", hash = "sha256:71f739f97f5f80627f1fee2331e63261355fd1e9a9cce0016394b6707ac3f4ec"},
- {file = "coverage-7.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:fde5c7a9d9864d3e07992f66767a9817f24324f354caa3d8129735a3dc74f126"},
- {file = "coverage-7.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bc7b667f8654376e9353dd93e55e12ce2a59fb6d8e29fce40de682273425e044"},
- {file = "coverage-7.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:697f4742aa3f26c107ddcb2b1784a74fe40180014edbd9adaa574eac0529914c"},
- {file = "coverage-7.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:541280dde49ce74a4262c5e395b48ea1207e78454788887118c421cb4ffbfcac"},
- {file = "coverage-7.2.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e7f1a8328eeec34c54f1d5968a708b50fc38d31e62ca8b0560e84a968fbf9a9"},
- {file = "coverage-7.2.6-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4bbd58eb5a2371bf160590f4262109f66b6043b0b991930693134cb617bc0169"},
- {file = "coverage-7.2.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ae82c5f168d2a39a5d69a12a69d4dc23837a43cf2ca99be60dfe59996ea6b113"},
- {file = "coverage-7.2.6-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f5440cdaf3099e7ab17a5a7065aed59aff8c8b079597b61c1f8be6f32fe60636"},
- {file = "coverage-7.2.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a6f03f87fea579d55e0b690d28f5042ec1368650466520fbc400e7aeaf09e995"},
- {file = "coverage-7.2.6-cp311-cp311-win32.whl", hash = "sha256:dc4d5187ef4d53e0d4c8eaf530233685667844c5fb0b855fea71ae659017854b"},
- {file = "coverage-7.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:c93d52c3dc7b9c65e39473704988602300e3cc1bad08b5ab5b03ca98bbbc68c1"},
- {file = "coverage-7.2.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:42c692b55a647a832025a4c048007034fe77b162b566ad537ce65ad824b12a84"},
- {file = "coverage-7.2.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7786b2fa7809bf835f830779ad285215a04da76293164bb6745796873f0942d"},
- {file = "coverage-7.2.6-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25bad4196104761bc26b1dae9b57383826542ec689ff0042f7f4f4dd7a815cba"},
- {file = "coverage-7.2.6-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2692306d3d4cb32d2cceed1e47cebd6b1d2565c993d6d2eda8e6e6adf53301e6"},
- {file = "coverage-7.2.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:392154d09bd4473b9d11351ab5d63391f3d5d24d752f27b3be7498b0ee2b5226"},
- {file = "coverage-7.2.6-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:fa079995432037b5e2ef5ddbb270bcd2ded9f52b8e191a5de11fe59a00ea30d8"},
- {file = "coverage-7.2.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d712cefff15c712329113b01088ba71bbcef0f7ea58478ca0bbec63a824844cb"},
- {file = "coverage-7.2.6-cp37-cp37m-win32.whl", hash = "sha256:004948e296149644d208964300cb3d98affc5211e9e490e9979af4030b0d6473"},
- {file = "coverage-7.2.6-cp37-cp37m-win_amd64.whl", hash = "sha256:c1d7a31603c3483ac49c1726723b0934f88f2c011c660e6471e7bd735c2fa110"},
- {file = "coverage-7.2.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3436927d1794fa6763b89b60c896f9e3bd53212001026ebc9080d23f0c2733c1"},
- {file = "coverage-7.2.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44c9b9f1a245f3d0d202b1a8fa666a80b5ecbe4ad5d0859c0fb16a52d9763224"},
- {file = "coverage-7.2.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e3783a286d5a93a2921396d50ce45a909aa8f13eee964465012f110f0cbb611"},
- {file = "coverage-7.2.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cff6980fe7100242170092bb40d2b1cdad79502cd532fd26b12a2b8a5f9aee0"},
- {file = "coverage-7.2.6-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c534431153caffc7c495c3eddf7e6a6033e7f81d78385b4e41611b51e8870446"},
- {file = "coverage-7.2.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3062fd5c62df988cea9f2972c593f77fed1182bfddc5a3b12b1e606cb7aba99e"},
- {file = "coverage-7.2.6-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6284a2005e4f8061c58c814b1600ad0074ccb0289fe61ea709655c5969877b70"},
- {file = "coverage-7.2.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:97729e6828643f168a2a3f07848e1b1b94a366b13a9f5aba5484c2215724edc8"},
- {file = "coverage-7.2.6-cp38-cp38-win32.whl", hash = "sha256:dc11b42fa61ff1e788dd095726a0aed6aad9c03d5c5984b54cb9e1e67b276aa5"},
- {file = "coverage-7.2.6-cp38-cp38-win_amd64.whl", hash = "sha256:cbcc874f454ee51f158afd604a315f30c0e31dff1d5d5bf499fc529229d964dd"},
- {file = "coverage-7.2.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d3cacc6a665221108ecdf90517a8028d07a2783df3417d12dcfef1c517e67478"},
- {file = "coverage-7.2.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:272ab31228a9df857ab5df5d67936d8861464dc89c5d3fab35132626e9369379"},
- {file = "coverage-7.2.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a8723ccec4e564d4b9a79923246f7b9a8de4ec55fa03ec4ec804459dade3c4f"},
- {file = "coverage-7.2.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5906f6a84b47f995cd1bf0aca1c72d591c55ee955f98074e93660d64dfc66eb9"},
- {file = "coverage-7.2.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52c139b7ab3f0b15f9aad0a3fedef5a1f8c0b2bdc291d88639ca2c97d3682416"},
- {file = "coverage-7.2.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a5ffd45c6b93c23a8507e2f436983015c6457aa832496b6a095505ca2f63e8f1"},
- {file = "coverage-7.2.6-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4f3c7c19581d471af0e9cb49d928172cd8492cd78a2b7a4e82345d33662929bb"},
- {file = "coverage-7.2.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2e8c0e79820cdd67978e1120983786422d279e07a381dbf89d03bbb23ec670a6"},
- {file = "coverage-7.2.6-cp39-cp39-win32.whl", hash = "sha256:13cde6bb0e58fb67d09e2f373de3899d1d1e866c5a9ff05d93615f2f54fbd2bb"},
- {file = "coverage-7.2.6-cp39-cp39-win_amd64.whl", hash = "sha256:6b9f64526286255735847aed0221b189486e0b9ed943446936e41b7e44b08783"},
- {file = "coverage-7.2.6-pp37.pp38.pp39-none-any.whl", hash = "sha256:6babcbf1e66e46052442f10833cfc4a0d3554d8276aa37af8531a83ed3c1a01d"},
- {file = "coverage-7.2.6.tar.gz", hash = "sha256:2025f913f2edb0272ef15d00b1f335ff8908c921c8eb2013536fcaf61f5a683d"},
+ {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"},
+ {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"},
+ {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"},
+ {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"},
+ {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"},
+ {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"},
+ {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"},
+ {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"},
+ {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"},
+ {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"},
+ {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"},
+ {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"},
+ {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"},
+ {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"},
+ {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"},
+ {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"},
+ {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"},
+ {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"},
+ {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"},
+ {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"},
+ {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"},
+ {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"},
+ {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"},
+ {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"},
+ {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"},
+ {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"},
+ {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"},
+ {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"},
+ {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"},
+ {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"},
+ {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"},
+ {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"},
+ {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"},
+ {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"},
+ {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"},
+ {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"},
+ {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"},
+ {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"},
+ {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"},
+ {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"},
+ {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"},
+ {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"},
+ {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"},
+ {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"},
+ {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"},
+ {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"},
+ {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"},
+ {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"},
+ {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"},
+ {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"},
+ {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"},
+ {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"},
+ {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"},
+ {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"},
+ {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"},
+ {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"},
+ {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"},
+ {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"},
+ {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"},
+ {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"},
]
[package.dependencies]
@@ -2292,14 +2301,14 @@ testutils = ["gitpython (>3)"]
[[package]]
name = "pyright"
-version = "1.1.311"
+version = "1.1.314"
description = "Command line wrapper for pyright"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
- {file = "pyright-1.1.311-py3-none-any.whl", hash = "sha256:04df30c6b31d05068effe5563411291c876f5e4221d0af225a267b61dce1ca85"},
- {file = "pyright-1.1.311.tar.gz", hash = "sha256:554b555d3f770e8da2e76d6bb94e2ac63b3edc7dcd5fb8de202f9dd53e36689a"},
+ {file = "pyright-1.1.314-py3-none-any.whl", hash = "sha256:5008a2e04b71e35c5f1b78b16adae9d012601197442ae6c798e9bb3456d1eecb"},
+ {file = "pyright-1.1.314.tar.gz", hash = "sha256:bd104c206fe40eaf5f836efa9027f07cc0efcbc452e6d22dfae36759c5fd28b3"},
]
[package.dependencies]
@@ -2348,18 +2357,17 @@ files = [
[[package]]
name = "pytest"
-version = "7.2.2"
+version = "7.3.2"
description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
- {file = "pytest-7.2.2-py3-none-any.whl", hash = "sha256:130328f552dcfac0b1cec75c12e3f005619dc5f874f0a06e8ff7263f0ee6225e"},
- {file = "pytest-7.2.2.tar.gz", hash = "sha256:c99ab0c73aceb050f68929bc93af19ab6db0558791c6a0715723abe9d0ade9d4"},
+ {file = "pytest-7.3.2-py3-none-any.whl", hash = "sha256:cdcbd012c9312258922f8cd3f1b62a6580fdced17db6014896053d47cddf9295"},
+ {file = "pytest-7.3.2.tar.gz", hash = "sha256:ee990a3cc55ba808b80795a79944756f315c67c12b56abd3ac993a7b8c17030b"},
]
[package.dependencies]
-attrs = ">=19.2.0"
colorama = {version = "*", markers = "sys_platform == \"win32\""}
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
iniconfig = "*"
@@ -2368,7 +2376,7 @@ pluggy = ">=0.12,<2.0"
tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
[package.extras]
-testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
+testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
[[package]]
name = "pytest-asyncio"
@@ -3039,34 +3047,30 @@ test = ["pytest"]
[[package]]
name = "starknet-devnet"
-version = "0.5.2"
+version = "0.5.4"
description = "A local testnet for Starknet"
category = "dev"
optional = false
python-versions = ">=3.9,<3.10"
-files = []
-develop = false
+files = [
+ {file = "starknet_devnet-0.5.4-py3-none-any.whl", hash = "sha256:cd22efa4ab222057dd602aee23b0bc37d979d7aceb038937eb17b64ae6fb9117"},
+ {file = "starknet_devnet-0.5.4.tar.gz", hash = "sha256:b76cc9c128f3625b48e57c6d8f0e8a189fc894c628d458ad25b904831828ae46"},
+]
[package.dependencies]
cairo-lang = "0.11.2"
-cloudpickle = "~2.1.0"
-crypto-cpp-py = "~1.4.0"
-Flask = {version = "~2.0.3", extras = ["async"]}
-flask-cors = "~3.0.10"
-gunicorn = "~20.1.0"
-jsonschema = "~4.17.0"
-marshmallow = "~3.17.0"
-marshmallow-dataclass = "~8.4"
-poseidon-py = "~0.1.3"
-typing-extensions = "~4.3.0"
-web3 = "~6.0.0"
-Werkzeug = "~2.0.3"
-
-[package.source]
-type = "git"
-url = "https://github.com/0xSpaceShard/starknet-devnet.git"
-reference = "744e9b3bd5fb9e856287158d87673e090df69d73"
-resolved_reference = "744e9b3bd5fb9e856287158d87673e090df69d73"
+cloudpickle = ">=2.1.0,<2.2.0"
+crypto-cpp-py = ">=1.4.0,<1.5.0"
+Flask = {version = ">=2.0.3,<2.1.0", extras = ["async"]}
+flask-cors = ">=3.0.10,<3.1.0"
+gunicorn = ">=20.1.0,<20.2.0"
+jsonschema = ">=4.17.0,<4.18.0"
+marshmallow = ">=3.17.0,<3.18.0"
+marshmallow-dataclass = ">=8.4,<8.5"
+poseidon-py = ">=0.1.3,<0.2.0"
+typing-extensions = ">=4.3.0,<4.4.0"
+web3 = ">=6.0.0,<6.1.0"
+Werkzeug = ">=2.0.3,<2.1.0"
[[package]]
name = "sympy"
@@ -3506,4 +3510,4 @@ docs = ["enum-tools", "furo", "sphinx"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.8, <3.12"
-content-hash = "5761bfcc8b95b4d6845a94efbc6a3298bfb0c78b1dee2bdcd1d25a70ae140f5c"
+content-hash = "6b79107f3f74b1f8a0bcd6728449a183dd7265156be6b569407e0832d7fa13fb"
diff --git a/pyproject.toml b/pyproject.toml
index 2c0b44412..5419c3043 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "starknet-py"
-version = "0.16.1"
+version = "0.17.0-alpha"
description = "A python SDK for Starknet"
authors = ["Tomasz Rejowski ", "Jakub Ptak "]
include = ["starknet_py", "starknet_py/utils/crypto/libcrypto_c_exports.*"]
@@ -50,12 +50,19 @@ pytest-rerunfailures = "^11.1"
[tool.poetry.group.py39-dev.dependencies]
cairo-lang = { version = "^0.11.2", python= ">=3.9, <3.10" }
-starknet-devnet = { git = "https://github.com/0xSpaceShard/starknet-devnet.git", rev = "744e9b3bd5fb9e856287158d87673e090df69d73", python= ">=3.9, <3.10" }
+starknet-devnet = {version = "0.5.4", python = ">=3.9,<3.10"}
[tool.poe.tasks]
test.shell = "pytest -n auto -v --reruns 10 --only-rerun aiohttp.client_exceptions.ClientConnectorError --cov=starknet_py starknet_py"
+
test_ci.shell = "coverage run -m pytest -v --reruns 5 --only-rerun aiohttp.client_exceptions.ClientConnectorError starknet_py --ignore=starknet_py/tests/e2e/docs --ignore=starknet_py/tests/e2e/core"
+test_ci_gateway.shell = "coverage run -m pytest --client=gateway -v --reruns 5 --only-rerun aiohttp.client_exceptions.ClientConnectorError starknet_py --ignore=starknet_py/tests/e2e/docs --ignore=starknet_py/tests/e2e/core"
+test_ci_full_node.shell = "coverage run -m pytest --client=full_node -v --reruns 5 --only-rerun aiohttp.client_exceptions.ClientConnectorError starknet_py --ignore=starknet_py/tests/e2e/docs --ignore=starknet_py/tests/e2e/core"
+
test_ci_docs.shell = "coverage run -m pytest -v --reruns 5 --only-rerun aiohttp.client_exceptions.ClientConnectorError starknet_py/tests/e2e/docs"
+test_ci_docs_gateway.shell = "coverage run -m pytest --client=gateway -v --reruns 5 --only-rerun aiohttp.client_exceptions.ClientConnectorError starknet_py/tests/e2e/docs"
+test_ci_docs_full_node.shell = "coverage run -m pytest --client=full_node -v --reruns 5 --only-rerun aiohttp.client_exceptions.ClientConnectorError starknet_py/tests/e2e/docs"
+
test_unit.shell = "pytest -n auto -v starknet_py --ignore=starknet_py/tests/e2e"
test_e2e.shell = "pytest -n auto -v starknet_py/tests/e2e --ignore=starknet_py/tests/e2e/docs"
test_docs.shell = "pytest -n auto -v starknet_py/tests/e2e/docs"
diff --git a/starknet_py/abi/parser.py b/starknet_py/abi/parser.py
index 0d6a3e036..742124a7a 100644
--- a/starknet_py/abi/parser.py
+++ b/starknet_py/abi/parser.py
@@ -125,9 +125,22 @@ def _parse_structures(self) -> Dict[str, StructType]:
# topological sorting with an additional "unresolved type", so this flow is much easier.
for name, struct in structs_dict.items():
structs[name] = StructType(name, OrderedDict())
+ without_offset = [
+ member for member in struct["members"] if member.get("offset") is None
+ ]
+ with_offset = [
+ member for member in struct["members"] if member not in without_offset
+ ]
struct_members[name] = sorted(
- struct["members"], key=lambda member: member["offset"]
+ with_offset, key=lambda member: member["offset"] # pyright: ignore
)
+ for member in without_offset:
+ member["offset"] = (
+ struct_members[name][-1].get("offset", 0) + 1
+ if struct_members[name]
+ else 0
+ )
+ struct_members[name].append(member)
# Now parse the types of members and save them.
self._type_parser = TypeParser(structs)
diff --git a/starknet_py/abi/parser_test.py b/starknet_py/abi/parser_test.py
index 65de7011a..a112273e0 100644
--- a/starknet_py/abi/parser_test.py
+++ b/starknet_py/abi/parser_test.py
@@ -37,6 +37,38 @@ def test_parsing_types_abi():
}
+def test_parsing_types_abi_missing_offset():
+ abi = AbiParser(
+ [
+ fixtures.user_missing_offset_dict,
+ fixtures.uint256_dict,
+ fixtures.pool_id_dict,
+ ]
+ ).parse()
+
+ assert abi.defined_structures == {
+ "Uint256": fixtures.uint256_struct,
+ "PoolId": fixtures.pool_id_struct,
+ "User": fixtures.user_struct,
+ }
+
+
+def test_parsing_types_abi_partial_missing_offset():
+ abi = AbiParser(
+ [
+ fixtures.user_partial_missing_offset_dict,
+ fixtures.uint256_dict,
+ fixtures.pool_id_dict,
+ ]
+ ).parse()
+
+ assert abi.defined_structures == {
+ "Uint256": fixtures.uint256_struct,
+ "PoolId": fixtures.pool_id_struct,
+ "User": fixtures.user_partial_missing_offset_struct,
+ }
+
+
def test_self_cycle():
self_referencing_struct = {
"type": "struct",
diff --git a/starknet_py/abi/schemas.py b/starknet_py/abi/schemas.py
index 62c6e8c32..3bfa50353 100644
--- a/starknet_py/abi/schemas.py
+++ b/starknet_py/abi/schemas.py
@@ -16,7 +16,7 @@ class TypedParameterSchema(Schema):
class StructMemberSchema(TypedParameterSchema):
- offset = fields.Integer(data_key="offset", required=True)
+ offset = fields.Integer(data_key="offset", required=False)
class FunctionBaseSchema(Schema):
diff --git a/starknet_py/abi/shape.py b/starknet_py/abi/shape.py
index 90da8765b..b2b8a52cd 100644
--- a/starknet_py/abi/shape.py
+++ b/starknet_py/abi/shape.py
@@ -1,4 +1,10 @@
-from typing import List, Literal, TypedDict, Union
+import sys
+from typing import List, Literal, Union
+
+if sys.version_info < (3, 11):
+ from typing_extensions import NotRequired, TypedDict
+else:
+ from typing import NotRequired, TypedDict
STRUCT_ENTRY = "struct"
FUNCTION_ENTRY = "function"
@@ -13,7 +19,7 @@ class TypedMemberDict(TypedDict):
class StructMemberDict(TypedMemberDict):
- offset: int
+ offset: NotRequired[int]
class StructDict(TypedDict):
@@ -27,6 +33,7 @@ class FunctionBaseDict(TypedDict):
name: str
inputs: List[TypedMemberDict]
outputs: List[TypedMemberDict]
+ stateMutability: NotRequired[Literal["view"]]
class FunctionDict(FunctionBaseDict):
diff --git a/starknet_py/abi/v1/__init__.py b/starknet_py/abi/v1/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/starknet_py/abi/v1/core_structures.json b/starknet_py/abi/v1/core_structures.json
new file mode 100644
index 000000000..0448bc28e
--- /dev/null
+++ b/starknet_py/abi/v1/core_structures.json
@@ -0,0 +1,14 @@
+{
+ "abi": [
+ {
+ "type": "struct",
+ "name": "core::starknet::eth_address::EthAddress",
+ "members": [
+ {
+ "name": "address",
+ "type": "core::felt252"
+ }
+ ]
+ }
+ ]
+}
diff --git a/starknet_py/abi/v1/model.py b/starknet_py/abi/v1/model.py
new file mode 100644
index 000000000..c5d621d6b
--- /dev/null
+++ b/starknet_py/abi/v1/model.py
@@ -0,0 +1,39 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import Dict, List, OrderedDict
+
+from starknet_py.cairo.data_types import CairoType, EnumType, StructType
+
+
+@dataclass
+class Abi:
+ """
+ Dataclass representing class abi. Contains parsed functions, enums, events and structures.
+ """
+
+ @dataclass
+ class Function:
+ """
+ Dataclass representing function's abi.
+ """
+
+ name: str
+ inputs: OrderedDict[str, CairoType]
+ outputs: List[CairoType]
+
+ @dataclass
+ class Event:
+ """
+ Dataclass representing event's abi.
+ """
+
+ name: str
+ inputs: OrderedDict[str, CairoType]
+
+ defined_structures: Dict[
+ str, StructType
+ ] #: Abi of structures defined by the class.
+ defined_enums: Dict[str, EnumType] #: Abi of enums defined by the class.
+ functions: Dict[str, Function] #: Functions defined by the class.
+ events: Dict[str, Event] #: Events defined by the class
diff --git a/starknet_py/abi/v1/parser.py b/starknet_py/abi/v1/parser.py
new file mode 100644
index 000000000..e12fc97ea
--- /dev/null
+++ b/starknet_py/abi/v1/parser.py
@@ -0,0 +1,220 @@
+from __future__ import annotations
+
+import dataclasses
+import json
+import os
+from collections import OrderedDict, defaultdict
+from pathlib import Path
+from typing import DefaultDict, Dict, List, Optional, Tuple, Union, cast
+
+from marshmallow import EXCLUDE
+
+from starknet_py.abi.v1.model import Abi
+from starknet_py.abi.v1.schemas import ContractAbiEntrySchema
+from starknet_py.abi.v1.shape import (
+ ENUM_ENTRY,
+ EVENT_ENTRY,
+ FUNCTION_ENTRY,
+ STRUCT_ENTRY,
+ EventDict,
+ FunctionDict,
+ TypedParameterDict,
+)
+from starknet_py.cairo.data_types import CairoType, EnumType, StructType
+from starknet_py.cairo.v1.type_parser import TypeParser
+
+
+class AbiParsingError(ValueError):
+ """
+ Error raised when something wrong goes during abi parsing.
+ """
+
+
+class AbiParser:
+ """
+ Utility class for parsing abi into a dataclass.
+ """
+
+ # Entries from ABI grouped by entry type
+ _grouped: DefaultDict[str, List[Dict]]
+ # lazy init property
+ _type_parser: Optional[TypeParser] = None
+
+ def __init__(self, abi_list: List[Dict]):
+ """
+ Abi parser constructor. Ensures that abi satisfies the abi schema.
+
+ :param abi_list: Contract's ABI as a list of dictionaries.
+ """
+ # prepend abi with core structures
+ core_structures = (
+ Path(os.path.dirname(__file__)) / "core_structures.json"
+ ).read_text("utf-8")
+ abi_list = json.loads(core_structures)["abi"] + abi_list
+ abi = [
+ ContractAbiEntrySchema().load(entry, unknown=EXCLUDE) for entry in abi_list
+ ]
+ grouped = defaultdict(list)
+ for entry in abi:
+ assert isinstance(entry, dict)
+ grouped[entry["type"]].append(entry)
+
+ self._grouped = grouped
+
+ def parse(self) -> Abi:
+ """
+ Parse abi provided to constructor and return it as a dataclass. Ensures that there are no cycles in the abi.
+
+ :raises: AbiParsingError: on any parsing error.
+ :return: Abi dataclass.
+ """
+ structures, enums = self._parse_structures_and_enums()
+ functions_dict = cast(
+ Dict[str, FunctionDict],
+ AbiParser._group_by_entry_name(
+ self._grouped[FUNCTION_ENTRY], "defined functions"
+ ),
+ )
+ events_dict = cast(
+ Dict[str, EventDict],
+ AbiParser._group_by_entry_name(
+ self._grouped[EVENT_ENTRY], "defined events"
+ ),
+ )
+
+ return Abi(
+ defined_structures=structures,
+ defined_enums=enums,
+ functions={
+ name: self._parse_function(entry)
+ for name, entry in functions_dict.items()
+ },
+ events={
+ name: self._parse_event(entry) for name, entry in events_dict.items()
+ },
+ )
+
+ @property
+ def type_parser(self) -> TypeParser:
+ if self._type_parser:
+ return self._type_parser
+
+ raise RuntimeError("Tried to get type_parser before it was set.")
+
+ def _parse_structures_and_enums(
+ self,
+ ) -> Tuple[Dict[str, StructType], Dict[str, EnumType]]:
+ structs_dict = AbiParser._group_by_entry_name(
+ self._grouped[STRUCT_ENTRY], "defined structures"
+ )
+ enums_dict = AbiParser._group_by_entry_name(
+ self._grouped[ENUM_ENTRY], "defined enums"
+ )
+
+ # Contains sorted members of the struct
+ struct_members: Dict[str, List[TypedParameterDict]] = {}
+ structs: Dict[str, StructType] = {}
+
+ # Contains sorted members of the enum
+ enum_members: Dict[str, List[TypedParameterDict]] = {}
+ enums: Dict[str, EnumType] = {}
+
+ # Example problem (with a simplified json structure):
+ # [{name: User, fields: {id: Uint256}}, {name: "Uint256", ...}]
+ # User refers to Uint256 even though it is not known yet (will be parsed next).
+ # This is why it is important to create the structure types first. This way other types can already refer to
+ # them when parsing types, even thought their fields are not filled yet.
+ # At the end we will mutate those structures to contain the right fields. An alternative would be to use
+ # topological sorting with an additional "unresolved type", so this flow is much easier.
+ for name, struct in structs_dict.items():
+ structs[name] = StructType(name, OrderedDict())
+ struct_members[name] = struct["members"]
+
+ for name, enum in enums_dict.items():
+ enums[name] = EnumType(name, OrderedDict())
+ enum_members[name] = enum["variants"]
+
+ # Now parse the types of members and save them.
+ defined_structs_enums: Dict[str, Union[StructType, EnumType]] = dict(structs)
+ defined_structs_enums.update(enums)
+
+ self._type_parser = TypeParser(defined_structs_enums)
+ for name, struct in structs.items():
+ members = self._parse_members(
+ cast(List[TypedParameterDict], struct_members[name]),
+ f"members of structure '{name}'",
+ )
+ struct.types.update(members)
+ for name, enum in enums.items():
+ members = self._parse_members(
+ cast(List[TypedParameterDict], enum_members[name]),
+ f"members of enum '{name}'",
+ )
+ enum.variants.update(members)
+
+ # All types have their members assigned now
+
+ self._check_for_cycles(defined_structs_enums)
+
+ return structs, enums
+
+ @staticmethod
+ def _check_for_cycles(structs: Dict[str, Union[StructType, EnumType]]):
+ # We want to avoid creating our own cycle checker as it would make it more complex. json module has a built-in
+ # checker for cycles.
+ try:
+ _to_json(structs)
+ except ValueError as err:
+ raise AbiParsingError(err) from ValueError
+
+ def _parse_function(self, function: FunctionDict) -> Abi.Function:
+ return Abi.Function(
+ name=function["name"],
+ inputs=self._parse_members(function["inputs"], function["name"]),
+ outputs=list(
+ self.type_parser.parse_inline_type(param["type"])
+ for param in function["outputs"]
+ ),
+ )
+
+ def _parse_event(self, event: EventDict) -> Abi.Event:
+ return Abi.Event(
+ name=event["name"],
+ inputs=self._parse_members(event["inputs"], event["name"]),
+ )
+
+ def _parse_members(
+ self, params: List[TypedParameterDict], entity_name: str
+ ) -> OrderedDict[str, CairoType]:
+ # Without cast, it complains that 'Type "TypedParameterDict" cannot be assigned to type "T@_group_by_name"'
+ members = AbiParser._group_by_entry_name(cast(List[Dict], params), entity_name)
+ return OrderedDict(
+ (name, self.type_parser.parse_inline_type(param["type"]))
+ for name, param in members.items()
+ )
+
+ @staticmethod
+ def _group_by_entry_name(
+ dicts: List[Dict], entity_name: str
+ ) -> OrderedDict[str, Dict]:
+ grouped = OrderedDict()
+ for entry in dicts:
+ name = entry["name"]
+ if name in grouped:
+ raise AbiParsingError(
+ f"Name '{name}' was used more than once in {entity_name}."
+ )
+ grouped[name] = entry
+ return grouped
+
+
+def _to_json(value):
+ class DataclassSupportingEncoder(json.JSONEncoder):
+ def default(self, o):
+ # Dataclasses are not supported by json. Additionally, dataclasses.asdict() works recursively and doesn't
+ # check for cycles, so we need to flatten dataclasses (by ONE LEVEL) ourselves.
+ if dataclasses.is_dataclass(o):
+ return tuple(getattr(o, field.name) for field in dataclasses.fields(o))
+ return super().default(o)
+
+ return json.dumps(value, cls=DataclassSupportingEncoder)
diff --git a/starknet_py/abi/v1/parser_test.py b/starknet_py/abi/v1/parser_test.py
new file mode 100644
index 000000000..d6faf9fb7
--- /dev/null
+++ b/starknet_py/abi/v1/parser_test.py
@@ -0,0 +1,174 @@
+import pytest
+
+import starknet_py.tests.e2e.fixtures.abi_v1_structures as fixtures
+from starknet_py.abi.v1.parser import AbiParser, AbiParsingError
+from starknet_py.cairo.v1.type_parser import UnknownCairoTypeError
+
+
+def test_parsing_types_abi():
+ # Even though user depend on pool id and uint256 it is defined first. Parser has to consider those cases
+ abi = AbiParser(
+ [
+ fixtures.user_dict,
+ fixtures.pool_id_dict,
+ fixtures.user_added_dict,
+ fixtures.pool_id_added_dict,
+ fixtures.get_user_dict,
+ fixtures.delete_pool_dict,
+ ]
+ ).parse()
+
+ assert abi.defined_structures == {
+ "PoolId": fixtures.pool_id_struct,
+ "User": fixtures.user_struct,
+ **fixtures.core_structures,
+ }
+ assert abi.events == {
+ "UserAdded": fixtures.user_added_event,
+ "PoolIdAdded": fixtures.pool_id_added_event,
+ }
+ assert abi.functions == {
+ "get_user": fixtures.get_user_fn,
+ "delete_pool": fixtures.delete_pool_fn,
+ }
+
+
+def test_parsing_types_abi2():
+ abi = AbiParser(
+ [
+ fixtures.foo_external_dict,
+ fixtures.foo_event_dict,
+ fixtures.foo_view_dict,
+ fixtures.my_enum_dict,
+ fixtures.my_struct_dict,
+ ]
+ ).parse()
+
+ assert abi.defined_structures == {
+ "test::MyStruct::": fixtures.my_struct,
+ **fixtures.core_structures,
+ }
+ assert abi.defined_enums == {
+ "test::MyEnum::": fixtures.my_enum,
+ }
+ assert abi.events == {
+ "foo_event": fixtures.foo_event,
+ }
+ assert abi.functions == {
+ "foo_external": fixtures.foo_external,
+ "foo_view": fixtures.foo_view,
+ }
+
+
+def test_self_cycle():
+ self_referencing_struct = {
+ "type": "struct",
+ "name": "Infinite",
+ "members": [
+ {"name": "value", "type": "Infinite"},
+ ],
+ }
+ with pytest.raises(
+ AbiParsingError,
+ match="Circular reference detected",
+ ):
+ AbiParser([self_referencing_struct]).parse()
+
+
+def test_bigger_cycle():
+ # first -> seconds -> third -> first...
+ first = {
+ "type": "struct",
+ "name": "First",
+ "members": [{"name": "value", "type": "Second"}],
+ }
+ second = {
+ "type": "struct",
+ "name": "Second",
+ "members": [{"name": "value", "type": "Third"}],
+ }
+ third = {
+ "type": "struct",
+ "name": "Third",
+ "members": [{"name": "value", "type": "First"}],
+ }
+ with pytest.raises(
+ AbiParsingError,
+ match="Circular reference detected",
+ ):
+ AbiParser([first, second, third]).parse()
+
+
+def test_duplicated_structure():
+ with pytest.raises(
+ AbiParsingError,
+ match="Name 'User' was used more than once in defined structures",
+ ):
+ AbiParser(
+ [fixtures.user_dict, fixtures.pool_id_dict, fixtures.user_dict]
+ ).parse()
+
+
+def test_duplicated_function():
+ with pytest.raises(
+ AbiParsingError,
+ match="Name 'get_user' was used more than once in defined functions",
+ ):
+ AbiParser(
+ [
+ fixtures.get_user_dict,
+ fixtures.delete_pool_dict,
+ fixtures.get_user_dict,
+ fixtures.delete_pool_dict,
+ ]
+ ).parse()
+
+
+def test_duplicated_event():
+ with pytest.raises(
+ AbiParsingError,
+ match="Name 'UserAdded' was used more than once in defined events",
+ ):
+ AbiParser(
+ [
+ fixtures.user_added_dict,
+ fixtures.delete_pool_dict,
+ fixtures.user_added_dict,
+ ]
+ ).parse()
+
+
+def test_duplicated_type_members():
+ type_dict = {
+ "type": "struct",
+ "name": "Record",
+ "members": [
+ {"name": "name", "type": "core::felt252"},
+ {"name": "value", "type": "core::felt252"},
+ {"name": "id", "type": "core::felt252"},
+ {"name": "value", "type": "core::felt252"},
+ ],
+ }
+ with pytest.raises(
+ AbiParsingError,
+ match="Name 'value' was used more than once in members of structure 'Record'",
+ ):
+ AbiParser([type_dict]).parse()
+
+
+@pytest.mark.parametrize(
+ "missing_name, input_dict",
+ [
+ # Type
+ ("PoolId", fixtures.user_dict),
+ # Function
+ ("User", fixtures.get_user_dict),
+ # Event
+ ("User", fixtures.user_added_dict),
+ ],
+)
+def test_missing_type_used(missing_name, input_dict):
+ with pytest.raises(
+ UnknownCairoTypeError, match=f"Type '{missing_name}' is not defined.*"
+ ):
+ AbiParser([input_dict]).parse()
diff --git a/starknet_py/abi/v1/parser_transformer.py b/starknet_py/abi/v1/parser_transformer.py
new file mode 100644
index 000000000..8b0ea761e
--- /dev/null
+++ b/starknet_py/abi/v1/parser_transformer.py
@@ -0,0 +1,169 @@
+from typing import Any, List, Optional
+
+import lark
+from lark import Token, Transformer
+
+from starknet_py.cairo.data_types import (
+ ArrayType,
+ BoolType,
+ CairoType,
+ FeltType,
+ OptionType,
+ TupleType,
+ TypeIdentifier,
+ UintType,
+ UnitType,
+)
+
+ABI_EBNF = """
+ IDENTIFIER: /[a-zA-Z_][a-zA-Z_0-9]*/
+
+ type: type_unit
+ | type_bool
+ | type_felt
+ | type_uint
+ | type_contract_address
+ | type_class_hash
+ | type_storage_address
+ | type_option
+ | type_array
+ | type_span
+ | tuple
+ | type_identifier
+
+
+ type_unit: "()"
+ type_felt: "core::felt252"
+ type_bool: "core::bool"
+ type_uint: "core::integer::u" INT
+ type_contract_address: "core::starknet::contract_address::ContractAddress"
+ type_class_hash: "core::starknet::class_hash::ClassHash"
+ type_storage_address: "core::starknet::storage_access::StorageAddress"
+ type_option: "core::option::Option::<" (type | type_identifier) ">"
+ type_array: "core::array::Array::<" (type | type_identifier) ">"
+ type_span: "core::array::Span::<" (type | type_identifier) ">"
+
+ tuple: "(" type? ("," type?)* ")"
+
+ type_identifier: (IDENTIFIER | "::")+ ("<" (type | ",")+ ">")?
+
+
+ %import common.INT
+ %import common.WS
+ %ignore WS
+"""
+
+
+class ParserTransformer(Transformer):
+ """
+ Transforms the lark tree into CairoTypes.
+ """
+
+ # pylint: disable=no-self-use
+
+ def __default__(self, data: str, children, meta):
+ raise TypeError(f"Unable to parse tree node of type {data}.")
+
+ def type(self, value: List[Optional[CairoType]]) -> Optional[CairoType]:
+ """
+ Tokens are read bottom-up, so here all of them are parsed and should be just returned.
+ `Optional` is added in case of the unit type.
+ """
+ assert len(value) == 1
+ return value[0]
+
+ def type_felt(self, _value: List[Any]) -> FeltType:
+ """
+ Felt does not contain any additional arguments, so `_value` is just an empty list.
+ """
+ return FeltType()
+
+ def type_bool(self, _value: List[Any]) -> BoolType:
+ """
+ Bool does not contain any additional arguments, so `_value` is just an empty list.
+ """
+ return BoolType()
+
+ def type_uint(self, value: List[Token]) -> UintType:
+ """
+ Uint type contains information about its size. It is present in the value[0].
+ """
+ return UintType(int(value[0]))
+
+ def type_unit(self, _value: List[Any]) -> UnitType:
+ """
+ `()` type.
+ """
+ return UnitType()
+
+ def type_option(self, value: List[CairoType]) -> OptionType:
+ """
+ Option includes an information about which type it eventually represents.
+ `Optional` is added in case of the unit type.
+ """
+ return OptionType(value[0])
+
+ def type_array(self, value: List[CairoType]) -> ArrayType:
+ """
+ Array contains values of type under `value[0]`.
+ """
+ return ArrayType(value[0])
+
+ def type_span(self, value: List[CairoType]) -> ArrayType:
+ """
+ Span contains values of type under `value[0]`.
+ """
+ return ArrayType(value[0])
+
+ def type_identifier(self, tokens: List[Token]) -> TypeIdentifier:
+ """
+ Structs and enums are defined as follows: (IDENTIFIER | "::")+ [some not important info]
+ where IDENTIFIER is a string.
+
+ Tokens would contain strings and types (if it is present).
+ We are interested only in the strings because a structure (or enum) name can be built from them.
+ """
+ name = "::".join(token for token in tokens if isinstance(token, str))
+ return TypeIdentifier(name)
+
+ def type_contract_address(self, _value: List[Any]) -> FeltType:
+ """
+ ContractAddress is represented by the felt252.
+ """
+ return FeltType()
+
+ def type_class_hash(self, _value: List[Any]) -> FeltType:
+ """
+ ClassHash is represented by the felt252.
+ """
+ return FeltType()
+
+ def type_storage_address(self, _value: List[Any]) -> FeltType:
+ """
+ StorageAddress is represented by the felt252.
+ """
+ return FeltType()
+
+ def tuple(self, types: List[CairoType]) -> TupleType:
+ """
+ Tuple contains values defined in the `types` argument.
+ """
+ return TupleType(types)
+
+
+def parse(
+ code: str,
+) -> CairoType:
+ """
+ Parse the given string and return a CairoType.
+ """
+ grammar_parser = lark.Lark(
+ grammar=ABI_EBNF,
+ start="type",
+ parser="earley",
+ )
+ parsed = grammar_parser.parse(code)
+
+ transformed = ParserTransformer().transform(parsed)
+
+ return transformed
diff --git a/starknet_py/abi/v1/parser_transformer_test.py b/starknet_py/abi/v1/parser_transformer_test.py
new file mode 100644
index 000000000..ed99ee56a
--- /dev/null
+++ b/starknet_py/abi/v1/parser_transformer_test.py
@@ -0,0 +1,9 @@
+import pytest
+from lark import Token, Tree
+
+from starknet_py.abi.v1.parser_transformer import ParserTransformer
+
+
+def test_default_parser_transformer():
+ with pytest.raises(TypeError, match="Unable to parse tree node of type wrong."):
+ ParserTransformer().transform(Tree(data=Token("RULE", "wrong"), children=[]))
diff --git a/starknet_py/abi/v1/schemas.py b/starknet_py/abi/v1/schemas.py
new file mode 100644
index 000000000..dacb4d1cc
--- /dev/null
+++ b/starknet_py/abi/v1/schemas.py
@@ -0,0 +1,66 @@
+from marshmallow import Schema, fields
+from marshmallow_oneofschema import OneOfSchema
+
+from starknet_py.abi.v1.shape import (
+ ENUM_ENTRY,
+ EVENT_ENTRY,
+ FUNCTION_ENTRY,
+ STRUCT_ENTRY,
+)
+
+
+class TypeSchema(Schema):
+ type = fields.String(data_key="type", required=True)
+
+
+class TypedParameterSchema(TypeSchema):
+ name = fields.String(data_key="name", required=True)
+
+
+class FunctionBaseSchema(Schema):
+ name = fields.String(data_key="name", required=True)
+ inputs = fields.List(
+ fields.Nested(TypedParameterSchema()), data_key="inputs", required=True
+ )
+ outputs = fields.List(
+ fields.Nested(TypeSchema()), data_key="outputs", required=True
+ )
+ state_mutability = fields.String(data_key="state_mutability", default=None)
+
+
+class FunctionAbiEntrySchema(FunctionBaseSchema):
+ type = fields.Constant(FUNCTION_ENTRY, data_key="type", required=True)
+
+
+class EventAbiEntrySchema(Schema):
+ type = fields.Constant(EVENT_ENTRY, data_key="type", required=True)
+ name = fields.String(data_key="name", required=True)
+ inputs = fields.List(
+ fields.Nested(TypedParameterSchema()), data_key="inputs", required=True
+ )
+
+
+class StructAbiEntrySchema(Schema):
+ type = fields.Constant(STRUCT_ENTRY, data_key="type", required=True)
+ name = fields.String(data_key="name", required=True)
+ members = fields.List(
+ fields.Nested(TypedParameterSchema()), data_key="members", required=True
+ )
+
+
+class EnumAbiEntrySchema(Schema):
+ type = fields.Constant(ENUM_ENTRY, data_key="type", required=True)
+ name = fields.String(data_key="name", required=True)
+ variants = fields.List(
+ fields.Nested(TypedParameterSchema(), data_key="variants", required=True)
+ )
+
+
+class ContractAbiEntrySchema(OneOfSchema):
+ type_field_remove = False
+ type_schemas = {
+ FUNCTION_ENTRY: FunctionAbiEntrySchema,
+ EVENT_ENTRY: EventAbiEntrySchema,
+ STRUCT_ENTRY: StructAbiEntrySchema,
+ ENUM_ENTRY: EnumAbiEntrySchema,
+ }
diff --git a/starknet_py/abi/v1/schemas_test.py b/starknet_py/abi/v1/schemas_test.py
new file mode 100644
index 000000000..95ca85245
--- /dev/null
+++ b/starknet_py/abi/v1/schemas_test.py
@@ -0,0 +1,32 @@
+import json
+
+import pytest
+from marshmallow import EXCLUDE
+
+from starknet_py.abi.v1.schemas import ContractAbiEntrySchema
+from starknet_py.tests.e2e.fixtures.constants import CONTRACTS_COMPILED_V1_DIR
+from starknet_py.tests.e2e.fixtures.misc import read_contract
+
+
+@pytest.mark.parametrize(
+ "contract_name",
+ [
+ "account",
+ "erc20",
+ "hello_starknet",
+ "minimal_contract",
+ "test_contract",
+ "token_bridge",
+ ],
+)
+def test_deserialize_abi(contract_name):
+ abi = json.loads(
+ read_contract(
+ f"{contract_name}_compiled.json", directory=CONTRACTS_COMPILED_V1_DIR
+ )
+ )["abi"]
+ deserialized = [
+ ContractAbiEntrySchema().load(entry, unknown=EXCLUDE) for entry in abi
+ ]
+
+ assert len(deserialized) == len(abi)
diff --git a/starknet_py/abi/v1/shape.py b/starknet_py/abi/v1/shape.py
new file mode 100644
index 000000000..0a4b94f8c
--- /dev/null
+++ b/starknet_py/abi/v1/shape.py
@@ -0,0 +1,47 @@
+from typing import List, Literal, Optional, TypedDict, Union
+
+ENUM_ENTRY = "enum"
+STRUCT_ENTRY = "struct"
+FUNCTION_ENTRY = "function"
+EVENT_ENTRY = "event"
+
+
+class TypeDict(TypedDict):
+ type: str
+
+
+class TypedParameterDict(TypeDict):
+ name: str
+
+
+class StructDict(TypedDict):
+ type: Literal["struct"]
+ name: str
+ members: List[TypedParameterDict]
+
+
+class FunctionBaseDict(TypedDict):
+ name: str
+ inputs: List[TypedParameterDict]
+ outputs: List[TypeDict]
+ state_mutability: Optional[Literal["external", "view"]]
+
+
+class FunctionDict(FunctionBaseDict):
+ type: Literal["function"]
+
+
+class EventDict(TypedDict):
+ name: str
+ type: Literal["event"]
+ inputs: List[TypedParameterDict]
+
+
+class EnumDict(TypedDict):
+ type: Literal["enum"]
+ name: str
+ variants: List[TypedParameterDict]
+
+
+AbiDictEntry = Union[StructDict, FunctionDict, EventDict, EnumDict]
+AbiDictList = List[AbiDictEntry]
diff --git a/starknet_py/cairo/data_types.py b/starknet_py/cairo/data_types.py
index b0a034949..d26844c3b 100644
--- a/starknet_py/cairo/data_types.py
+++ b/starknet_py/cairo/data_types.py
@@ -19,6 +19,13 @@ class FeltType(CairoType):
"""
+@dataclass
+class BoolType(CairoType):
+ """
+ Type representation of Cairo boolean.
+ """
+
+
@dataclass
class TupleType(CairoType):
"""
@@ -55,3 +62,52 @@ class StructType(CairoType):
name: str #: Structure name
# We need ordered dict, because it is important in serialization
types: OrderedDict[str, CairoType] #: types of every structure member.
+
+
+@dataclass
+class EnumType(CairoType):
+ """
+ Type representation of Cairo enums.
+ """
+
+ name: str
+ variants: OrderedDict[str, CairoType]
+
+
+@dataclass
+class OptionType(CairoType):
+ """
+ Type representation of Cairo options.
+ """
+
+ type: CairoType
+
+
+@dataclass
+class UintType(CairoType):
+ """
+ Type representation of Cairo unsigned integers.
+ """
+
+ bits: int
+
+ def check_range(self, value: int):
+ """
+ Utility method checking if the `value` is in range.
+ """
+
+
+@dataclass
+class TypeIdentifier(CairoType):
+ """
+ Type representation of Cairo identifiers.
+ """
+
+ name: str
+
+
+@dataclass
+class UnitType(CairoType):
+ """
+ Type representation of Cairo unit `()`.
+ """
diff --git a/starknet_py/cairo/deprecated_parse/cairo.ebnf b/starknet_py/cairo/deprecated_parse/cairo.ebnf
deleted file mode 100644
index 71e2d71e2..000000000
--- a/starknet_py/cairo/deprecated_parse/cairo.ebnf
+++ /dev/null
@@ -1,20 +0,0 @@
-%import common.WS_INLINE
-%ignore WS_INLINE
-
-IDENTIFIER: /[a-zA-Z_][a-zA-Z_0-9]*/
-_DBL_STAR: "**"
-COMMA: ","
-
-?type: non_identifier_type
- | identifier -> type_struct
-
-comma_separated{item}: item? (COMMA item)* COMMA?
-
-named_type: identifier (":" type)? | non_identifier_type
-non_identifier_type: "felt" -> type_felt
- | "codeoffset" -> type_codeoffset
- | type "*" -> type_pointer
- | type _DBL_STAR -> type_pointer2
- | "(" comma_separated{named_type} ")" -> type_tuple
-
-identifier: IDENTIFIER ("." IDENTIFIER)*
diff --git a/starknet_py/cairo/deprecated_parse/parser.py b/starknet_py/cairo/deprecated_parse/parser.py
index 39d382144..9111bc0bf 100644
--- a/starknet_py/cairo/deprecated_parse/parser.py
+++ b/starknet_py/cairo/deprecated_parse/parser.py
@@ -1,19 +1,38 @@
-import os
-
import lark
from starknet_py.cairo.deprecated_parse.cairo_types import CairoType
from starknet_py.cairo.deprecated_parse.parser_transformer import ParserTransformer
+CAIRO_EBNF = """
+ %import common.WS_INLINE
+ %ignore WS_INLINE
+
+ IDENTIFIER: /[a-zA-Z_][a-zA-Z_0-9]*/
+ _DBL_STAR: "**"
+ COMMA: ","
+
+ ?type: non_identifier_type
+ | identifier -> type_struct
+
+ comma_separated{item}: item? (COMMA item)* COMMA?
+
+ named_type: identifier (":" type)? | non_identifier_type
+ non_identifier_type: "felt" -> type_felt
+ | "codeoffset" -> type_codeoffset
+ | type "*" -> type_pointer
+ | type _DBL_STAR -> type_pointer2
+ | "(" comma_separated{named_type} ")" -> type_tuple
+
+ identifier: IDENTIFIER ("." IDENTIFIER)*
+"""
+
def parse(code: str) -> CairoType:
"""
Parses the given string and returns a CairoType.
"""
- with open(
- os.path.join(os.path.dirname(__file__), "cairo.ebnf"), "r", encoding="utf-8"
- ) as grammar_file:
- grammar = grammar_file.read()
+
+ grammar = CAIRO_EBNF
grammar_parser = lark.Lark(
grammar=grammar,
diff --git a/starknet_py/cairo/v1/__init__.py b/starknet_py/cairo/v1/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/starknet_py/cairo/v1/type_parser.py b/starknet_py/cairo/v1/type_parser.py
new file mode 100644
index 000000000..74f1f35ce
--- /dev/null
+++ b/starknet_py/cairo/v1/type_parser.py
@@ -0,0 +1,76 @@
+from __future__ import annotations
+
+from typing import Dict, Union
+
+from starknet_py.abi.v1.parser_transformer import parse
+from starknet_py.cairo.data_types import (
+ ArrayType,
+ CairoType,
+ EnumType,
+ StructType,
+ TypeIdentifier,
+)
+
+
+class UnknownCairoTypeError(ValueError):
+ """
+ Error thrown when TypeParser finds type that was not declared prior to parsing.
+ """
+
+ type_name: str
+
+ def __init__(self, type_name: str):
+ super().__init__(
+ # pylint: disable=line-too-long
+ f"Type '{type_name}' is not defined. Please report this issue at https://github.com/software-mansion/starknet.py/issues"
+ )
+ self.type_name = type_name
+
+
+class TypeParser:
+ """
+ Low level utility class for parsing Cairo types that can be used in external methods.
+ """
+
+ defined_types: Dict[str, Union[StructType, EnumType]]
+
+ def __init__(self, defined_types: Dict[str, Union[StructType, EnumType]]):
+ """
+ TypeParser constructor.
+
+ :param defined_types: dictionary containing all defined types. For now, they can only be structures.
+ """
+ self.defined_types = defined_types
+ for name, defined_type in defined_types.items():
+ if name != defined_type.name:
+ raise ValueError(
+ f"Keys must match name of type, '{name}' != '{defined_type.name}'."
+ )
+
+ def parse_inline_type(self, type_string: str) -> CairoType:
+ """
+ Inline type is one that can be used inline, for instance as return type. For instance
+ (core::felt252, (), (core::felt252,)). Structure can only be referenced in inline type, can't be defined
+ this way.
+
+ :param type_string: type to parse.
+ """
+ parsed = parse(type_string)
+
+ if isinstance(parsed, TypeIdentifier):
+ return self._get_struct(parsed)
+
+ # TODO (#1079): add recursive support for iterables with structures (tuples?)
+ if isinstance(parsed, ArrayType):
+ inner_type_string = type_string[
+ type_string.find("<") + 1 : type_string.rfind(">")
+ ]
+ parsed.inner_type = self.parse_inline_type(inner_type_string)
+
+ return parsed
+
+ def _get_struct(self, identifier: TypeIdentifier):
+ for struct_name in self.defined_types.keys():
+ if identifier.name == struct_name.split("<")[0].strip(":"):
+ return self.defined_types[struct_name]
+ raise UnknownCairoTypeError(identifier.name)
diff --git a/starknet_py/cairo/v1/type_parser_test.py b/starknet_py/cairo/v1/type_parser_test.py
new file mode 100644
index 000000000..0ebc68582
--- /dev/null
+++ b/starknet_py/cairo/v1/type_parser_test.py
@@ -0,0 +1,111 @@
+from collections import OrderedDict
+from typing import Dict, Union
+
+import pytest
+
+from starknet_py.cairo.data_types import (
+ ArrayType,
+ EnumType,
+ FeltType,
+ StructType,
+ TupleType,
+ TypeIdentifier,
+ UnitType,
+)
+from starknet_py.cairo.v1.type_parser import TypeParser, UnknownCairoTypeError
+
+
+@pytest.mark.parametrize(
+ "type_string, expected",
+ [
+ ("core::felt252", FeltType()),
+ ("core::array::Array::", ArrayType(FeltType())),
+ (
+ "(core::felt252, core::felt252, core::felt252, core::felt252)",
+ TupleType([FeltType()] * 4),
+ ),
+ ("(,)", TupleType([])),
+ ("()", UnitType()),
+ (
+ "(core::felt252, (core::felt252, (core::array::Array::, core::felt252)))",
+ TupleType(
+ [
+ FeltType(),
+ TupleType(
+ [
+ FeltType(),
+ TupleType(
+ [
+ ArrayType(FeltType()),
+ FeltType(),
+ ]
+ ),
+ ]
+ ),
+ ]
+ ),
+ ),
+ ],
+)
+def test_parse_without_defined_types(type_string, expected):
+ parsed = TypeParser({}).parse_inline_type(type_string)
+ assert parsed == expected
+
+
+uint256_type = StructType("Uint256", OrderedDict(low=FeltType(), high=FeltType()))
+wrapped_felt_type = StructType("WrappedFelt", OrderedDict(value=FeltType()))
+
+
+@pytest.mark.parametrize(
+ "type_string, expected",
+ [
+ ("Uint256", uint256_type),
+ ("core::array::Array::", ArrayType(uint256_type)),
+ (
+ "(Uint256, WrappedFelt)",
+ TupleType([TypeIdentifier("Uint256"), TypeIdentifier("WrappedFelt")]),
+ ),
+ (
+ "(Uint256, (WrappedFelt, (core::array::Array::, core::array::Array::)))",
+ TupleType(
+ [
+ TypeIdentifier("Uint256"),
+ TupleType(
+ [
+ TypeIdentifier("WrappedFelt"),
+ TupleType(
+ [
+ ArrayType(FeltType()),
+ ArrayType(TypeIdentifier("WrappedFelt")),
+ ]
+ ),
+ ]
+ ),
+ ]
+ ),
+ ),
+ ],
+)
+def test_parse_with_defined_types(type_string, expected):
+ types: Dict[str, Union[StructType, EnumType]] = {
+ "Uint256": uint256_type,
+ "WrappedFelt": wrapped_felt_type,
+ }
+ parsed = TypeParser(types).parse_inline_type(type_string)
+ assert parsed == expected
+
+
+def test_missing_type():
+ with pytest.raises(
+ UnknownCairoTypeError, match="Type 'Uint256' is not defined"
+ ) as err_info:
+ TypeParser({}).parse_inline_type("Uint256")
+
+ assert err_info.value.type_name == "Uint256"
+
+
+def test_names_not_matching():
+ with pytest.raises(
+ ValueError, match="Keys must match name of type, 'OtherName' != 'Uint256'."
+ ):
+ TypeParser({"OtherName": uint256_type})
diff --git a/starknet_py/conftest.py b/starknet_py/conftest.py
index 4c4de0294..f586a5338 100644
--- a/starknet_py/conftest.py
+++ b/starknet_py/conftest.py
@@ -4,6 +4,7 @@
"starknet_py.tests.e2e.fixtures.clients",
"starknet_py.tests.e2e.fixtures.accounts",
"starknet_py.tests.e2e.fixtures.contracts",
+ "starknet_py.tests.e2e.fixtures.contracts_v1",
"starknet_py.tests.e2e.fixtures.misc",
"starknet_py.tests.e2e.fixtures.devnet",
"starknet_py.tests.e2e.fixtures.constants",
diff --git a/starknet_py/constants.py b/starknet_py/constants.py
index 62d35484f..07bd3e5b7 100644
--- a/starknet_py/constants.py
+++ b/starknet_py/constants.py
@@ -12,8 +12,8 @@
API_VERSION = 0
RPC_CONTRACT_NOT_FOUND_ERROR = 20
-RPC_INVALID_MESSAGE_SELECTOR_ERROR = 21
RPC_CLASS_HASH_NOT_FOUND_ERROR = 28
+RPC_CONTRACT_ERROR = 40
DEFAULT_ENTRY_POINT_NAME = "__default__"
DEFAULT_L1_ENTRY_POINT_NAME = "__l1_default__"
diff --git a/starknet_py/contract.py b/starknet_py/contract.py
index a6292532a..07974bdd0 100644
--- a/starknet_py/contract.py
+++ b/starknet_py/contract.py
@@ -1,18 +1,25 @@
from __future__ import annotations
import dataclasses
-import warnings
+import json
from dataclasses import dataclass
from functools import cached_property
-from typing import Dict, List, Optional, Tuple, TypeVar, Union
+from typing import Dict, List, Optional, Tuple, TypeVar, Union, cast
from marshmallow import ValidationError
from starknet_py.abi.model import Abi
from starknet_py.abi.parser import AbiParser
-from starknet_py.common import create_compiled_contract
+from starknet_py.abi.v1.model import Abi as AbiV1
+from starknet_py.abi.v1.parser import AbiParser as AbiV1Parser
+from starknet_py.common import (
+ create_casm_class,
+ create_compiled_contract,
+ create_sierra_compiled_contract,
+)
from starknet_py.constants import DEFAULT_DEPLOYER_ADDRESS
from starknet_py.hash.address import compute_address
+from starknet_py.hash.casm_class_hash import compute_casm_class_hash
from starknet_py.hash.class_hash import compute_class_hash
from starknet_py.hash.selector import get_selector_from_name
from starknet_py.net.account.base_account import BaseAccount
@@ -23,10 +30,10 @@
from starknet_py.proxy.contract_abi_resolver import (
ContractAbiResolver,
ProxyConfig,
- UnsupportedAbiError,
prepare_proxy_config,
)
from starknet_py.serialization import TupleDataclass, serializer_for_function
+from starknet_py.serialization.factory import serializer_for_function_v1
from starknet_py.serialization.function_serialization_adapter import (
FunctionSerializationAdapter,
)
@@ -46,28 +53,33 @@ class ContractData:
address: int
abi: ABI
+ cairo_version: int
@cached_property
- def parsed_abi(self) -> Abi:
+ def parsed_abi(self) -> Union[Abi, AbiV1]:
"""
Abi parsed into proper dataclass.
:return: Abi
"""
+ if self.cairo_version == 1:
+ return AbiV1Parser(self.abi).parse()
return AbiParser(self.abi).parse()
@staticmethod
- def from_abi(address: int, abi: ABI) -> ContractData:
+ def from_abi(address: int, abi: ABI, cairo_version: int = 0) -> ContractData:
"""
Create ContractData from ABI.
:param address: Address of the deployed contract.
:param abi: Abi of the contract.
+ :param cairo_version: Version of the Cairo in which contract is written.
:return: ContractData instance.
"""
return ContractData(
address=address,
abi=abi,
+ cairo_version=cairo_version,
)
@@ -140,6 +152,7 @@ class DeclareResult(SentTransaction):
"""
_account: BaseAccount = None # pyright: ignore
+ _cairo_version: int = 0
class_hash: int = None # pyright: ignore
"""Class hash of the declared contract."""
@@ -164,6 +177,7 @@ async def deploy(
salt: Optional[int] = None,
unique: bool = True,
constructor_args: Optional[Union[List, Dict]] = None,
+ nonce: Optional[int] = None,
max_fee: Optional[int] = None,
auto_estimate: bool = False,
) -> "DeployResult":
@@ -176,28 +190,46 @@ async def deploy(
:param salt: Optional salt. Random value is selected if it is not provided.
:param unique: Determines if the contract should be salted with the account address.
:param constructor_args: a ``list`` or ``dict`` of arguments for the constructor.
+ :param nonce: Nonce of the transaction with call to deployer.
:param max_fee: Max amount of Wei to be paid when executing transaction.
:param auto_estimate: Use automatic fee estimation (not recommended, as it may lead to high costs).
:return: DeployResult instance.
"""
- # pylint: disable=too-many-arguments
- abi = create_compiled_contract(compiled_contract=self.compiled_contract).abi
+ # pylint: disable=too-many-arguments, too-many-locals
+ if self._cairo_version == 0:
+ abi = create_compiled_contract(compiled_contract=self.compiled_contract).abi
+ else:
+ try:
+ sierra_compiled_contract = create_sierra_compiled_contract(
+ compiled_contract=self.compiled_contract
+ )
+ abi = json.loads(sierra_compiled_contract.abi)
+ except Exception as exc:
+ raise ValueError(
+ "Contract's ABI can't be converted to format List[Dict]. "
+ "Make sure provided compiled_contract is correct."
+ ) from exc
deployer = Deployer(
deployer_address=deployer_address,
account_address=self._account.address if unique else None,
)
deploy_call, address = deployer.create_contract_deployment(
- class_hash=self.class_hash, salt=salt, abi=abi, calldata=constructor_args
+ class_hash=self.class_hash,
+ salt=salt,
+ abi=abi,
+ calldata=constructor_args,
+ cairo_version=self._cairo_version,
)
res = await self._account.execute(
- calls=deploy_call, max_fee=max_fee, auto_estimate=auto_estimate
+ calls=deploy_call, nonce=nonce, max_fee=max_fee, auto_estimate=auto_estimate
)
deployed_contract = Contract(
provider=self._account,
address=address,
abi=abi,
+ cairo_version=self._cairo_version,
)
deploy_result = DeployResult(
@@ -290,12 +322,15 @@ async def invoke(
self,
max_fee: Optional[int] = None,
auto_estimate: bool = False,
+ *,
+ nonce: Optional[int] = None,
) -> InvokeResult:
"""
Invokes a method.
:param max_fee: Max amount of Wei to be paid when executing transaction.
:param auto_estimate: Use automatic fee estimation, not recommend as it may lead to high costs.
+ :param nonce: Nonce of the transaction.
:return: InvokeResult.
"""
if max_fee is not None:
@@ -303,6 +338,7 @@ async def invoke(
transaction = await self._account.sign_invoke_transaction(
calls=self,
+ nonce=nonce,
max_fee=self.max_fee,
auto_estimate=auto_estimate,
)
@@ -322,6 +358,8 @@ async def estimate_fee(
self,
block_hash: Optional[Union[Hash, Tag]] = None,
block_number: Optional[Union[int, Tag]] = None,
+ *,
+ nonce: Optional[int] = None,
) -> EstimatedFee:
"""
Estimate fee for prepared function call.
@@ -329,9 +367,12 @@ async def estimate_fee(
:param block_hash: Estimate fee at specific block hash.
:param block_number: Estimate fee at given block number
(or "latest" / "pending" for the latest / pending block), default is "pending".
+ :param nonce: Nonce of the transaction.
:return: Estimated amount of Wei executing specified transaction will cost.
"""
- tx = await self._account.sign_invoke_transaction(calls=self, max_fee=0)
+ tx = await self._account.sign_invoke_transaction(
+ calls=self, nonce=nonce, max_fee=0
+ )
estimated_fee = await self._client.estimate_fee(
tx=tx,
@@ -352,6 +393,7 @@ def __init__(
contract_data: ContractData,
client: Client,
account: Optional[BaseAccount],
+ cairo_version: int = 0,
):
# pylint: disable=too-many-arguments
self.name = name
@@ -360,8 +402,14 @@ def __init__(
self.contract_data = contract_data
self.client = client
self.account = account
- self._payload_transformer = serializer_for_function(
- contract_data.parsed_abi.functions[name]
+ self._payload_transformer = (
+ serializer_for_function_v1(
+ cast(AbiV1, contract_data.parsed_abi).functions[name]
+ )
+ if cairo_version == 1
+ else serializer_for_function(
+ cast(Abi, contract_data.parsed_abi).functions[name]
+ )
)
def prepare(
@@ -414,6 +462,7 @@ async def invoke(
*args,
max_fee: Optional[int] = None,
auto_estimate: bool = False,
+ nonce: Optional[int] = None,
**kwargs,
) -> InvokeResult:
"""
@@ -422,9 +471,12 @@ async def invoke(
:param max_fee: Max amount of Wei to be paid when executing transaction.
:param auto_estimate: Use automatic fee estimation, not recommend as it may lead to high costs.
+ :param nonce: Nonce of the transaction.
"""
prepared_call = self.prepare(*args, **kwargs)
- return await prepared_call.invoke(max_fee=max_fee, auto_estimate=auto_estimate)
+ return await prepared_call.invoke(
+ max_fee=max_fee, nonce=nonce, auto_estimate=auto_estimate
+ )
@staticmethod
def get_selector(function_name: str):
@@ -449,6 +501,8 @@ def __init__(
address: AddressRepresentation,
abi: list,
provider: Union[BaseAccount, Client],
+ *,
+ cairo_version: int = 0,
):
"""
Should be used instead of ``from_address`` when ABI is known statically.
@@ -458,22 +512,25 @@ def __init__(
:param address: contract's address.
:param abi: contract's abi.
:param provider: BaseAccount or Client used to perform transactions.
+ :param cairo_version: Version of the Cairo in which contract is written.
"""
client, account = _unpack_provider(provider)
self.account: Optional[BaseAccount] = account
self.client: Client = client
- self.data = ContractData.from_abi(parse_address(address), abi)
+ self.data = ContractData.from_abi(parse_address(address), abi, cairo_version)
try:
- self._functions = self._make_functions(self.data, self.client, self.account)
- except ValidationError:
- warnings.warn(
- "Make sure valid ABI is used to create a Contract instance: "
- "Cairo1 contract ABIs are currently unsupported."
+ self._functions = self._make_functions(
+ contract_data=self.data,
+ client=self.client,
+ account=self.account,
+ cairo_version=cairo_version,
)
- # Re-raise the exception
- raise
+ except ValidationError as exc:
+ raise ValueError(
+ "Make sure valid ABI is used to create a Contract instance"
+ ) from exc
@property
def functions(self) -> FunctionsRepository:
@@ -519,22 +576,25 @@ async def from_address(
address = parse_address(address)
proxy_config = Contract._create_proxy_config(proxy_config)
- try:
- abi = await ContractAbiResolver(
- address=address, client=client, proxy_config=proxy_config
- ).resolve()
- except UnsupportedAbiError as err:
- raise ValueError(
- "Provided address of Cairo1 contract which is currently not supported in Contract."
- ) from err
+ abi, cairo_version = await ContractAbiResolver(
+ address=address, client=client, proxy_config=proxy_config
+ ).resolve()
- return Contract(address=address, abi=abi, provider=account or client)
+ return Contract(
+ address=address,
+ abi=abi,
+ provider=account or client,
+ cairo_version=cairo_version,
+ )
@staticmethod
async def declare(
account: BaseAccount,
compiled_contract: str,
*,
+ compiled_contract_casm: Optional[str] = None,
+ casm_class_hash: Optional[int] = None,
+ nonce: Optional[int] = None,
max_fee: Optional[int] = None,
auto_estimate: bool = False,
) -> DeclareResult:
@@ -543,16 +603,43 @@ async def declare(
:param account: BaseAccount used to sign and send declare transaction.
:param compiled_contract: String containing compiled contract.
+ :param compiled_contract_casm: String containing the content of the starknet-sierra-compile (.casm file).
+ Used when declaring Cairo1 contracts.
+ :param casm_class_hash: Hash of the compiled_contract_casm.
+ :param nonce: Nonce of the transaction.
:param max_fee: Max amount of Wei to be paid when executing transaction.
:param auto_estimate: Use automatic fee estimation (not recommended, as it may lead to high costs).
:return: DeclareResult instance.
"""
- declare_tx = await account.sign_declare_transaction(
- compiled_contract=compiled_contract,
- max_fee=max_fee,
- auto_estimate=auto_estimate,
- )
+ if Contract._get_cairo_version(compiled_contract) == 1:
+ if casm_class_hash is None and compiled_contract_casm is None:
+ raise ValueError(
+ "Cairo 1.0 contract was provided without casm_class_hash or compiled_contract_casm argument."
+ )
+
+ cairo_version = 1
+ if casm_class_hash is None:
+ assert compiled_contract_casm is not None
+ casm_class_hash = compute_casm_class_hash(
+ create_casm_class(compiled_contract_casm)
+ )
+
+ declare_tx = await account.sign_declare_v2_transaction(
+ compiled_contract=compiled_contract,
+ compiled_class_hash=casm_class_hash,
+ nonce=nonce,
+ max_fee=max_fee,
+ auto_estimate=auto_estimate,
+ )
+ else:
+ cairo_version = 0
+ declare_tx = await account.sign_declare_transaction(
+ compiled_contract=compiled_contract,
+ nonce=nonce,
+ max_fee=max_fee,
+ auto_estimate=auto_estimate,
+ )
res = await account.client.declare(transaction=declare_tx)
return DeclareResult(
@@ -561,8 +648,13 @@ async def declare(
class_hash=res.class_hash,
_account=account,
compiled_contract=compiled_contract,
+ _cairo_version=cairo_version,
)
+ @staticmethod
+ def _get_cairo_version(compiled_contract: str) -> int:
+ return 1 if "sierra_program" in compiled_contract else 0
+
@staticmethod
async def deploy_contract(
account: BaseAccount,
@@ -571,6 +663,8 @@ async def deploy_contract(
constructor_args: Optional[Union[List, Dict]] = None,
*,
deployer_address: AddressRepresentation = DEFAULT_DEPLOYER_ADDRESS,
+ cairo_version: int = 0,
+ nonce: Optional[int] = None,
max_fee: Optional[int] = None,
auto_estimate: bool = False,
) -> "DeployResult":
@@ -584,6 +678,8 @@ async def deploy_contract(
:param deployer_address: Address of the UDC. Is set to the address of
the default UDC (same address on mainnet/testnet/devnet) by default.
Must be set when using custom network other than ones listed above.
+ :param cairo_version: Version of the Cairo in which contract is written.
+ :param nonce: Nonce of the transaction.
:param max_fee: Max amount of Wei to be paid when executing transaction.
:param auto_estimate: Use automatic fee estimation (not recommended, as it may lead to high costs).
:return: DeployResult instance.
@@ -593,16 +689,17 @@ async def deploy_contract(
deployer_address=deployer_address, account_address=account.address
)
deploy_call, address = deployer.create_contract_deployment(
- class_hash=class_hash, abi=abi, calldata=constructor_args
+ class_hash=class_hash,
+ abi=abi,
+ calldata=constructor_args,
+ cairo_version=cairo_version,
)
res = await account.execute(
- calls=deploy_call, max_fee=max_fee, auto_estimate=auto_estimate
+ calls=deploy_call, nonce=nonce, max_fee=max_fee, auto_estimate=auto_estimate
)
deployed_contract = Contract(
- provider=account,
- address=address,
- abi=abi,
+ provider=account, address=address, abi=abi, cairo_version=cairo_version
)
deploy_result = DeployResult(
hash=res.transaction_hash,
@@ -654,7 +751,11 @@ def compute_contract_hash(compiled_contract: str) -> int:
@classmethod
def _make_functions(
- cls, contract_data: ContractData, client: Client, account: Optional[BaseAccount]
+ cls,
+ contract_data: ContractData,
+ client: Client,
+ account: Optional[BaseAccount],
+ cairo_version: int = 0,
) -> FunctionsRepository:
repository = {}
@@ -669,6 +770,7 @@ def _make_functions(
contract_data=contract_data,
client=client,
account=account,
+ cairo_version=cairo_version,
)
return repository
diff --git a/starknet_py/contract_test.py b/starknet_py/contract_test.py
index 94f75fe82..04ca51597 100644
--- a/starknet_py/contract_test.py
+++ b/starknet_py/contract_test.py
@@ -64,3 +64,21 @@ def test_contract_create_with_client(client):
contract = Contract(address=0x1, abi=[], provider=client)
assert contract.account is None
assert contract.client == client
+
+
+def test_throws_on_wrong_abi(account):
+ with pytest.raises(
+ ValueError, match="Make sure valid ABI is used to create a Contract instance"
+ ):
+ Contract(
+ address=0x1,
+ abi=[
+ {
+ "type": "function",
+ "name": "empty",
+ "inputs": "", # inputs should be a list
+ }
+ ],
+ provider=account,
+ cairo_version=1,
+ )
diff --git a/starknet_py/hash/class_hash.py b/starknet_py/hash/class_hash.py
index 50b35bc00..27050ba87 100644
--- a/starknet_py/hash/class_hash.py
+++ b/starknet_py/hash/class_hash.py
@@ -1,3 +1,4 @@
+import copy
import json
import re
from typing import List
@@ -31,7 +32,7 @@ def compute_class_hash(contract_class: ContractClass) -> int:
]
builtins_hash = compute_hash_on_elements(_encoded_builtins)
- hinted_class_hash = _compute_hinted_class_hash(contract_class)
+ hinted_class_hash = _compute_hinted_class_hash(copy.deepcopy(contract_class))
program_data_hash = compute_hash_on_elements(
[int(data_, 0) for data_ in contract_class.program["data"]]
diff --git a/starknet_py/hash/class_hash_test.py b/starknet_py/hash/class_hash_test.py
index 1da113f80..08a3603de 100644
--- a/starknet_py/hash/class_hash_test.py
+++ b/starknet_py/hash/class_hash_test.py
@@ -1,5 +1,7 @@
# pylint: disable=line-too-long
# fmt: off
+import copy
+
import pytest
from starknet_py.common import create_contract_class
@@ -21,6 +23,8 @@
def test_compute_class_hash(contract_source, expected_class_hash):
compiled_contract = read_contract(contract_source)
contract_class = create_contract_class(compiled_contract)
+ initial_contract_class = copy.deepcopy(contract_class)
class_hash = compute_class_hash(contract_class)
assert class_hash == expected_class_hash
+ assert contract_class == initial_contract_class
diff --git a/starknet_py/hash/transaction.py b/starknet_py/hash/transaction.py
index 143d4d911..d0f572740 100644
--- a/starknet_py/hash/transaction.py
+++ b/starknet_py/hash/transaction.py
@@ -1,5 +1,5 @@
from enum import Enum
-from typing import Sequence
+from typing import Optional, Sequence
from starknet_py.common import int_from_bytes
from starknet_py.constants import DEFAULT_ENTRY_POINT_SELECTOR
@@ -30,7 +30,7 @@ def compute_transaction_hash(
calldata: Sequence[int],
max_fee: int,
chain_id: int,
- additional_data: Sequence[int],
+ additional_data: Optional[Sequence[int]] = None,
) -> int:
"""
Calculates the transaction hash in the Starknet network - a unique identifier of the
@@ -59,6 +59,8 @@ def compute_transaction_hash(
:param additional_data: Additional data, required for some transactions (e.g. DeployAccount, Declare).
:return: Hash of the transaction.
"""
+ if additional_data is None:
+ additional_data = []
calldata_hash = compute_hash_on_elements(data=calldata)
data_to_hash = [
tx_hash_prefix.value,
@@ -76,6 +78,38 @@ def compute_transaction_hash(
)
+def compute_invoke_transaction_hash(
+ *,
+ version: int,
+ sender_address: int,
+ calldata: Sequence[int],
+ max_fee: int,
+ chain_id: int,
+ nonce: int,
+) -> int:
+ """
+ Computes hash of the Invoke transaction.
+
+ :param version: The transaction's version.
+ :param sender_address: Sender address.
+ :param calldata: Calldata of the function.
+ :param max_fee: The transaction's maximum fee.
+ :param chain_id: The network's chain ID.
+ :param nonce: Nonce of the transaction.
+ :return: Hash of the transaction.
+ """
+ return compute_transaction_hash(
+ tx_hash_prefix=TransactionHashPrefix.INVOKE,
+ version=version,
+ contract_address=sender_address,
+ entry_point_selector=DEFAULT_ENTRY_POINT_SELECTOR,
+ calldata=calldata,
+ max_fee=max_fee,
+ chain_id=chain_id,
+ additional_data=[nonce],
+ )
+
+
def compute_deploy_account_transaction_hash(
version: int,
contract_address: int,
@@ -146,7 +180,8 @@ def compute_declare_transaction_hash(
def compute_declare_v2_transaction_hash(
*,
- contract_class: SierraContractClass,
+ contract_class: Optional[SierraContractClass] = None,
+ class_hash: Optional[int] = None,
compiled_class_hash: int,
chain_id: int,
sender_address: int,
@@ -158,6 +193,7 @@ def compute_declare_v2_transaction_hash(
Computes class hash of declare transaction version 2.
:param contract_class: SierraContractClass of the contract.
+ :param class_hash: Class hash of the contract.
:param compiled_class_hash: compiled class hash of the program.
:param chain_id: The network's chain ID.
:param sender_address: Address which sends the transaction.
@@ -166,7 +202,14 @@ def compute_declare_v2_transaction_hash(
:param nonce: Nonce of the transaction.
:return: Hash of the transaction.
"""
- class_hash = compute_sierra_class_hash(contract_class)
+ if contract_class is None and class_hash is None:
+ raise ValueError("Either contract_class or class_hash is required.")
+ if contract_class is not None and class_hash is not None:
+ raise ValueError("Both contract_class and class_hash passed.")
+
+ if class_hash is None:
+ assert contract_class is not None
+ class_hash = compute_sierra_class_hash(contract_class)
return compute_transaction_hash(
tx_hash_prefix=TransactionHashPrefix.DECLARE,
diff --git a/starknet_py/hash/transaction_test.py b/starknet_py/hash/transaction_test.py
index 1bb844356..680196935 100644
--- a/starknet_py/hash/transaction_test.py
+++ b/starknet_py/hash/transaction_test.py
@@ -1,25 +1,20 @@
import pytest
-from starknet_py.common import (
- create_casm_class,
- create_compiled_contract,
- create_sierra_compiled_contract,
-)
-from starknet_py.hash.casm_class_hash import compute_casm_class_hash
+from starknet_py.common import create_compiled_contract
from starknet_py.hash.transaction import (
TransactionHashPrefix,
compute_declare_transaction_hash,
compute_declare_v2_transaction_hash,
compute_deploy_account_transaction_hash,
+ compute_invoke_transaction_hash,
compute_transaction_hash,
)
from starknet_py.net.models import StarknetChainId
-from starknet_py.tests.e2e.fixtures.constants import CONTRACTS_COMPILED_V1_DIR
from starknet_py.tests.e2e.fixtures.misc import read_contract
@pytest.mark.parametrize(
- "data, calculated_hash",
+ "data, expected_hash",
(
(
[TransactionHashPrefix.INVOKE, 2, 3, 4, [5], 6, 7, [8]],
@@ -29,27 +24,44 @@
[TransactionHashPrefix.L1_HANDLER, 15, 39, 74, [74], 39, 15, [28]],
1226653506056503634668815848352741482067480791322607584496401451909331743178,
),
+ (
+ [
+ TransactionHashPrefix.INVOKE,
+ 0x0,
+ 0x2A,
+ 0x64,
+ [],
+ 0x0,
+ StarknetChainId.TESTNET.value,
+ ],
+ 0x7D260744DE9D8C55E7675A34512D1951A7B262C79E685D26599EDD2948DE959,
+ ),
),
)
-def test_compute_transaction_hash(data, calculated_hash):
- assert compute_transaction_hash(*data) == calculated_hash
+def test_compute_transaction_hash(data, expected_hash):
+ assert compute_transaction_hash(*data) == expected_hash
@pytest.mark.parametrize(
- "data, calculated_hash",
+ "data, expected_hash",
(
(
- [2, 3, 4, [5], 6, 7, 8, 9],
- 3319639522829811634906602140344071246050815451799261765214603967516640029516,
- ),
- (
- [12, 23, 34, [45], 56, 67, 78, 89],
- 1704331554042454954615983716430494560849200211800542196314933915246556687567,
+ {
+ "contract_address": 2,
+ "class_hash": 3,
+ "constructor_calldata": [4],
+ "salt": 5,
+ "version": 6,
+ "max_fee": 7,
+ "chain_id": 8,
+ "nonce": 9,
+ },
+ 0x6199E956E541CBB06589C4A63C2578A8ED6B697C0FA35B002F48923DFE648EE,
),
),
)
-def test_compute_deploy_account_transaction_hash(data, calculated_hash):
- assert compute_deploy_account_transaction_hash(*data) == calculated_hash
+def test_compute_deploy_account_transaction_hash(data, expected_hash):
+ assert compute_deploy_account_transaction_hash(**data) == expected_hash
@pytest.mark.parametrize(
@@ -69,29 +81,41 @@ def test_compute_declare_transaction_hash(contract_json, data):
@pytest.mark.parametrize(
- "sierra_contract_class_source",
- ["account_compiled", "erc20_compiled", "minimal_contract_compiled"],
+ "data, expected_hash",
+ (
+ (
+ {
+ "class_hash": 2,
+ "sender_address": 3,
+ "version": 4,
+ "max_fee": 5,
+ "chain_id": 6,
+ "nonce": 7,
+ "compiled_class_hash": 8,
+ },
+ 0x67EA411072DD2EF3BA36D9680F040A02E599F80F4770E204ECBB2C47C226793,
+ ),
+ ),
)
-def test_compute_declare_v2_transaction_hash(sierra_contract_class_source):
- compiled_contract = read_contract(
- f"{sierra_contract_class_source}.json", directory=CONTRACTS_COMPILED_V1_DIR
- )
- compiled_contract_casm = read_contract(
- f"{sierra_contract_class_source}.casm", directory=CONTRACTS_COMPILED_V1_DIR
- )
- casm_class = create_casm_class(compiled_contract_casm)
- casm_class_hash = compute_casm_class_hash(casm_class)
-
- compiled_contract = create_sierra_compiled_contract(compiled_contract)
+def test_compute_declare_v2_transaction_hash(data, expected_hash):
+ assert compute_declare_v2_transaction_hash(**data) == expected_hash
- declare_v2_hash = compute_declare_v2_transaction_hash(
- contract_class=compiled_contract,
- compiled_class_hash=casm_class_hash,
- chain_id=StarknetChainId.TESTNET.value,
- sender_address=0x1,
- max_fee=2000,
- version=2,
- nonce=23,
- )
- assert declare_v2_hash > 0
+@pytest.mark.parametrize(
+ "data, expected_hash",
+ (
+ (
+ {
+ "sender_address": 3,
+ "version": 4,
+ "calldata": [5],
+ "max_fee": 6,
+ "chain_id": 7,
+ "nonce": 8,
+ },
+ 0x505BBF7CD810531C53526631078DAA314BFD036C80C7C6E3A02C608DB8E31DE,
+ ),
+ ),
+)
+def test_compute_invoke_transaction_hash(data, expected_hash):
+ assert compute_invoke_transaction_hash(**data) == expected_hash
diff --git a/starknet_py/net/account/account.py b/starknet_py/net/account/account.py
index d94e21669..dd5b668f5 100644
--- a/starknet_py/net/account/account.py
+++ b/starknet_py/net/account/account.py
@@ -133,6 +133,8 @@ async def _get_max_fee(
async def _prepare_invoke(
self,
calls: Calls,
+ *,
+ nonce: Optional[int] = None,
max_fee: Optional[int] = None,
auto_estimate: bool = False,
) -> Invoke:
@@ -144,7 +146,8 @@ async def _prepare_invoke(
:param auto_estimate: Use automatic fee estimation, not recommend as it may lead to high costs.
:return: Invoke created from the calls (without the signature).
"""
- nonce = await self.get_nonce()
+ if nonce is None:
+ nonce = await self.get_nonce()
call_descriptions, calldata = _merge_calls(ensure_iterable(calls))
wrapped_calldata = _execute_payload_serializer.serialize(
@@ -187,20 +190,30 @@ async def _estimate_fee(
return estimated_fee
- async def get_nonce(self) -> int:
+ async def get_nonce(
+ self,
+ *,
+ block_hash: Optional[Union[Hash, Tag]] = None,
+ block_number: Optional[Union[int, Tag]] = None,
+ ) -> int:
"""
Get the current nonce of the account.
+ :param block_hash: Block's hash or literals `"pending"` or `"latest"`
+ :param block_number: Block's number or literals `"pending"` or `"latest"`
:return: nonce.
"""
return await self._client.get_contract_nonce(
- self.address, block_number="pending"
+ self.address, block_hash=block_hash, block_number=block_number
)
async def get_balance(
self,
token_address: Optional[AddressRepresentation] = None,
chain_id: Optional[StarknetChainId] = None,
+ *,
+ block_hash: Optional[Union[Hash, Tag]] = None,
+ block_number: Optional[Union[int, Tag]] = None,
) -> int:
if token_address is None:
token_address = self._default_token_address_for_chain(chain_id)
@@ -211,7 +224,8 @@ async def get_balance(
selector=get_selector_from_name("balanceOf"),
calldata=[self.address],
),
- block_hash="pending",
+ block_hash=block_hash,
+ block_number=block_number,
)
return (high << 128) + low
@@ -229,10 +243,13 @@ async def sign_invoke_transaction(
self,
calls: Calls,
*,
+ nonce: Optional[int] = None,
max_fee: Optional[int] = None,
auto_estimate: bool = False,
) -> Invoke:
- execute_tx = await self._prepare_invoke(calls, max_fee, auto_estimate)
+ execute_tx = await self._prepare_invoke(
+ calls, nonce=nonce, max_fee=max_fee, auto_estimate=auto_estimate
+ )
signature = self.signer.sign_transaction(execute_tx)
return _add_signature_to_transaction(execute_tx, signature)
@@ -240,6 +257,7 @@ async def sign_declare_transaction(
self,
compiled_contract: str,
*,
+ nonce: Optional[int] = None,
max_fee: Optional[int] = None,
auto_estimate: bool = False,
) -> Declare:
@@ -248,7 +266,9 @@ async def sign_declare_transaction(
"Signing sierra contracts requires using `sign_declare_v2_transaction` method."
)
- declare_tx = await self._make_declare_transaction(compiled_contract)
+ declare_tx = await self._make_declare_transaction(
+ compiled_contract, nonce=nonce
+ )
max_fee = await self._get_max_fee(
transaction=declare_tx, max_fee=max_fee, auto_estimate=auto_estimate
@@ -262,11 +282,12 @@ async def sign_declare_v2_transaction(
compiled_contract: str,
compiled_class_hash: int,
*,
+ nonce: Optional[int] = None,
max_fee: Optional[int] = None,
auto_estimate: bool = False,
) -> DeclareV2:
declare_tx = await self._make_declare_v2_transaction(
- compiled_contract, compiled_class_hash
+ compiled_contract, compiled_class_hash, nonce=nonce
)
max_fee = await self._get_max_fee(
transaction=declare_tx, max_fee=max_fee, auto_estimate=auto_estimate
@@ -275,31 +296,45 @@ async def sign_declare_v2_transaction(
signature = self.signer.sign_transaction(declare_tx)
return _add_signature_to_transaction(declare_tx, signature)
- async def _make_declare_transaction(self, compiled_contract: str) -> Declare:
+ async def _make_declare_transaction(
+ self, compiled_contract: str, *, nonce: Optional[int] = None
+ ) -> Declare:
contract_class = create_compiled_contract(compiled_contract=compiled_contract)
+
+ if nonce is None:
+ nonce = await self.get_nonce()
+
declare_tx = Declare(
contract_class=contract_class,
sender_address=self.address,
max_fee=0,
signature=[],
- nonce=await self.get_nonce(),
+ nonce=nonce,
version=1,
)
return declare_tx
async def _make_declare_v2_transaction(
- self, compiled_contract: str, compiled_class_hash: int
+ self,
+ compiled_contract: str,
+ compiled_class_hash: int,
+ *,
+ nonce: Optional[int] = None,
) -> DeclareV2:
contract_class = create_sierra_compiled_contract(
compiled_contract=compiled_contract
)
+
+ if nonce is None:
+ nonce = await self.get_nonce()
+
declare_tx = DeclareV2(
contract_class=contract_class,
compiled_class_hash=compiled_class_hash,
sender_address=self.address,
max_fee=0,
signature=[],
- nonce=await self.get_nonce(),
+ nonce=nonce,
version=2,
)
return declare_tx
@@ -310,6 +345,7 @@ async def sign_deploy_account_transaction(
contract_address_salt: int,
constructor_calldata: Optional[List[int]] = None,
*,
+ nonce: int = 0,
max_fee: Optional[int] = None,
auto_estimate: bool = False,
) -> DeployAccount:
@@ -322,7 +358,7 @@ async def sign_deploy_account_transaction(
version=1,
max_fee=0,
signature=[],
- nonce=0,
+ nonce=nonce,
)
max_fee = await self._get_max_fee(
@@ -336,11 +372,12 @@ async def execute(
self,
calls: Calls,
*,
+ nonce: Optional[int] = None,
max_fee: Optional[int] = None,
auto_estimate: bool = False,
) -> SentTransactionResponse:
execute_transaction = await self.sign_invoke_transaction(
- calls, max_fee=max_fee, auto_estimate=auto_estimate
+ calls, nonce=nonce, max_fee=max_fee, auto_estimate=auto_estimate
)
return await self._client.send_transaction(execute_transaction)
@@ -363,9 +400,11 @@ async def deploy_account(
client: Client,
chain: StarknetChainId,
constructor_calldata: Optional[List[int]] = None,
+ nonce: int = 0,
max_fee: Optional[int] = None,
auto_estimate: bool = False,
) -> AccountDeploymentResult:
+ # pylint: disable=too-many-locals
"""
Deploys an account contract with provided class_hash on Starknet and returns
an AccountDeploymentResult that allows waiting for transaction acceptance.
@@ -383,6 +422,7 @@ async def deploy_account(
:param chain: id of the Starknet chain used.
:param constructor_calldata: optional calldata to account contract constructor. If ``None`` is passed,
``[key_pair.public_key]`` will be used as calldata.
+ :param nonce: Nonce of the transaction.
:param max_fee: max fee to be paid for deployment, must be less or equal to the amount of tokens prefunded.
:param auto_estimate: Use automatic fee estimation, not recommend as it may lead to high costs.
"""
@@ -414,6 +454,7 @@ async def deploy_account(
class_hash=class_hash,
contract_address_salt=salt,
constructor_calldata=calldata,
+ nonce=nonce,
max_fee=max_fee,
auto_estimate=auto_estimate,
)
diff --git a/starknet_py/net/account/account_test.py b/starknet_py/net/account/account_test.py
index e014e87a7..ddee7e092 100644
--- a/starknet_py/net/account/account_test.py
+++ b/starknet_py/net/account/account_test.py
@@ -9,6 +9,7 @@
from starknet_py.net.models import StarknetChainId, parse_address
from starknet_py.net.networks import MAINNET, TESTNET, TESTNET2
from starknet_py.net.signer.stark_curve_signer import KeyPair, StarkCurveSigner
+from starknet_py.tests.e2e.fixtures.constants import MAX_FEE
@pytest.mark.asyncio
@@ -48,6 +49,21 @@ async def test_get_balance_default_token_address(net, call_contract):
assert call.to_addr == parse_address(FEE_CONTRACT_ADDRESS)
+@pytest.mark.asyncio
+async def test_account_get_balance(account, map_contract):
+ balance = await account.get_balance()
+ block = await account.client.get_block()
+
+ await map_contract.functions["put"].invoke(key=10, value=10, max_fee=MAX_FEE)
+
+ new_balance = await account.get_balance()
+ old_balance = await account.get_balance(block_number=block.block_number)
+
+ assert balance > 0
+ assert new_balance < balance
+ assert old_balance == balance
+
+
def test_create_account():
key_pair = KeyPair.from_private_key(0x111)
account = Account(
diff --git a/starknet_py/net/account/base_account.py b/starknet_py/net/account/base_account.py
index 7fe5df5db..a75ba50a6 100644
--- a/starknet_py/net/account/base_account.py
+++ b/starknet_py/net/account/base_account.py
@@ -1,8 +1,8 @@
from abc import ABC, abstractmethod
-from typing import List, Optional
+from typing import List, Optional, Union
from starknet_py.net.client import Client
-from starknet_py.net.client_models import Calls, SentTransactionResponse
+from starknet_py.net.client_models import Calls, Hash, SentTransactionResponse, Tag
from starknet_py.net.models import AddressRepresentation, StarknetChainId
from starknet_py.net.models.transaction import (
Declare,
@@ -46,10 +46,17 @@ def supported_transaction_version(self) -> int:
"""
@abstractmethod
- async def get_nonce(self) -> int:
+ async def get_nonce(
+ self,
+ *,
+ block_hash: Optional[Union[Hash, Tag]] = None,
+ block_number: Optional[Union[int, Tag]] = None,
+ ) -> int:
"""
Get the current nonce of the account.
+ :param block_hash: Block's hash or literals `"pending"` or `"latest"`
+ :param block_number: Block's number or literals `"pending"` or `"latest"`
:return: nonce of the account.
"""
@@ -58,6 +65,9 @@ async def get_balance(
self,
token_address: Optional[AddressRepresentation] = None,
chain_id: Optional[StarknetChainId] = None,
+ *,
+ block_hash: Optional[Union[Hash, Tag]] = None,
+ block_number: Optional[Union[int, Tag]] = None,
) -> int:
"""
Checks account's balance of specified token.
@@ -66,6 +76,8 @@ async def get_balance(
:param chain_id: Identifier of the Starknet chain used.
If token_address is not specified it will be used to determine network's payment token address.
If token_address is provided, chain_id will be ignored.
+ :param block_hash: Block's hash or literals `"pending"` or `"latest"`
+ :param block_number: Block's number or literals `"pending"` or `"latest"`
:return: Token balance.
"""
@@ -87,6 +99,7 @@ async def sign_invoke_transaction(
self,
calls: Calls,
*,
+ nonce: Optional[int] = None,
max_fee: Optional[int] = None,
auto_estimate: bool = False,
) -> Invoke:
@@ -94,6 +107,7 @@ async def sign_invoke_transaction(
Takes calls and creates signed Invoke.
:param calls: Single call or list of calls.
+ :param nonce: Nonce of the transaction.
:param max_fee: Max amount of Wei to be paid when executing transaction.
:param auto_estimate: Use automatic fee estimation, not recommend as it may lead to high costs.
:return: Invoke created from the calls.
@@ -104,6 +118,7 @@ async def sign_declare_transaction(
self,
compiled_contract: str,
*,
+ nonce: Optional[int] = None,
max_fee: Optional[int] = None,
auto_estimate: bool = False,
) -> Declare:
@@ -111,6 +126,7 @@ async def sign_declare_transaction(
Create and sign declare transaction.
:param compiled_contract: string containing a compiled Starknet contract. Supports old contracts.
+ :param nonce: Nonce of the transaction.
:param max_fee: Max amount of Wei to be paid when executing transaction.
:param auto_estimate: Use automatic fee estimation, not recommend as it may lead to high costs.
:return: Signed Declare transaction.
@@ -122,6 +138,7 @@ async def sign_declare_v2_transaction(
compiled_contract: str,
compiled_class_hash: int,
*,
+ nonce: Optional[int] = None,
max_fee: Optional[int] = None,
auto_estimate: bool = False,
) -> DeclareV2:
@@ -132,6 +149,7 @@ async def sign_declare_v2_transaction(
Supports new contracts (compiled to sierra).
:param compiled_class_hash: a class hash of the sierra compiled contract used in the declare transaction.
Computed from casm compiled contract.
+ :param nonce: Nonce of the transaction.
:param max_fee: Max amount of Wei to be paid when executing transaction.
:param auto_estimate: Use automatic fee estimation, not recommend as it may lead to high costs.
:return: Signed DeclareV2 transaction.
@@ -144,6 +162,7 @@ async def sign_deploy_account_transaction(
contract_address_salt: int,
constructor_calldata: Optional[List[int]] = None,
*,
+ nonce: Optional[int] = None,
max_fee: Optional[int] = None,
auto_estimate: bool = False,
) -> DeployAccount:
@@ -154,6 +173,7 @@ async def sign_deploy_account_transaction(
:param contract_address_salt: A salt used to calculate deployed contract address.
:param constructor_calldata: Calldata to be ed to contract constructor
and used to calculate deployed contract address.
+ :param nonce: Nonce of the transaction.
:param max_fee: Max fee to be paid for deploying account transaction. Enough tokens must be prefunded before
sending the transaction for it to succeed.
:param auto_estimate: Use automatic fee estimation, not recommend as it may lead to high costs.
@@ -165,6 +185,7 @@ async def execute(
self,
calls: Calls,
*,
+ nonce: Optional[int] = None,
max_fee: Optional[int] = None,
auto_estimate: bool = False,
) -> SentTransactionResponse:
@@ -172,6 +193,7 @@ async def execute(
Takes calls and executes transaction.
:param calls: Single call or list of calls.
+ :param nonce: Nonce of the transaction.
:param max_fee: Max amount of Wei to be paid when executing transaction.
:param auto_estimate: Use automatic fee estimation, not recommend as it may lead to high costs.
:return: SentTransactionResponse.
diff --git a/starknet_py/net/client.py b/starknet_py/net/client.py
index 9fe8b0505..134124652 100644
--- a/starknet_py/net/client.py
+++ b/starknet_py/net/client.py
@@ -4,6 +4,7 @@
from abc import ABC, abstractmethod
from typing import List, Optional, Tuple, Union
+from starknet_py.net.client_errors import ClientError
from starknet_py.net.client_models import (
BlockStateUpdate,
BlockTransactionTraces,
@@ -139,12 +140,13 @@ async def wait_for_tx(
) -> Tuple[int, TransactionStatus]:
# pylint: disable=too-many-branches
"""
- Awaits for transaction to get accepted or at least pending by polling its status
+ Awaits for transaction to get accepted or at least pending by polling its status.
- :param tx_hash: Transaction's hash
- :param wait_for_accept: If true waits for at least ACCEPTED_ON_L2 status, otherwise waits for at least PENDING
- :param check_interval: Defines interval between checks
- :return: Tuple containing block number and transaction status
+ :param tx_hash: Transaction's hash.
+ :param wait_for_accept: If true waits for at least ACCEPTED_ON_L2 status, otherwise waits for at least PENDING.
+ Defaults to false
+ :param check_interval: Defines interval between checks.
+ :return: Tuple containing block number and transaction status.
"""
if check_interval <= 0:
raise ValueError("Argument check_interval has to be greater than 0.")
@@ -182,6 +184,12 @@ async def wait_for_tx(
await asyncio.sleep(check_interval)
except asyncio.CancelledError as exc:
raise TransactionNotReceivedError from exc
+ except ClientError as exc:
+ if "Transaction hash not found" in exc.message:
+ raise ClientError(
+ "Nodes can't access pending transactions, try using parameter 'wait_for_accept=True'."
+ ) from exc
+ raise exc
@abstractmethod
async def estimate_fee(
diff --git a/starknet_py/net/client_models.py b/starknet_py/net/client_models.py
index bc8fee608..0a3c0a03e 100644
--- a/starknet_py/net/client_models.py
+++ b/starknet_py/net/client_models.py
@@ -68,6 +68,7 @@ class L2toL1Message:
Dataclass representing a L2->L1 message.
"""
+ from_address: int
payload: List[int]
l1_address: int
l2_address: Optional[int] = None
@@ -183,6 +184,8 @@ class TransactionReceipt:
hash: int
status: TransactionStatus
+ type: Optional[TransactionType] = None
+ contract_address: Optional[int] = None
block_number: Optional[int] = None
block_hash: Optional[int] = None
actual_fee: int = 0
diff --git a/starknet_py/net/full_node_client.py b/starknet_py/net/full_node_client.py
index ab5796d9b..8dd3868cb 100644
--- a/starknet_py/net/full_node_client.py
+++ b/starknet_py/net/full_node_client.py
@@ -113,8 +113,8 @@ async def get_block_traces(
# TODO (#809): add tests with multiple emitted keys
async def get_events(
self,
- address: Hash,
- keys: List[List[str]],
+ address: Optional[Hash] = None,
+ keys: Optional[List[List[Hash]]] = None,
*,
from_block_number: Optional[Union[int, Tag]] = None,
from_block_hash: Optional[Union[Hash, Tag]] = None,
@@ -132,12 +132,16 @@ async def get_events(
the first key, any value for their second key and 3 for their third key.
:param from_block_number: Number of the block from which events searched for **starts**
or literals `"pending"` or `"latest"`. Mutually exclusive with ``from_block_hash`` parameter.
+ If not provided, query starts from block 0.
:param from_block_hash: Hash of the block from which events searched for **starts**
or literals `"pending"` or `"latest"`. Mutually exclusive with ``from_block_number`` parameter.
+ If not provided, query starts from block 0.
:param to_block_number: Number of the block to which events searched for **end**
or literals `"pending"` or `"latest"`. Mutually exclusive with ``to_block_hash`` parameter.
+ If not provided, query ends at block `"pending"`.
:param to_block_hash: Hash of the block to which events searched for **end**
or literals `"pending"` or `"latest"`. Mutually exclusive with ``to_block_number`` parameter.
+ If not provided, query ends at block `"pending"`.
:param follow_continuation_token: Flag deciding whether all events should be collected during one function call,
defaults to False.
:param continuation_token: Continuation token from which the returned events start.
@@ -149,9 +153,16 @@ async def get_events(
if chunk_size <= 0:
raise ValueError("Argument chunk_size must be greater than 0.")
+ if keys is None:
+ keys = []
+ if address is not None:
+ address = _to_rpc_felt(address)
+ if from_block_number is None and from_block_hash is None:
+ from_block_number = 0
+
from_block = _get_raw_block_identifier(from_block_hash, from_block_number)
to_block = _get_raw_block_identifier(to_block_hash, to_block_number)
- address = _to_rpc_felt(address)
+ keys = [[_to_rpc_felt(key) for key in inner_list] for inner_list in keys]
events_list = []
while True:
@@ -180,9 +191,9 @@ async def _get_events_chunk(
self,
from_block: Union[dict, Hash, Tag, None],
to_block: Union[dict, Hash, Tag, None],
- address: Hash,
- keys: List[List[str]],
+ keys: List[List[Hash]],
chunk_size: int,
+ address: Optional[Hash] = None,
continuation_token: Optional[str] = None,
) -> Tuple[list, Optional[str]]:
# pylint: disable=too-many-arguments
@@ -190,11 +201,12 @@ async def _get_events_chunk(
"chunk_size": chunk_size,
"from_block": from_block,
"to_block": to_block,
- "address": address,
"keys": keys,
}
if continuation_token is not None:
params["continuation_token"] = continuation_token
+ if address is not None:
+ params["address"] = address
res = await self._client.call(
method_name="getEvents",
diff --git a/starknet_py/net/models/__init__.py b/starknet_py/net/models/__init__.py
index 4b78d8f5c..736b9b9a8 100644
--- a/starknet_py/net/models/__init__.py
+++ b/starknet_py/net/models/__init__.py
@@ -7,5 +7,4 @@
DeployAccount,
Invoke,
Transaction,
- compute_invoke_hash,
)
diff --git a/starknet_py/net/models/transaction.py b/starknet_py/net/models/transaction.py
index 90e6ca271..81884d8a3 100644
--- a/starknet_py/net/models/transaction.py
+++ b/starknet_py/net/models/transaction.py
@@ -7,24 +7,20 @@
import base64
import gzip
import json
-import warnings
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
-from typing import Any, Dict, List, Sequence, TypeVar, Union
+from typing import Any, Dict, List, TypeVar
import marshmallow
import marshmallow_dataclass
from marshmallow import fields
-from starknet_py.constants import DEFAULT_ENTRY_POINT_SELECTOR
from starknet_py.hash.address import compute_address
-from starknet_py.hash.selector import get_selector_from_name
from starknet_py.hash.transaction import (
- TransactionHashPrefix,
compute_declare_transaction_hash,
compute_declare_v2_transaction_hash,
compute_deploy_account_transaction_hash,
- compute_transaction_hash,
+ compute_invoke_transaction_hash,
)
from starknet_py.net.client_models import (
ContractClass,
@@ -213,15 +209,13 @@ def calculate_hash(self, chain_id: StarknetChainId) -> int:
"""
Calculates the transaction hash in the Starknet network.
"""
- return compute_transaction_hash(
- tx_hash_prefix=TransactionHashPrefix.INVOKE,
+ return compute_invoke_transaction_hash(
version=self.version,
- contract_address=self.sender_address,
- entry_point_selector=DEFAULT_ENTRY_POINT_SELECTOR,
+ sender_address=self.sender_address,
calldata=self.calldata,
max_fee=self.max_fee,
chain_id=chain_id.value,
- additional_data=[self.nonce],
+ nonce=self.nonce,
)
@@ -231,51 +225,6 @@ def calculate_hash(self, chain_id: StarknetChainId) -> int:
DeployAccountSchema = marshmallow_dataclass.class_schema(DeployAccount)
-def compute_invoke_hash(
- sender_address: int,
- entry_point_selector: Union[int, str],
- calldata: Sequence[int],
- chain_id: StarknetChainId,
- max_fee: int,
- version: int,
-) -> int:
- # pylint: disable=too-many-arguments
- """
- Computes invocation hash.
-
- .. deprecated:: 0.15.0
- To compute hash of an invoke transaction use
- :py:meth:`~starknet_py.hash.transaction.compute_transaction_hash`.
-
- :param sender_address: int
- :param entry_point_selector: Union[int, str]
- :param calldata: Sequence[int]
- :param chain_id: StarknetChainId
- :param max_fee: Max fee
- :param version: Contract version
- :return: calculated hash
- """
- warnings.warn(
- "Function compute_invoke_hash is deprecated."
- "To compute hash of an invoke transaction use compute_transaction_hash.",
- category=DeprecationWarning,
- )
-
- if isinstance(entry_point_selector, str):
- entry_point_selector = get_selector_from_name(entry_point_selector)
-
- return compute_transaction_hash(
- tx_hash_prefix=TransactionHashPrefix.INVOKE,
- contract_address=sender_address,
- entry_point_selector=entry_point_selector,
- calldata=calldata,
- chain_id=chain_id.value,
- additional_data=[],
- max_fee=max_fee,
- version=version,
- )
-
-
def compress_program(data: dict, program_name: str = "program") -> dict:
program = data["contract_class"][program_name]
compressed_program = json.dumps(program)
diff --git a/starknet_py/net/models/transaction_test.py b/starknet_py/net/models/transaction_test.py
index 03997cf70..982c3107e 100644
--- a/starknet_py/net/models/transaction_test.py
+++ b/starknet_py/net/models/transaction_test.py
@@ -2,46 +2,18 @@
import typing
from typing import cast
-import pytest
-
-from starknet_py.common import (
- create_compiled_contract,
- create_contract_class,
- create_sierra_compiled_contract,
-)
+from starknet_py.common import create_contract_class
from starknet_py.net.client_models import TransactionType
-from starknet_py.net.models import StarknetChainId
from starknet_py.net.models.transaction import (
Declare,
DeclareSchema,
- DeclareV2,
- DeployAccount,
Invoke,
InvokeSchema,
- compute_invoke_hash,
)
from starknet_py.tests.e2e.fixtures.constants import CONTRACTS_COMPILED_V1_DIR
from starknet_py.tests.e2e.fixtures.misc import read_contract
-def test_invoke_hash():
- for selector in [
- "increase_balance",
- 1530486729947006463063166157847785599120665941190480211966374137237989315360,
- ]:
- assert (
- compute_invoke_hash(
- entry_point_selector=selector,
- sender_address=0x03606DB92E563E41F4A590BC01C243E8178E9BA8C980F8E464579F862DA3537C,
- calldata=[1234],
- chain_id=StarknetChainId.TESTNET,
- version=0,
- max_fee=0,
- )
- == 0xD0A52D6E77B836613B9F709AD7F4A88297697FEFBEF1ADA3C59692FF46702C
- )
-
-
def test_declare_compress_program(balance_contract):
contract_class = create_contract_class(balance_contract)
declare_transaction = Declare(
@@ -73,67 +45,6 @@ def test_declare_compress_program(balance_contract):
)
-@pytest.mark.parametrize(
- "transaction, calculated_hash",
- [
- (
- Invoke(
- sender_address=0x1,
- calldata=[1, 2, 3],
- max_fee=10000,
- signature=[],
- nonce=23,
- version=1,
- ),
- 3484767022419258107070028252604380065385354331198975073942248877262069264133,
- ),
- (
- DeployAccount(
- class_hash=0x1,
- contract_address_salt=0x2,
- constructor_calldata=[1, 2, 3, 4],
- max_fee=10000,
- signature=[],
- nonce=23,
- version=1,
- ),
- 1258460340144554539989794559757396219553018532617589681714052999991876798273,
- ),
- (
- Declare(
- contract_class=create_compiled_contract(
- compiled_contract=compiled_contract
- ),
- sender_address=123,
- max_fee=10000,
- signature=[],
- nonce=23,
- version=1,
- ),
- 548241482519463597399416578757678814995754071952538857702978733086902207659,
- ),
- (
- DeclareV2(
- contract_class=create_sierra_compiled_contract(
- compiled_contract=sierra_compiled_contract
- ),
- compiled_class_hash=0x1,
- max_fee=1000,
- nonce=20,
- sender_address=0x1234,
- signature=[0x1, 0x2],
- version=2,
- ),
- 2391287073123315831211443928603796208441862227055564920937005298570351208379,
- ),
- ],
-)
-def test_calculate_transaction_hash(transaction, calculated_hash):
- assert (
- transaction.calculate_hash(chain_id=StarknetChainId.TESTNET) == calculated_hash
- )
-
-
def test_serialize_deserialize_invoke():
data = {
"sender_address": "0x1",
diff --git a/starknet_py/net/schemas/rpc.py b/starknet_py/net/schemas/rpc.py
index b2723cf17..3a7cb43fc 100644
--- a/starknet_py/net/schemas/rpc.py
+++ b/starknet_py/net/schemas/rpc.py
@@ -39,6 +39,7 @@
NonPrefixedHex,
StatusField,
StorageEntrySchema,
+ TransactionTypeField,
)
from starknet_py.net.schemas.utils import (
_replace_invoke_contract_address_with_sender_address,
@@ -104,6 +105,8 @@ class TransactionReceiptSchema(Schema):
block_number = fields.Integer(data_key="block_number", load_default=None)
block_hash = Felt(data_key="block_hash", load_default=None)
actual_fee = Felt(data_key="actual_fee", required=True)
+ type = TransactionTypeField(data_key="type", load_default=None)
+ contract_address = Felt(data_key="contract_address", load_default=None)
rejection_reason = fields.String(data_key="status_data", load_default=None)
events = fields.List(
fields.Nested(EventSchema()), data_key="events", load_default=[]
@@ -154,6 +157,7 @@ class DeclareTransactionSchema(TransactionSchema):
class_hash = Felt(data_key="class_hash", required=True)
sender_address = Felt(data_key="sender_address", required=True)
nonce = Felt(data_key="nonce", load_default=None)
+ compiled_class_hash = Felt(data_key="compiled_class_hash", load_default=None)
@post_load
def make_dataclass(self, data, **kwargs) -> DeclareTransaction:
diff --git a/starknet_py/net/udc_deployer/deployer.py b/starknet_py/net/udc_deployer/deployer.py
index ff54c5b3f..805a15eeb 100644
--- a/starknet_py/net/udc_deployer/deployer.py
+++ b/starknet_py/net/udc_deployer/deployer.py
@@ -86,6 +86,7 @@ def create_contract_deployment(
*,
salt: Optional[int] = None,
abi: Optional[List] = None,
+ cairo_version: int = 0,
calldata: Optional[Union[List, dict]] = None,
) -> ContractDeployment:
"""
@@ -94,6 +95,8 @@ def create_contract_deployment(
:param class_hash: The class_hash of the contract to be deployed.
:param salt: The salt for a contract to be deployed. Random value is selected if it is not provided.
:param abi: ABI of the contract to be deployed.
+ :param cairo_version: Version of the Cairo [0 or 1] in which contract to be deployed is written.
+ Used when abi is provided.
:param calldata: Constructor args of the contract to be deployed.
:return: NamedTuple with call and address of the contract to be deployed.
"""
@@ -101,7 +104,7 @@ def create_contract_deployment(
raise ValueError("Argument calldata was provided without an ABI.")
raw_calldata = translate_constructor_args(
- abi=abi or [], constructor_args=calldata
+ abi=abi or [], constructor_args=calldata, cairo_version=cairo_version
)
return self.create_contract_deployment_raw(
diff --git a/starknet_py/proxy/contract_abi_resolver.py b/starknet_py/proxy/contract_abi_resolver.py
index 466d81425..fd846b09a 100644
--- a/starknet_py/proxy/contract_abi_resolver.py
+++ b/starknet_py/proxy/contract_abi_resolver.py
@@ -1,12 +1,13 @@
+import json
import re
from enum import Enum
-from typing import AsyncGenerator, List, Tuple, TypedDict, Union
+from typing import AsyncGenerator, List, Tuple, TypedDict, Union, cast
from starknet_py.abi.shape import AbiDictList
from starknet_py.constants import (
RPC_CLASS_HASH_NOT_FOUND_ERROR,
+ RPC_CONTRACT_ERROR,
RPC_CONTRACT_NOT_FOUND_ERROR,
- RPC_INVALID_MESSAGE_SELECTOR_ERROR,
)
from starknet_py.net.client import Client
from starknet_py.net.client_errors import ClientError, ContractNotFoundError
@@ -73,9 +74,10 @@ def __init__(
self.client = client
self.proxy_config = proxy_config
- async def resolve(self) -> AbiDictList:
+ async def resolve(self) -> Tuple[AbiDictList, int]:
"""
- Returns abi of either direct contract or contract proxied by direct contract depending on proxy_config.
+ Returns abi and cairo version of either direct contract
+ or contract proxied by direct contract depending on proxy_config.
:raises ContractNotFoundError: when contract could not be found at address
:raises ProxyResolutionError: when given ProxyChecks were not sufficient to resolve proxy
@@ -85,26 +87,25 @@ async def resolve(self) -> AbiDictList:
return await self.get_abi_for_address()
return await self.resolve_abi()
- async def get_abi_for_address(self) -> AbiDictList:
+ async def get_abi_for_address(self) -> Tuple[AbiDictList, int]:
"""
- Returns abi of a contract directly from address.
+ Returns abi and cairo version of a contract directly from address.
:raises ContractNotFoundError: when contract could not be found at address
:raises AbiNotFoundError: when abi is not present in contract class at address
"""
contract_class = await _get_class_at(address=self.address, client=self.client)
- if isinstance(contract_class, SierraContractClass):
- # TODO (#1012): Consider better handling
- raise UnsupportedAbiError(
- "Proxy resolver does not currently support Cairo1 ABIs."
- )
+
if contract_class.abi is None:
raise AbiNotFoundError()
- return contract_class.abi
- async def resolve_abi(self) -> AbiDictList:
+ return self._get_abi_from_contract_class(
+ contract_class
+ ), self._get_cairo_version(contract_class)
+
+ async def resolve_abi(self) -> Tuple[AbiDictList, int]:
"""
- Returns abi of a contract that is being proxied by contract at address.
+ Returns abi and cairo version of a contract that is being proxied by contract at address.
:raises ContractNotFoundError: when contract could not be found at address
:raises ProxyResolutionError: when given ProxyChecks were not sufficient to resolve proxy
@@ -122,15 +123,13 @@ async def resolve_abi(self) -> AbiDictList:
address=implementation, client=self.client
)
- if isinstance(contract_class, SierraContractClass):
- # TODO (#1012): Consider better handling
- raise UnsupportedAbiError(
- "Proxy resolver does not currently support Cairo1 ABIs."
- )
if contract_class.abi is None:
# Some contract_class has been found, but it does not have abi
raise AbiNotFoundError()
- return contract_class.abi
+
+ return self._get_abi_from_contract_class(
+ contract_class
+ ), self._get_cairo_version(contract_class)
except ClientError as err:
if not (
"is not declared" in err.message
@@ -141,6 +140,22 @@ async def resolve_abi(self) -> AbiDictList:
raise ProxyResolutionError()
+ @staticmethod
+ def _get_cairo_version(
+ contract_class: Union[ContractClass, SierraContractClass]
+ ) -> int:
+ return 1 if isinstance(contract_class, SierraContractClass) else 0
+
+ @staticmethod
+ def _get_abi_from_contract_class(
+ contract_class: Union[ContractClass, SierraContractClass]
+ ) -> AbiDictList:
+ return (
+ cast(AbiDictList, contract_class.abi)
+ if isinstance(contract_class, ContractClass)
+ else json.loads(cast(str, contract_class.abi))
+ )
+
async def _get_implementation_from_proxy(
self,
) -> AsyncGenerator[Tuple[int, ImplementationType], None]:
@@ -164,9 +179,9 @@ async def _get_implementation_from_proxy(
re.search(err_msg, err.message, re.IGNORECASE)
or err.code
in [
- RPC_INVALID_MESSAGE_SELECTOR_ERROR,
RPC_CLASS_HASH_NOT_FOUND_ERROR,
RPC_CONTRACT_NOT_FOUND_ERROR,
+ RPC_CONTRACT_ERROR,
]
):
raise err
@@ -178,16 +193,6 @@ class AbiNotFoundError(Exception):
"""
-class UnsupportedAbiError(Exception):
- """
- Incompatible Abi error.
- """
-
- def __init__(self, message):
- self.message = message
- super().__init__(message)
-
-
class ProxyResolutionError(Exception):
"""
Error while resolving proxy using ProxyChecks.
diff --git a/starknet_py/serialization/data_serializers/__init__.py b/starknet_py/serialization/data_serializers/__init__.py
index 49d854e87..017d4797f 100644
--- a/starknet_py/serialization/data_serializers/__init__.py
+++ b/starknet_py/serialization/data_serializers/__init__.py
@@ -1,4 +1,5 @@
from .array_serializer import ArraySerializer
+from .bool_serializer import BoolSerializer
from .cairo_data_serializer import CairoDataSerializer
from .felt_serializer import FeltSerializer
from .named_tuple_serializer import NamedTupleSerializer
diff --git a/starknet_py/serialization/data_serializers/bool_serializer.py b/starknet_py/serialization/data_serializers/bool_serializer.py
new file mode 100644
index 000000000..f5cfb67fd
--- /dev/null
+++ b/starknet_py/serialization/data_serializers/bool_serializer.py
@@ -0,0 +1,37 @@
+from dataclasses import dataclass
+from typing import Generator
+
+from starknet_py.serialization._context import (
+ Context,
+ DeserializationContext,
+ SerializationContext,
+)
+from starknet_py.serialization.data_serializers.cairo_data_serializer import (
+ CairoDataSerializer,
+)
+
+
+@dataclass
+class BoolSerializer(CairoDataSerializer[bool, int]):
+ """
+ Serializer for boolean.
+ """
+
+ def deserialize_with_context(self, context: DeserializationContext) -> bool:
+ [val] = context.reader.read(1)
+ self._ensure_bool(context, val)
+ return bool(val)
+
+ def serialize_with_context(
+ self, context: SerializationContext, value: bool
+ ) -> Generator[int, None, None]:
+ context.ensure_valid_type(value, isinstance(value, bool), "bool")
+ self._ensure_bool(context, value)
+ yield int(value)
+
+ @staticmethod
+ def _ensure_bool(context: Context, value: int):
+ context.ensure_valid_value(
+ value in [0, 1],
+ f"invalid value '{value}' - must be in [0, 2) range",
+ )
diff --git a/starknet_py/serialization/data_serializers/bool_serializer_test.py b/starknet_py/serialization/data_serializers/bool_serializer_test.py
new file mode 100644
index 000000000..a51360442
--- /dev/null
+++ b/starknet_py/serialization/data_serializers/bool_serializer_test.py
@@ -0,0 +1,24 @@
+from typing import cast
+
+import pytest
+
+from starknet_py.serialization.data_serializers import BoolSerializer
+from starknet_py.serialization.errors import InvalidTypeException
+
+
+@pytest.mark.parametrize(
+ "value",
+ [True, False],
+)
+def test_valid_bool_values(value):
+ serialized = BoolSerializer().serialize(value)
+ deserialized = BoolSerializer().deserialize([value])
+
+ assert deserialized == value
+ assert serialized == [value]
+
+
+def test_invalid_type():
+ error_message = "Error: expected bool, received '{}' of type '."
+ with pytest.raises(InvalidTypeException, match=error_message):
+ BoolSerializer().serialize(cast(bool, {}))
diff --git a/starknet_py/serialization/data_serializers/cairo_data_serializer.py b/starknet_py/serialization/data_serializers/cairo_data_serializer.py
index a431fe666..2bad3b8e6 100644
--- a/starknet_py/serialization/data_serializers/cairo_data_serializer.py
+++ b/starknet_py/serialization/data_serializers/cairo_data_serializer.py
@@ -39,7 +39,9 @@ def serialize(self, data: SerializationType) -> CairoData:
:return: calldata.
"""
with SerializationContext.create() as context:
- return list(self.serialize_with_context(context, data))
+ serialized_data = list(self.serialize_with_context(context, data))
+
+ return self.remove_units_from_serialized_data(serialized_data)
@abstractmethod
def deserialize_with_context(
@@ -63,3 +65,7 @@ def serialize_with_context(
:param value: python value to serialize.
:return: defined SerializationType.
"""
+
+ @staticmethod
+ def remove_units_from_serialized_data(serialized_data: List) -> List:
+ return [x for x in serialized_data if x is not None]
diff --git a/starknet_py/serialization/data_serializers/enum_serializer.py b/starknet_py/serialization/data_serializers/enum_serializer.py
new file mode 100644
index 000000000..b31b938ae
--- /dev/null
+++ b/starknet_py/serialization/data_serializers/enum_serializer.py
@@ -0,0 +1,71 @@
+from dataclasses import dataclass
+from typing import Dict, Generator, OrderedDict, Tuple, Union
+
+from starknet_py.serialization._context import (
+ DeserializationContext,
+ SerializationContext,
+)
+from starknet_py.serialization.data_serializers.cairo_data_serializer import (
+ CairoDataSerializer,
+)
+from starknet_py.serialization.tuple_dataclass import TupleDataclass
+
+
+@dataclass
+class EnumSerializer(CairoDataSerializer[Union[Dict, TupleDataclass], TupleDataclass]):
+ """
+ Serializer of enums.
+ Can serialize a dictionary and TupleDataclass.
+ Deserializes data to a TupleDataclass.
+
+ Example:
+ enum MyEnum {
+ a: u128,
+ b: u128
+ }
+
+ {"a": 1} => [0, 1]
+ {"b": 100} => [1, 100]
+ TupleDataclass(variant='a', value=100) => [0, 100]
+ """
+
+ serializers: OrderedDict[str, CairoDataSerializer]
+
+ def deserialize_with_context(
+ self, context: DeserializationContext
+ ) -> TupleDataclass:
+ [variant_index] = context.reader.read(1)
+ variant_name, serializer = self._get_variant(variant_index)
+
+ with context.push_entity("enum.variant: " + variant_name):
+ result_dict = {
+ "variant": variant_name,
+ "value": serializer.deserialize_with_context(context),
+ }
+
+ return TupleDataclass.from_dict(result_dict)
+
+ def serialize_with_context(
+ self, context: SerializationContext, value: Union[Dict, TupleDataclass]
+ ) -> Generator[int, None, None]:
+ if isinstance(value, Dict):
+ items = list(value.items())
+ if len(items) != 1:
+ raise ValueError(
+ "Can serialize only one enum variant, got: " + str(len(items))
+ )
+
+ variant_name, variant_value = items[0]
+ else:
+ variant_name, variant_value = value
+
+ yield self._get_variant_index(variant_name)
+ yield from self.serializers[variant_name].serialize_with_context(
+ context, variant_value
+ )
+
+ def _get_variant(self, variant_index: int) -> Tuple[str, CairoDataSerializer]:
+ return list(self.serializers.items())[variant_index]
+
+ def _get_variant_index(self, variant_name: str) -> int:
+ return list(self.serializers.keys()).index(variant_name)
diff --git a/starknet_py/serialization/data_serializers/enum_serializer_test.py b/starknet_py/serialization/data_serializers/enum_serializer_test.py
new file mode 100644
index 000000000..f933fc796
--- /dev/null
+++ b/starknet_py/serialization/data_serializers/enum_serializer_test.py
@@ -0,0 +1,52 @@
+from collections import OrderedDict
+
+import pytest
+
+from starknet_py.serialization.data_serializers.enum_serializer import EnumSerializer
+from starknet_py.serialization.data_serializers.option_serializer import (
+ OptionSerializer,
+)
+from starknet_py.serialization.data_serializers.struct_serializer import (
+ StructSerializer,
+)
+from starknet_py.serialization.data_serializers.uint_serializer import UintSerializer
+
+serializer = EnumSerializer(
+ serializers=OrderedDict(
+ a=UintSerializer(256),
+ b=UintSerializer(128),
+ c=StructSerializer(
+ OrderedDict(
+ my_option=OptionSerializer(UintSerializer(128)),
+ my_uint=UintSerializer(256),
+ )
+ ),
+ )
+)
+
+
+@pytest.mark.parametrize(
+ "value, correct_serialized_value",
+ [
+ ({"a": 100}, [0, 100, 0]),
+ ({"b": 200}, [1, 200]),
+ ({"c": {"my_option": 300, "my_uint": 300}}, [2, 0, 300, 300, 0]),
+ ],
+)
+def test_output_serializer(value, correct_serialized_value):
+ deserialized = serializer.deserialize(correct_serialized_value)
+
+ deserialized_and_serialized = serializer.serialize(deserialized)
+ serialized_value = serializer.serialize(value)
+
+ assert deserialized_and_serialized == correct_serialized_value
+ assert serialized_value == correct_serialized_value
+ assert serialized_value == deserialized_and_serialized
+
+
+def test_serializer_throws_on_wrong_parameters():
+ with pytest.raises(ValueError, match="Can serialize only one enum variant, got: 2"):
+ serializer.serialize({"a": 100, "b": 200})
+
+ with pytest.raises(ValueError, match="Can serialize only one enum variant, got: 0"):
+ serializer.serialize({})
diff --git a/starknet_py/serialization/data_serializers/option_serializer.py b/starknet_py/serialization/data_serializers/option_serializer.py
new file mode 100644
index 000000000..eab921256
--- /dev/null
+++ b/starknet_py/serialization/data_serializers/option_serializer.py
@@ -0,0 +1,41 @@
+from dataclasses import dataclass
+from typing import Any, Generator, Optional
+
+from starknet_py.serialization import CairoDataSerializer
+from starknet_py.serialization._context import (
+ DeserializationContext,
+ SerializationContext,
+)
+
+
+@dataclass
+class OptionSerializer(CairoDataSerializer[Optional[Any], Optional[Any]]):
+ """
+ Serializer for Option type.
+ Can serialize None and common CairoTypes.
+ Deserializes data to None or CairoType.
+
+ Example:
+ None => [1]
+ {"option1": 123, "option2": None} => [0, 123, 1]
+ """
+
+ serializer: CairoDataSerializer
+
+ def deserialize_with_context(
+ self, context: DeserializationContext
+ ) -> Optional[Any]:
+ (is_none,) = context.reader.read(1)
+ if is_none == 1:
+ return None
+
+ return self.serializer.deserialize_with_context(context)
+
+ def serialize_with_context(
+ self, context: SerializationContext, value: Optional[Any]
+ ) -> Generator[int, None, None]:
+ if value is None:
+ yield 1
+ else:
+ yield 0
+ yield from self.serializer.serialize_with_context(context, value)
diff --git a/starknet_py/serialization/data_serializers/option_serializer_test.py b/starknet_py/serialization/data_serializers/option_serializer_test.py
new file mode 100644
index 000000000..e2cd76be6
--- /dev/null
+++ b/starknet_py/serialization/data_serializers/option_serializer_test.py
@@ -0,0 +1,23 @@
+import pytest
+
+from starknet_py.serialization.data_serializers.option_serializer import (
+ OptionSerializer,
+)
+from starknet_py.serialization.data_serializers.uint_serializer import UintSerializer
+
+
+@pytest.mark.parametrize(
+ "serializer, value, serialized_value",
+ [
+ (OptionSerializer(UintSerializer(128)), 123, [0, 123]),
+ (OptionSerializer(UintSerializer(256)), 1, [0, 1, 0]),
+ (OptionSerializer(UintSerializer(128)), None, [1]),
+ (OptionSerializer(UintSerializer(256)), None, [1]),
+ ],
+)
+def test_option_serializer(serializer, value, serialized_value):
+ deserialized = serializer.deserialize(serialized_value)
+ assert deserialized == value
+
+ serialized = serializer.serialize(value)
+ assert serialized == serialized_value
diff --git a/starknet_py/serialization/data_serializers/output_serializer.py b/starknet_py/serialization/data_serializers/output_serializer.py
new file mode 100644
index 000000000..9059aa662
--- /dev/null
+++ b/starknet_py/serialization/data_serializers/output_serializer.py
@@ -0,0 +1,38 @@
+from dataclasses import dataclass, field
+from typing import Dict, Generator, List, Tuple
+
+from starknet_py.serialization import CairoDataSerializer
+from starknet_py.serialization._context import (
+ DeserializationContext,
+ SerializationContext,
+)
+
+
+@dataclass
+class OutputSerializer(CairoDataSerializer[List, Tuple]):
+ """
+ Serializer for function output.
+ Can't serialize anything.
+ Deserializes data to a Tuple.
+
+ Example:
+ [1, 1, 1] => (340282366920938463463374607431768211457)
+ """
+
+ serializers: List[CairoDataSerializer] = field(init=True)
+
+ def deserialize_with_context(self, context: DeserializationContext) -> Tuple:
+ result = []
+
+ for index, serializer in enumerate(self.serializers):
+ with context.push_entity("output[" + str(index) + "]"):
+ result.append(serializer.deserialize_with_context(context))
+
+ return tuple(result)
+
+ def serialize_with_context(
+ self, context: SerializationContext, value: Dict
+ ) -> Generator[int, None, None]:
+ raise ValueError(
+ "Output serializer can't be used to transform python data into calldata."
+ )
diff --git a/starknet_py/serialization/data_serializers/output_serializer_test.py b/starknet_py/serialization/data_serializers/output_serializer_test.py
new file mode 100644
index 000000000..d1d00d621
--- /dev/null
+++ b/starknet_py/serialization/data_serializers/output_serializer_test.py
@@ -0,0 +1,54 @@
+import re
+from collections import OrderedDict
+
+import pytest
+
+from starknet_py.serialization.data_serializers.option_serializer import (
+ OptionSerializer,
+)
+from starknet_py.serialization.data_serializers.output_serializer import (
+ OutputSerializer,
+)
+from starknet_py.serialization.data_serializers.struct_serializer import (
+ StructSerializer,
+)
+from starknet_py.serialization.data_serializers.uint_serializer import UintSerializer
+from starknet_py.serialization.data_serializers.uint_serializer_test import SHIFT
+
+serializer = OutputSerializer(
+ serializers=[
+ UintSerializer(256),
+ OptionSerializer(
+ StructSerializer(
+ OrderedDict(
+ my_option=OptionSerializer(UintSerializer(128)),
+ my_uint=UintSerializer(256),
+ )
+ )
+ ),
+ ]
+)
+
+
+@pytest.mark.parametrize(
+ "value, serialized_value",
+ [
+ (
+ (1 + 1 * SHIFT, OrderedDict(my_option=123, my_uint=1 + 1 * SHIFT)),
+ [1, 1, 0, 0, 123, 1, 1],
+ ),
+ ((0, OrderedDict(my_option=None, my_uint=1)), [0, 0, 0, 1, 1, 0]),
+ ((1, None), [1, 0, 1]),
+ ],
+)
+def test_output_serializer_deserialize(value, serialized_value):
+ deserialized = serializer.deserialize(serialized_value)
+ assert deserialized == value
+
+
+def test_output_serializer_serialize():
+ error_message = re.escape(
+ "Output serializer can't be used to transform python data into calldata."
+ )
+ with pytest.raises(ValueError, match=error_message):
+ serializer.serialize([1, None])
diff --git a/starknet_py/serialization/data_serializers/uint_serializer.py b/starknet_py/serialization/data_serializers/uint_serializer.py
new file mode 100644
index 000000000..7f6322fc2
--- /dev/null
+++ b/starknet_py/serialization/data_serializers/uint_serializer.py
@@ -0,0 +1,98 @@
+from dataclasses import dataclass
+from typing import Generator, TypedDict, Union
+
+from starknet_py.cairo.felt import uint256_range_check
+from starknet_py.serialization import CairoDataSerializer
+from starknet_py.serialization._context import (
+ Context,
+ DeserializationContext,
+ SerializationContext,
+)
+
+
+class Uint256Dict(TypedDict):
+ low: int
+ high: int
+
+
+@dataclass
+class UintSerializer(CairoDataSerializer[Union[int, Uint256Dict], int]):
+ """
+ Serializer of uint. In Cairo there are few uints (u8, ..., u128 and u256).
+ u256 is represented by structure {low: u128, high: u128}.
+ Can serialize an int and dict.
+ Deserializes data to an int.
+
+ Examples:
+ if bits < 256:
+ 0 => [0]
+ 1 => [1]
+ 2**128-1 => [2**128-1]
+ else:
+ 0 => [0,0]
+ 1 => [1,0]
+ 2**128 => [0,1]
+ 3 + 2**128 => [3,1]
+ """
+
+ bits: int
+
+ def deserialize_with_context(self, context: DeserializationContext) -> int:
+ if self.bits < 256:
+ (uint,) = context.reader.read(1)
+ with context.push_entity("uint" + str(self.bits)):
+ self._ensure_valid_uint(uint, context, self.bits)
+
+ return uint
+
+ [low, high] = context.reader.read(2)
+
+ # Checking if resulting value is in [0, 2**256) range is not enough. Uint256 should be made of two uint128.
+ with context.push_entity("low"):
+ self._ensure_valid_uint(low, context, bits=128)
+ with context.push_entity("high"):
+ self._ensure_valid_uint(high, context, bits=128)
+
+ return (high << 128) + low
+
+ def serialize_with_context(
+ self, context: SerializationContext, value: Union[int, Uint256Dict]
+ ) -> Generator[int, None, None]:
+ context.ensure_valid_type(value, isinstance(value, (int, dict)), "int or dict")
+ if isinstance(value, int):
+ yield from self._serialize_from_int(value, context, self.bits)
+ else:
+ yield from self._serialize_from_dict(context, value)
+
+ @staticmethod
+ def _serialize_from_int(
+ value: int, context: SerializationContext, bits: int
+ ) -> Generator[int, None, None]:
+ if bits < 256:
+ UintSerializer._ensure_valid_uint(value, context, bits)
+
+ yield value
+ else:
+ uint256_range_check(value)
+
+ result = (value % 2**128, value >> 128)
+ yield from result
+
+ def _serialize_from_dict(
+ self, context: SerializationContext, value: Uint256Dict
+ ) -> Generator[int, None, None]:
+ with context.push_entity("low"):
+ self._ensure_valid_uint(value["low"], context, bits=128)
+ yield value["low"]
+ with context.push_entity("high"):
+ self._ensure_valid_uint(value["high"], context, bits=128)
+ yield value["high"]
+
+ @staticmethod
+ def _ensure_valid_uint(value: int, context: Context, bits: int):
+ """
+ Ensures that value is a valid uint on `bits` bits.
+ """
+ context.ensure_valid_value(
+ 0 <= value < 2**bits, "expected value in range [0;2**" + str(bits) + ")"
+ )
diff --git a/starknet_py/serialization/data_serializers/uint_serializer_test.py b/starknet_py/serialization/data_serializers/uint_serializer_test.py
new file mode 100644
index 000000000..0a421a4b0
--- /dev/null
+++ b/starknet_py/serialization/data_serializers/uint_serializer_test.py
@@ -0,0 +1,116 @@
+import re
+
+import pytest
+
+from starknet_py.serialization.data_serializers.uint_serializer import UintSerializer
+from starknet_py.serialization.errors import InvalidTypeException, InvalidValueException
+
+u128_serializer = UintSerializer(bits=128)
+u256_serializer = UintSerializer(bits=256)
+
+SHIFT = 2**128
+MAX_U128 = SHIFT - 1
+
+
+@pytest.mark.parametrize(
+ "value, serializer, serialized_value",
+ [
+ (123 + 456 * SHIFT, u256_serializer, [123, 456]),
+ (
+ 21323213211421424142 + 347932774343 * SHIFT,
+ u256_serializer,
+ [21323213211421424142, 347932774343],
+ ),
+ (0, u256_serializer, [0, 0]),
+ (MAX_U128, u256_serializer, [MAX_U128, 0]),
+ (MAX_U128 * SHIFT, u256_serializer, [0, MAX_U128]),
+ (MAX_U128 + MAX_U128 * SHIFT, u256_serializer, [MAX_U128, MAX_U128]),
+ (123, u128_serializer, [123]),
+ (0, u128_serializer, [0]),
+ (MAX_U128, u128_serializer, [MAX_U128]),
+ ],
+)
+def test_valid_values(value, serializer, serialized_value):
+ deserialized = serializer.deserialize(serialized_value)
+ assert deserialized == value
+
+ serialized = serializer.serialize(value)
+ assert serialized == serialized_value
+
+ if serializer.bits == 256:
+ assert serialized_value == serializer.serialize(
+ {"low": serialized_value[0], "high": serialized_value[1]}
+ )
+
+
+@pytest.mark.parametrize(
+ "value, uint256_part",
+ [
+ ([MAX_U128 + 1, 0], "low"),
+ ([MAX_U128 + 1, MAX_U128 + 1], "low"),
+ ([-1, 0], "low"),
+ ([0, MAX_U128 + 1], "high"),
+ ([0, -1], "high"),
+ ],
+)
+def test_deserialize_invalid_256_values(value, uint256_part):
+ # We need to escape braces
+ error_message = re.escape(
+ "Error at path '" + uint256_part + "': expected value in range [0;2**128)"
+ )
+ with pytest.raises(InvalidValueException, match=error_message):
+ u256_serializer.deserialize(value)
+
+
+def test_deserialize_invalid_128_values():
+ # We need to escape braces
+ error_message = re.escape(
+ "Error at path 'uint128': expected value in range [0;2**128)"
+ )
+ with pytest.raises(InvalidValueException, match=error_message):
+ u128_serializer.deserialize([MAX_U128 + 1])
+ with pytest.raises(InvalidValueException, match=error_message):
+ u128_serializer.deserialize([-1])
+
+
+def test_serialize_invalid_256_int_value():
+ error_message = re.escape("Error: Uint256 is expected to be in range [0;2**256)")
+ with pytest.raises(InvalidValueException, match=error_message):
+ u256_serializer.serialize(2**256)
+ with pytest.raises(InvalidValueException, match=error_message):
+ u256_serializer.serialize(-1)
+
+
+def test_serialize_invalid_128_int_value():
+ error_message = re.escape("Error: expected value in range [0;2**128)")
+ with pytest.raises(InvalidValueException, match=error_message):
+ u128_serializer.serialize(2**128)
+ with pytest.raises(InvalidValueException, match=error_message):
+ u128_serializer.serialize(-1)
+
+
+def test_serialize_invalid_dict_values():
+ low_error_message = re.escape(
+ "Error at path 'low': expected value in range [0;2**128)"
+ )
+ with pytest.raises(InvalidValueException, match=low_error_message):
+ u256_serializer.serialize({"low": -1, "high": 12324})
+ with pytest.raises(InvalidValueException, match=low_error_message):
+ u256_serializer.serialize({"low": MAX_U128 + 1, "high": 4543535})
+
+ high_error_message = re.escape(
+ "Error at path 'high': expected value in range [0;2**128)"
+ )
+ with pytest.raises(InvalidValueException, match=high_error_message):
+ u256_serializer.serialize({"low": 652432, "high": -1})
+ with pytest.raises(InvalidValueException, match=high_error_message):
+ u256_serializer.serialize({"low": 0, "high": MAX_U128 + 1})
+
+
+@pytest.mark.parametrize("serializer", (u128_serializer, u256_serializer))
+def test_invalid_type(serializer):
+ error_message = re.escape(
+ "Error: expected int or dict, received 'wololoo' of type ''."
+ )
+ with pytest.raises(InvalidTypeException, match=error_message):
+ serializer.serialize("wololoo") # type: ignore
diff --git a/starknet_py/serialization/data_serializers/unit_serializer.py b/starknet_py/serialization/data_serializers/unit_serializer.py
new file mode 100644
index 000000000..8c8124022
--- /dev/null
+++ b/starknet_py/serialization/data_serializers/unit_serializer.py
@@ -0,0 +1,30 @@
+from dataclasses import dataclass
+from typing import Any, Generator, Optional
+
+from starknet_py.serialization import CairoDataSerializer
+from starknet_py.serialization._context import (
+ DeserializationContext,
+ SerializationContext,
+)
+
+
+@dataclass
+class UnitSerializer(CairoDataSerializer[None, None]):
+ """
+ Serializer for unit type.
+ Can only serialize None.
+ Deserializes data to None.
+
+ Example:
+ [] => None
+ """
+
+ def deserialize_with_context(self, context: DeserializationContext) -> None:
+ return None
+
+ def serialize_with_context(
+ self, context: SerializationContext, value: Optional[Any]
+ ) -> Generator[None, None, None]:
+ if value is not None:
+ raise ValueError("Can only serialize `None`.")
+ yield None
diff --git a/starknet_py/serialization/data_serializers/unit_serializer_test.py b/starknet_py/serialization/data_serializers/unit_serializer_test.py
new file mode 100644
index 000000000..aa17a954b
--- /dev/null
+++ b/starknet_py/serialization/data_serializers/unit_serializer_test.py
@@ -0,0 +1,23 @@
+import pytest
+
+from starknet_py.serialization.data_serializers.unit_serializer import UnitSerializer
+
+serializer = UnitSerializer()
+
+
+def test_deserialize_unit():
+ deserialized = serializer.deserialize([])
+
+ assert deserialized is None
+
+
+def test_serialize_unit():
+ # pylint: disable=use-implicit-booleaness-not-comparison
+ serialized = serializer.serialize(None)
+
+ assert serialized == []
+
+
+def test_throws_on_not_none():
+ with pytest.raises(ValueError, match="Can only serialize `None`."):
+ serializer.serialize("abc") # type: ignore
diff --git a/starknet_py/serialization/factory.py b/starknet_py/serialization/factory.py
index d3f593ed2..2afaa3de0 100644
--- a/starknet_py/serialization/factory.py
+++ b/starknet_py/serialization/factory.py
@@ -1,25 +1,39 @@
from __future__ import annotations
from collections import OrderedDict
-from typing import Dict
+from typing import Dict, List
from starknet_py.abi.model import Abi
+from starknet_py.abi.v1.model import Abi as AbiV1
from starknet_py.cairo.data_types import (
ArrayType,
+ BoolType,
CairoType,
+ EnumType,
FeltType,
NamedTupleType,
+ OptionType,
StructType,
TupleType,
+ UintType,
+ UnitType,
)
+from starknet_py.serialization.data_serializers import BoolSerializer
from starknet_py.serialization.data_serializers.array_serializer import ArraySerializer
from starknet_py.serialization.data_serializers.cairo_data_serializer import (
CairoDataSerializer,
)
+from starknet_py.serialization.data_serializers.enum_serializer import EnumSerializer
from starknet_py.serialization.data_serializers.felt_serializer import FeltSerializer
from starknet_py.serialization.data_serializers.named_tuple_serializer import (
NamedTupleSerializer,
)
+from starknet_py.serialization.data_serializers.option_serializer import (
+ OptionSerializer,
+)
+from starknet_py.serialization.data_serializers.output_serializer import (
+ OutputSerializer,
+)
from starknet_py.serialization.data_serializers.payload_serializer import (
PayloadSerializer,
)
@@ -30,9 +44,12 @@
from starknet_py.serialization.data_serializers.uint256_serializer import (
Uint256Serializer,
)
+from starknet_py.serialization.data_serializers.uint_serializer import UintSerializer
+from starknet_py.serialization.data_serializers.unit_serializer import UnitSerializer
from starknet_py.serialization.errors import InvalidTypeException
from starknet_py.serialization.function_serialization_adapter import (
FunctionSerializationAdapter,
+ FunctionSerializationAdapterV1,
)
_uint256_type = StructType("Uint256", OrderedDict(low=FeltType(), high=FeltType()))
@@ -45,9 +62,13 @@ def serializer_for_type(cairo_type: CairoType) -> CairoDataSerializer:
:param cairo_type: CairoType.
:return: CairoDataSerializer.
"""
+ # pylint: disable=too-many-return-statements
if isinstance(cairo_type, FeltType):
return FeltSerializer()
+ if isinstance(cairo_type, BoolType):
+ return BoolSerializer()
+
if isinstance(cairo_type, StructType):
# Special case: Uint256 is represented as struct
if cairo_type == _uint256_type:
@@ -76,6 +97,23 @@ def serializer_for_type(cairo_type: CairoType) -> CairoDataSerializer:
)
)
+ if isinstance(cairo_type, UintType):
+ return UintSerializer(bits=cairo_type.bits)
+
+ if isinstance(cairo_type, OptionType):
+ return OptionSerializer(serializer_for_type(cairo_type.type))
+
+ if isinstance(cairo_type, UnitType):
+ return UnitSerializer()
+
+ if isinstance(cairo_type, EnumType):
+ return EnumSerializer(
+ OrderedDict(
+ (name, serializer_for_type(variant_type))
+ for name, variant_type in cairo_type.variants.items()
+ )
+ )
+
raise InvalidTypeException(f"Received unknown Cairo type '{cairo_type}'.")
@@ -96,6 +134,19 @@ def serializer_for_payload(payload: Dict[str, CairoType]) -> PayloadSerializer:
)
+def serializer_for_outputs(payload: List[CairoType]) -> OutputSerializer:
+ """
+ Create OutputSerializer for types in list. Please note that the order of fields in the list is
+ very important. Make sure the types are provided in the right order.
+
+ :param payload: list with cairo types.
+ :return: OutputSerializer that can be used to deserialize function outputs.
+ """
+ return OutputSerializer(
+ serializers=[serializer_for_type(cairo_type) for cairo_type in payload]
+ )
+
+
def serializer_for_event(event: Abi.Event) -> PayloadSerializer:
"""
Create serializer for an event.
@@ -117,3 +168,18 @@ def serializer_for_function(abi_function: Abi.Function) -> FunctionSerialization
inputs_serializer=serializer_for_payload(abi_function.inputs),
outputs_deserializer=serializer_for_payload(abi_function.outputs),
)
+
+
+def serializer_for_function_v1(
+ abi_function: AbiV1.Function,
+) -> FunctionSerializationAdapter:
+ """
+ Create FunctionSerializationAdapter for serializing function inputs and deserializing function outputs.
+
+ :param abi_function: parsed function's abi.
+ :return: FunctionSerializationAdapter.
+ """
+ return FunctionSerializationAdapterV1(
+ inputs_serializer=serializer_for_payload(abi_function.inputs),
+ outputs_deserializer=serializer_for_outputs(abi_function.outputs),
+ )
diff --git a/starknet_py/serialization/function_serialization_adapter.py b/starknet_py/serialization/function_serialization_adapter.py
index 0853e7807..b3ba0a263 100644
--- a/starknet_py/serialization/function_serialization_adapter.py
+++ b/starknet_py/serialization/function_serialization_adapter.py
@@ -4,6 +4,9 @@
from typing import Dict, List, Set, Tuple
from starknet_py.cairo.felt import CairoData
+from starknet_py.serialization.data_serializers.output_serializer import (
+ OutputSerializer,
+)
from starknet_py.serialization.data_serializers.payload_serializer import (
PayloadSerializer,
)
@@ -90,3 +93,16 @@ def _ensure_no_missing_args(expected_args: Set[str], provided_args: Set[str]):
raise InvalidTypeException(
f"Missing arguments: '{', '.join(missing_arguments)}'."
)
+
+
+@dataclass
+class FunctionSerializationAdapterV1(FunctionSerializationAdapter):
+ outputs_deserializer: OutputSerializer
+
+ def deserialize(self, data: List[int]) -> Tuple:
+ """
+ Deserializes data into TupleDataclass containing python representations.
+
+ :return: cairo data.
+ """
+ return self.outputs_deserializer.deserialize(data)
diff --git a/starknet_py/tests/e2e/account/account_test.py b/starknet_py/tests/e2e/account/account_test.py
index 1bd26a7c2..35b8d9f32 100644
--- a/starknet_py/tests/e2e/account/account_test.py
+++ b/starknet_py/tests/e2e/account/account_test.py
@@ -21,6 +21,7 @@
from starknet_py.net.models import StarknetChainId
from starknet_py.net.models.transaction import Declare, DeclareV2
from starknet_py.net.signer.stark_curve_signer import KeyPair
+from starknet_py.net.udc_deployer.deployer import Deployer
from starknet_py.tests.e2e.fixtures.constants import MAX_FEE
from starknet_py.transaction_errors import TransactionRejectedError
@@ -151,6 +152,7 @@ async def test_get_class_hash_at(map_contract, account):
async def test_get_nonce(account, map_contract):
nonce = await account.get_nonce()
address = map_contract.address
+ block = await account.client.get_block()
tx = await account.execute(
Call(
@@ -161,9 +163,15 @@ async def test_get_nonce(account, map_contract):
await account.client.wait_for_tx(tx.transaction_hash)
new_nonce = await account.get_nonce()
+ new_nonce_latest_block = await account.get_nonce(block_number="latest")
+
+ old_nonce = await account.get_nonce(block_number=block.block_number)
assert isinstance(nonce, int) and isinstance(new_nonce, int)
- assert new_nonce > nonce
+ assert new_nonce == nonce + 1
+
+ assert old_nonce == nonce
+ assert new_nonce_latest_block == new_nonce
@pytest.mark.asyncio
@@ -546,3 +554,33 @@ async def test_sign_deploy_account_tx_for_fee_estimation(
# Verify that original transaction can be sent
result = await account.client.deploy_account(transaction)
await account.client.wait_for_tx(result.transaction_hash)
+
+
+@pytest.mark.asyncio
+async def test_sign_transaction_custom_nonce(account, cairo1_hello_starknet_class_hash):
+ deployment = Deployer().create_contract_deployment(cairo1_hello_starknet_class_hash)
+ deploy_tx = await account.sign_invoke_transaction(deployment.call, max_fee=MAX_FEE)
+
+ new_balance = 30
+ invoke_tx = await account.sign_invoke_transaction(
+ Call(
+ deployment.address,
+ get_selector_from_name("increase_balance"),
+ [new_balance],
+ ),
+ nonce=deploy_tx.nonce + 1,
+ max_fee=MAX_FEE,
+ )
+
+ deploy_res = await account.client.send_transaction(deploy_tx)
+ invoke_res = await account.client.send_transaction(invoke_tx)
+
+ await account.client.wait_for_tx(deploy_res.transaction_hash)
+ await account.client.wait_for_tx(invoke_res.transaction_hash)
+
+ result = await account.client.call_contract(
+ Call(deployment.address, get_selector_from_name("get_balance"), [])
+ )
+
+ assert invoke_tx.nonce == deploy_tx.nonce + 1
+ assert result == [new_balance]
diff --git a/starknet_py/tests/e2e/client/client_test.py b/starknet_py/tests/e2e/client/client_test.py
index 1b7320f5a..8f7f672d9 100644
--- a/starknet_py/tests/e2e/client/client_test.py
+++ b/starknet_py/tests/e2e/client/client_test.py
@@ -9,6 +9,7 @@
from starknet_py.hash.selector import get_selector_from_name
from starknet_py.hash.storage import get_storage_var_address
+from starknet_py.net.client_errors import ClientError
from starknet_py.net.client_models import (
Call,
ContractClass,
@@ -23,6 +24,7 @@
SierraEntryPointsByType,
TransactionReceipt,
TransactionStatus,
+ TransactionType,
)
from starknet_py.net.full_node_client import FullNodeClient
from starknet_py.net.gateway_client import GatewayClient
@@ -129,6 +131,8 @@ async def test_get_transaction_receipt(
assert receipt.hash == invoke_transaction_hash
assert receipt.block_number == block_with_invoke_number
+ if isinstance(client, FullNodeClient):
+ assert receipt.type == TransactionType.INVOKE
@pytest.mark.asyncio
@@ -224,6 +228,8 @@ async def test_add_transaction(map_contract, client, account):
transaction_receipt = await client.get_transaction_receipt(result.transaction_hash)
assert transaction_receipt.status != TransactionStatus.NOT_RECEIVED
+ if isinstance(client, FullNodeClient):
+ assert transaction_receipt.type == TransactionType.INVOKE
@pytest.mark.asyncio
@@ -266,7 +272,10 @@ async def test_wait_for_tx_accepted(client, get_tx_receipt, request):
AsyncMock(),
) as mocked_receipt:
mocked_receipt.return_value = TransactionReceipt(
- hash=0x1, status=TransactionStatus.ACCEPTED_ON_L2, block_number=1
+ hash=0x1,
+ status=TransactionStatus.ACCEPTED_ON_L2,
+ block_number=1,
+ type=TransactionType.INVOKE,
)
client = request.getfixturevalue(client)
block_number, tx_status = await client.wait_for_tx(tx_hash=0x1)
@@ -296,7 +305,10 @@ async def test_wait_for_tx_pending(client, get_tx_receipt, request):
AsyncMock(),
) as mocked_receipt:
mocked_receipt.return_value = TransactionReceipt(
- hash=0x1, status=TransactionStatus.PENDING, block_number=1
+ hash=0x1,
+ status=TransactionStatus.PENDING,
+ block_number=1,
+ type=TransactionType.INVOKE,
)
client = request.getfixturevalue(client)
@@ -344,7 +356,11 @@ async def test_wait_for_tx_rejected(
AsyncMock(),
) as mocked_receipt:
mocked_receipt.return_value = TransactionReceipt(
- hash=0x1, status=status, block_number=1, rejection_reason=exc_message
+ hash=0x1,
+ status=status,
+ block_number=1,
+ rejection_reason=exc_message,
+ type=TransactionType.INVOKE,
)
client = request.getfixturevalue(client)
with pytest.raises(exception) as err:
@@ -375,7 +391,10 @@ async def test_wait_for_tx_cancelled(client, get_tx_receipt, request):
AsyncMock(),
) as mocked_receipt:
mocked_receipt.return_value = TransactionReceipt(
- hash=0x1, status=TransactionStatus.PENDING, block_number=1
+ hash=0x1,
+ status=TransactionStatus.PENDING,
+ block_number=1,
+ type=TransactionType.INVOKE,
)
client = request.getfixturevalue(client)
task = asyncio.create_task(
@@ -388,6 +407,34 @@ async def test_wait_for_tx_cancelled(client, get_tx_receipt, request):
await task
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "client, get_tx_receipt",
+ [
+ (
+ "gateway_client",
+ "tx_receipt_gateway_path",
+ ),
+ (
+ "full_node_client",
+ "tx_receipt_full_node_path",
+ ),
+ ],
+)
+async def test_wait_for_tx_unknown_error(client, get_tx_receipt, request):
+ get_tx_receipt = request.getfixturevalue(get_tx_receipt)
+
+ with patch(
+ get_tx_receipt,
+ AsyncMock(),
+ ) as mocked_receipt:
+ mocked_receipt.side_effect = ClientError(message="Unknown error")
+ client = request.getfixturevalue(client)
+
+ with pytest.raises(ClientError, match="Unknown error"):
+ await client.wait_for_tx(tx_hash="0x2137")
+
+
@pytest.mark.asyncio
async def test_declare_contract(map_compiled_contract, account):
declare_tx = await account.sign_declare_transaction(
@@ -402,6 +449,8 @@ async def test_declare_contract(map_compiled_contract, account):
assert transaction_receipt.status != TransactionStatus.NOT_RECEIVED
assert transaction_receipt.hash
assert 0 < transaction_receipt.actual_fee <= MAX_FEE
+ if isinstance(client, FullNodeClient):
+ assert transaction_receipt.type == TransactionType.DECLARE
@pytest.mark.asyncio
@@ -551,11 +600,11 @@ async def test_state_update_deployed_contracts(
@pytest.mark.asyncio
async def test_get_class_by_hash_sierra_program(
- client, hello_starknet_class_hash_tx_hash: Tuple[int, int]
+ client, cairo1_hello_starknet_class_hash: int
):
- (class_hash, _) = hello_starknet_class_hash_tx_hash
-
- contract_class = await client.get_class_by_hash(class_hash=class_hash)
+ contract_class = await client.get_class_by_hash(
+ class_hash=cairo1_hello_starknet_class_hash
+ )
assert isinstance(contract_class, SierraContractClass)
assert contract_class.contract_class_version == "0.1.0"
@@ -567,19 +616,17 @@ async def test_get_class_by_hash_sierra_program(
@pytest.mark.asyncio
async def test_get_declare_v2_transaction(
client,
- hello_starknet_class_hash_tx_hash: Tuple[int, int],
+ cairo1_hello_starknet_class_hash_tx_hash: Tuple[int, int],
declare_v2_hello_starknet: DeclareV2,
):
- (class_hash, tx_hash) = hello_starknet_class_hash_tx_hash
+ (class_hash, tx_hash) = cairo1_hello_starknet_class_hash_tx_hash
transaction = await client.get_transaction(tx_hash=tx_hash)
assert isinstance(transaction, DeclareTransaction)
assert transaction == DeclareTransaction(
class_hash=class_hash,
- compiled_class_hash=declare_v2_hello_starknet.compiled_class_hash
- if isinstance(client, GatewayClient)
- else None,
+ compiled_class_hash=declare_v2_hello_starknet.compiled_class_hash,
sender_address=declare_v2_hello_starknet.sender_address,
hash=tx_hash,
max_fee=declare_v2_hello_starknet.max_fee,
@@ -592,20 +639,18 @@ async def test_get_declare_v2_transaction(
@pytest.mark.asyncio
async def test_get_block_with_declare_v2(
client,
- hello_starknet_class_hash_tx_hash: Tuple[int, int],
+ cairo1_hello_starknet_class_hash_tx_hash: Tuple[int, int],
declare_v2_hello_starknet: DeclareV2,
block_with_declare_v2_number: int,
):
- (class_hash, tx_hash) = hello_starknet_class_hash_tx_hash
+ (class_hash, tx_hash) = cairo1_hello_starknet_class_hash_tx_hash
block = await client.get_block(block_number=block_with_declare_v2_number)
assert (
DeclareTransaction(
class_hash=class_hash,
- compiled_class_hash=declare_v2_hello_starknet.compiled_class_hash
- if isinstance(client, GatewayClient)
- else None,
+ compiled_class_hash=declare_v2_hello_starknet.compiled_class_hash,
sender_address=declare_v2_hello_starknet.sender_address,
hash=tx_hash,
max_fee=declare_v2_hello_starknet.max_fee,
@@ -620,20 +665,18 @@ async def test_get_block_with_declare_v2(
@pytest.mark.asyncio
async def test_get_new_state_update(
client,
- hello_starknet_class_hash_tx_hash: Tuple[int, int],
+ cairo1_hello_starknet_class_hash: int,
declare_v2_hello_starknet: DeclareV2,
block_with_declare_v2_number: int,
replaced_class: Tuple[int, int, int],
):
- (class_hash, _) = hello_starknet_class_hash_tx_hash
-
state_update = await client.get_state_update(
block_number=block_with_declare_v2_number
)
assert state_update.state_diff.replaced_classes == []
assert (
DeclaredContractHash(
- class_hash=class_hash,
+ class_hash=cairo1_hello_starknet_class_hash,
compiled_class_hash=declare_v2_hello_starknet.compiled_class_hash,
)
in state_update.state_diff.declared_contract_hashes
diff --git a/starknet_py/tests/e2e/client/fixtures/transactions.py b/starknet_py/tests/e2e/client/fixtures/transactions.py
index bd0ddeb60..fbd6e9db5 100644
--- a/starknet_py/tests/e2e/client/fixtures/transactions.py
+++ b/starknet_py/tests/e2e/client/fixtures/transactions.py
@@ -4,17 +4,14 @@
import pytest
import pytest_asyncio
-from starknet_py.common import create_casm_class
from starknet_py.contract import Contract
-from starknet_py.hash.casm_class_hash import compute_casm_class_hash
from starknet_py.net.account.account import Account
-from starknet_py.net.client import Client
-from starknet_py.net.models.transaction import DeclareV2, DeployAccount
+from starknet_py.net.models.transaction import DeployAccount
from starknet_py.net.udc_deployer.deployer import Deployer
from starknet_py.tests.e2e.client.fixtures.prepare_net_for_gateway_test import (
PreparedNetworkData,
)
-from starknet_py.tests.e2e.fixtures.constants import CONTRACTS_COMPILED_V1_DIR, MAX_FEE
+from starknet_py.tests.e2e.fixtures.constants import MAX_FEE
from starknet_py.tests.e2e.fixtures.misc import read_contract
from starknet_py.tests.e2e.utils import (
get_deploy_account_details,
@@ -63,67 +60,32 @@ def block_with_deploy_account_number(
return prepared_data.block_with_deploy_account_number
-@pytest_asyncio.fixture(scope="package")
-async def declare_v2_hello_starknet(gateway_account: Account) -> DeclareV2:
- """
- Returns DeclareV2 transaction.
- """
- hello_starknet_compiled = read_contract(
- "hello_starknet_compiled.json", directory=CONTRACTS_COMPILED_V1_DIR
- )
- compiled_class_hash = compute_casm_class_hash(
- create_casm_class(
- read_contract(
- "hello_starknet_compiled.casm", directory=CONTRACTS_COMPILED_V1_DIR
- )
- )
- )
- return await gateway_account.sign_declare_v2_transaction(
- hello_starknet_compiled,
- compiled_class_hash=compiled_class_hash,
- max_fee=MAX_FEE,
- )
-
-
-@pytest_asyncio.fixture(scope="package")
-async def hello_starknet_class_hash_tx_hash(
- gateway_client: Client, declare_v2_hello_starknet: DeclareV2
-) -> Tuple[int, int]:
- """
- Returns class_hash and transaction_hash from the declare_v2_hello_starknet transaction.
- """
- result = await gateway_client.declare(declare_v2_hello_starknet)
- await gateway_client.wait_for_tx(
- tx_hash=result.transaction_hash, wait_for_accept=True
- )
-
- return result.class_hash, result.transaction_hash
-
-
@pytest_asyncio.fixture(scope="package")
async def hello_starknet_deploy_transaction_address(
- gateway_account: Account, hello_starknet_class_hash_tx_hash: Tuple[int, int]
+ account: Account, cairo1_hello_starknet_class_hash
) -> int:
- class_hash, _ = hello_starknet_class_hash_tx_hash
deployer = Deployer()
- contract_deployment = deployer.create_contract_deployment_raw(class_hash=class_hash)
- deploy_invoke_transaction = await gateway_account.sign_invoke_transaction(
+ contract_deployment = deployer.create_contract_deployment_raw(
+ class_hash=cairo1_hello_starknet_class_hash
+ )
+ deploy_invoke_transaction = await account.sign_invoke_transaction(
calls=contract_deployment.call, max_fee=MAX_FEE
)
- resp = await gateway_account.client.send_transaction(deploy_invoke_transaction)
- await gateway_account.client.wait_for_tx(resp.transaction_hash)
+ resp = await account.client.send_transaction(deploy_invoke_transaction)
+ await account.client.wait_for_tx(resp.transaction_hash)
return contract_deployment.address
@pytest_asyncio.fixture(scope="package")
async def block_with_declare_v2_number(
- hello_starknet_class_hash_tx_hash: Tuple[int, int], full_node_client
+ cairo1_hello_starknet_tx_hash: int, client
) -> int:
"""
Returns number of the block with DeclareV2 transaction
"""
- (_, tx_hash) = hello_starknet_class_hash_tx_hash
- declare_v2_receipt = await full_node_client.get_transaction_receipt(tx_hash)
+ declare_v2_receipt = await client.get_transaction_receipt(
+ cairo1_hello_starknet_tx_hash
+ )
return declare_v2_receipt.block_number
diff --git a/starknet_py/tests/e2e/client/full_node_test.py b/starknet_py/tests/e2e/client/full_node_test.py
index 9520a0779..e050d2750 100644
--- a/starknet_py/tests/e2e/client/full_node_test.py
+++ b/starknet_py/tests/e2e/client/full_node_test.py
@@ -6,13 +6,16 @@
from starknet_py.contract import Contract
from starknet_py.hash.selector import get_selector_from_name
from starknet_py.hash.storage import get_storage_var_address
+from starknet_py.net.account.account import Account
from starknet_py.net.client_errors import ClientError
from starknet_py.net.client_models import (
ContractClass,
DeclareTransaction,
SierraContractClass,
+ TransactionType,
)
from starknet_py.net.full_node_client import _to_rpc_felt
+from starknet_py.net.models import StarknetChainId
def _parse_event_name(event: str) -> str:
@@ -123,6 +126,27 @@ async def test_pending_transactions(full_node_client):
assert pending_transactions[0].max_fee == 0
+@pytest.mark.asyncio
+async def test_get_transaction_receipt_deploy_account(
+ full_node_client, deploy_account_details_factory
+):
+ address, key_pair, salt, class_hash = await deploy_account_details_factory.get()
+ deploy_result = await Account.deploy_account(
+ address=address,
+ class_hash=class_hash,
+ salt=salt,
+ key_pair=key_pair,
+ client=full_node_client,
+ chain=StarknetChainId.TESTNET,
+ max_fee=int(1e16),
+ )
+ await deploy_result.wait_for_acceptance()
+
+ receipt = await full_node_client.get_transaction_receipt(tx_hash=deploy_result.hash)
+ assert receipt.type == TransactionType.DEPLOY_ACCOUNT
+ assert receipt.contract_address == deploy_result.account.address
+
+
@pytest.mark.run_on_devnet
@pytest.mark.asyncio
async def test_get_storage_at_incorrect_address_full_node_client(full_node_client):
@@ -134,6 +158,15 @@ async def test_get_storage_at_incorrect_address_full_node_client(full_node_clien
)
+@pytest.mark.asyncio
+async def test_wait_for_tx_invalid_tx_hash(full_node_client):
+ with pytest.raises(
+ ClientError,
+ match="Nodes can't access pending transactions, try using parameter 'wait_for_accept=True'.",
+ ):
+ _ = await full_node_client.wait_for_tx(tx_hash=0x123456789)
+
+
@pytest.mark.run_on_devnet
@pytest.mark.asyncio
async def test_get_events_without_following_continuation_token(
@@ -242,7 +275,7 @@ async def test_get_events_with_two_events(
from_block_number=0,
to_block_hash="latest",
address=simple_storage_with_event_contract.address,
- keys=[[EVENT_ONE_PARSED_NAME, EVENT_TWO_PARSED_NAME]],
+ keys=[[int(EVENT_ONE_PARSED_NAME, 0), int(EVENT_TWO_PARSED_NAME, 0)]],
follow_continuation_token=True,
)
@@ -283,6 +316,25 @@ async def test_get_events_start_from_continuation_token(
assert events_response.continuation_token == expected_continuation_token
+@pytest.mark.run_on_devnet
+@pytest.mark.asyncio
+async def test_get_events_no_params(
+ full_node_client,
+ simple_storage_with_event_contract: Contract,
+):
+ default_chunk_size = 1
+ for i in range(3):
+ await simple_storage_with_event_contract.functions[FUNCTION_ONE_NAME].invoke(
+ i, i + 1, auto_estimate=True
+ )
+ await simple_storage_with_event_contract.functions[FUNCTION_TWO_NAME].invoke(
+ i, i + 1, auto_estimate=True
+ )
+ events_response = await full_node_client.get_events()
+
+ assert len(events_response.events) == default_chunk_size
+
+
@pytest.mark.run_on_devnet
@pytest.mark.asyncio
async def test_get_events_nonexistent_starting_block(
diff --git a/starknet_py/tests/e2e/client/gateway_test.py b/starknet_py/tests/e2e/client/gateway_test.py
index 2171e4c14..aceb1b518 100644
--- a/starknet_py/tests/e2e/client/gateway_test.py
+++ b/starknet_py/tests/e2e/client/gateway_test.py
@@ -1,4 +1,3 @@
-from typing import Tuple
from unittest.mock import AsyncMock, patch
import pytest
@@ -41,12 +40,10 @@ async def test_get_class_hash_at(contract_address, gateway_client, class_hash):
@pytest.mark.asyncio
async def test_get_compiled_class_by_class_hash(
- gateway_client, hello_starknet_class_hash_tx_hash: Tuple[int, int]
+ gateway_client, cairo1_hello_starknet_class_hash
):
- (class_hash, _) = hello_starknet_class_hash_tx_hash
-
compiled_class = await gateway_client.get_compiled_class_by_class_hash(
- class_hash=class_hash
+ class_hash=cairo1_hello_starknet_class_hash
)
assert isinstance(compiled_class, CasmClass)
diff --git a/starknet_py/tests/e2e/contract_interaction/declare_test.py b/starknet_py/tests/e2e/contract_interaction/declare_test.py
new file mode 100644
index 000000000..72242dffe
--- /dev/null
+++ b/starknet_py/tests/e2e/contract_interaction/declare_test.py
@@ -0,0 +1,44 @@
+import pytest
+
+from starknet_py.contract import Contract
+from starknet_py.tests.e2e.fixtures.constants import CONTRACTS_COMPILED_V1_DIR, MAX_FEE
+from starknet_py.tests.e2e.fixtures.misc import read_contract
+
+
+@pytest.mark.asyncio
+async def test_contract_declare(account):
+ compiled_contract = read_contract(
+ "test_contract_declare_compiled.json", directory=CONTRACTS_COMPILED_V1_DIR
+ )
+ compiled_contract_casm = read_contract(
+ "test_contract_declare_compiled.casm", directory=CONTRACTS_COMPILED_V1_DIR
+ )
+
+ declare_result = await Contract.declare(
+ account,
+ compiled_contract=compiled_contract,
+ compiled_contract_casm=compiled_contract_casm,
+ max_fee=MAX_FEE,
+ )
+ await declare_result.wait_for_acceptance()
+
+ assert isinstance(declare_result.hash, int)
+ assert isinstance(declare_result.class_hash, int)
+ assert declare_result.compiled_contract == compiled_contract
+
+
+@pytest.mark.asyncio
+async def test_throws_when_cairo1_without_compiled_contract_casm_and_casm_class_hash(
+ account,
+):
+ compiled_contract = read_contract(
+ "erc20_compiled.json", directory=CONTRACTS_COMPILED_V1_DIR
+ )
+
+ with pytest.raises(
+ ValueError,
+ match="Cairo 1.0 contract was provided without casm_class_hash or compiled_contract_casm argument.",
+ ):
+ await Contract.declare(
+ account, compiled_contract=compiled_contract, max_fee=MAX_FEE
+ )
diff --git a/starknet_py/tests/e2e/contract_interaction/deploy_test.py b/starknet_py/tests/e2e/contract_interaction/deploy_test.py
new file mode 100644
index 000000000..63b3d90f7
--- /dev/null
+++ b/starknet_py/tests/e2e/contract_interaction/deploy_test.py
@@ -0,0 +1,88 @@
+import dataclasses
+import json
+import re
+
+import pytest
+
+from starknet_py.common import create_sierra_compiled_contract
+from starknet_py.contract import Contract, DeclareResult
+from starknet_py.tests.e2e.fixtures.constants import CONTRACTS_COMPILED_V1_DIR, MAX_FEE
+from starknet_py.tests.e2e.fixtures.misc import read_contract
+
+
+@pytest.mark.asyncio
+async def test_declare_deploy(
+ account,
+ cairo1_minimal_contract_class_hash: int,
+):
+ compiled_contract = read_contract(
+ "minimal_contract_compiled.json", directory=CONTRACTS_COMPILED_V1_DIR
+ )
+
+ declare_result = DeclareResult(
+ _account=account,
+ _client=account.client,
+ _cairo_version=1,
+ class_hash=cairo1_minimal_contract_class_hash,
+ compiled_contract=compiled_contract,
+ hash=0,
+ )
+
+ deploy_result = await declare_result.deploy(max_fee=MAX_FEE)
+ await deploy_result.wait_for_acceptance()
+
+ assert isinstance(deploy_result.hash, int)
+ assert deploy_result.hash != 0
+ assert deploy_result.deployed_contract.address != 0
+
+
+@pytest.mark.asyncio
+async def test_throws_on_wrong_abi(account, cairo1_minimal_contract_class_hash: int):
+ compiled_contract = read_contract(
+ "minimal_contract_compiled.json", directory=CONTRACTS_COMPILED_V1_DIR
+ )
+
+ declare_result = DeclareResult(
+ _account=account,
+ _client=account.client,
+ _cairo_version=1,
+ class_hash=cairo1_minimal_contract_class_hash,
+ compiled_contract=compiled_contract,
+ hash=0,
+ )
+
+ compiled_contract = compiled_contract.replace('"abi": [', '"abi": ')
+ declare_result = dataclasses.replace(
+ declare_result, compiled_contract=compiled_contract
+ )
+
+ with pytest.raises(
+ ValueError,
+ match=re.escape(
+ "Contract's ABI can't be converted to format List[Dict]. "
+ "Make sure provided compiled_contract is correct."
+ ),
+ ):
+ await declare_result.deploy(max_fee=MAX_FEE)
+
+
+@pytest.mark.asyncio
+async def test_deploy_contract_flow(account, cairo1_hello_starknet_class_hash: int):
+ compiled_contract = read_contract(
+ "hello_starknet_compiled.json", directory=CONTRACTS_COMPILED_V1_DIR
+ )
+ abi = create_sierra_compiled_contract(compiled_contract=compiled_contract).abi
+
+ deploy_result = await Contract.deploy_contract(
+ class_hash=cairo1_hello_starknet_class_hash,
+ account=account,
+ abi=json.loads(abi),
+ max_fee=MAX_FEE,
+ cairo_version=1,
+ )
+ await deploy_result.wait_for_acceptance()
+
+ contract = deploy_result.deployed_contract
+
+ assert isinstance(contract.address, int)
+ assert len(contract.functions) != 0
diff --git a/starknet_py/tests/e2e/contract_interaction/interaction_test.py b/starknet_py/tests/e2e/contract_interaction/interaction_test.py
index 35f9be24a..4336b0f4d 100644
--- a/starknet_py/tests/e2e/contract_interaction/interaction_test.py
+++ b/starknet_py/tests/e2e/contract_interaction/interaction_test.py
@@ -6,7 +6,12 @@
from starknet_py.contract import Contract
from starknet_py.hash.selector import get_selector_from_name
from starknet_py.net.client_errors import ClientError
-from starknet_py.net.client_models import Call, TransactionReceipt, TransactionStatus
+from starknet_py.net.client_models import (
+ Call,
+ TransactionReceipt,
+ TransactionStatus,
+ TransactionType,
+)
from starknet_py.net.gateway_client import GatewayClient
from starknet_py.tests.e2e.fixtures.constants import MAX_FEE
from starknet_py.transaction_errors import (
@@ -157,7 +162,7 @@ async def test_wait_for_tx_throws_on_transaction_rejected(client, map_contract):
@patch("starknet_py.net.gateway_client.GatewayClient.get_transaction_receipt")
async def test_transaction_not_received_error(mocked_get_receipt, map_contract):
mocked_get_receipt.return_value = TransactionReceipt(
- hash=1, status=TransactionStatus.NOT_RECEIVED
+ hash=1, status=TransactionStatus.NOT_RECEIVED, type=TransactionType.INVOKE
)
result = await map_contract.functions["put"].invoke(10, 20, max_fee=MAX_FEE)
diff --git a/starknet_py/tests/e2e/contract_interaction/v1_interaction_test.py b/starknet_py/tests/e2e/contract_interaction/v1_interaction_test.py
new file mode 100644
index 000000000..c5df3bb17
--- /dev/null
+++ b/starknet_py/tests/e2e/contract_interaction/v1_interaction_test.py
@@ -0,0 +1,160 @@
+import pytest
+
+from starknet_py.cairo.felt import decode_shortstring, encode_shortstring
+from starknet_py.contract import Contract
+from starknet_py.tests.e2e.fixtures.constants import MAX_FEE
+from starknet_py.tests.e2e.fixtures.contracts import deploy_v1_contract
+
+
+@pytest.mark.asyncio
+async def test_general_v1_interaction(account, cairo1_erc20_class_hash: int):
+ calldata = {
+ "name_": encode_shortstring("erc20_basic"),
+ "symbol_": encode_shortstring("ERC20B"),
+ "decimals_": 10,
+ "initial_supply": 12345,
+ "recipient": account.address,
+ }
+ erc20 = await deploy_v1_contract(
+ account=account,
+ contract_file_name="erc20",
+ class_hash=cairo1_erc20_class_hash,
+ calldata=calldata,
+ )
+
+ (name,) = await erc20.functions["get_name"].call()
+ decoded_name = decode_shortstring(name).lstrip("\x00")
+ (decimals,) = await erc20.functions["get_decimals"].call()
+ (supply,) = await erc20.functions["get_total_supply"].call()
+ (account_balance,) = await erc20.functions["balance_of"].call(
+ account=account.address
+ )
+
+ transfer_amount = 10
+ await (
+ await erc20.functions["transfer"].invoke(
+ recipient=0x11, amount=transfer_amount, max_fee=MAX_FEE
+ )
+ ).wait_for_acceptance()
+
+ (after_transfer_balance,) = await erc20.functions["balance_of"].call(
+ account=account.address
+ )
+
+ assert decoded_name == "erc20_basic"
+ assert decimals == calldata["decimals_"]
+ assert supply == calldata["initial_supply"]
+ assert account_balance == calldata["initial_supply"]
+ assert after_transfer_balance == calldata["initial_supply"] - transfer_amount
+
+
+@pytest.mark.asyncio
+async def test_serializing_struct(account, cairo1_token_bridge_class_hash: int):
+ bridge = await deploy_v1_contract(
+ account=account,
+ contract_file_name="token_bridge",
+ class_hash=cairo1_token_bridge_class_hash,
+ calldata={"governor_address": account.address},
+ )
+
+ await (
+ await bridge.functions["set_l1_bridge"].invoke(
+ l1_bridge_address={"address": 0x11}, max_fee=MAX_FEE
+ )
+ ).wait_for_acceptance()
+
+
+@pytest.mark.asyncio
+async def test_serializing_option(account, cairo1_test_option_class_hash: int):
+ test_option = await deploy_v1_contract(
+ account=account,
+ contract_file_name="test_option",
+ class_hash=cairo1_test_option_class_hash,
+ )
+
+ (received_option,) = await test_option.functions["get_option_struct"].call()
+
+ assert dict(received_option) == {
+ "first_field": 1,
+ "second_field": 2,
+ "third_field": None,
+ "fourth_field": 4,
+ }
+
+ option_struct = {
+ "first_field": 1,
+ "second_field": 2**128 + 1,
+ "third_field": None,
+ "fourth_field": 4,
+ }
+
+ (received_option,) = await test_option.functions[
+ "receive_and_send_option_struct"
+ ].call(option_struct=option_struct)
+
+ assert dict(received_option) == option_struct
+
+ (received_option,) = await test_option.functions["get_empty_option"].call()
+
+ assert received_option is None
+
+
+@pytest.mark.asyncio
+async def test_serializing_enum(account, cairo1_test_enum_class_hash: int):
+ test_enum = await deploy_v1_contract(
+ account=account,
+ contract_file_name="test_enum",
+ class_hash=cairo1_test_enum_class_hash,
+ )
+
+ (received_enum,) = await test_enum.functions["get_enum"].call()
+
+ assert received_enum.variant == "a"
+ assert received_enum.value == 100
+
+ (received_enum,) = await test_enum.functions["get_enum_without_value"].call()
+
+ assert received_enum.variant == "c"
+ assert received_enum.value is None
+
+ variant_name = "b"
+ value = 200
+ (received_enum,) = await test_enum.functions["receive_and_send_enum"].call(
+ my_enum={variant_name: value}
+ )
+
+ assert received_enum.variant == variant_name
+ assert received_enum.value == value
+
+ variant_name = "c"
+ value = None
+ (received_enum,) = await test_enum.functions["receive_and_send_enum"].call(
+ my_enum={variant_name: value}
+ )
+
+ assert received_enum.variant == variant_name
+ assert received_enum.value == value
+
+
+@pytest.mark.asyncio
+async def test_from_address_on_v1_contract(account, cairo1_erc20_class_hash: int):
+ calldata = {
+ "name_": encode_shortstring("erc20_basic"),
+ "symbol_": encode_shortstring("ERC20B"),
+ "decimals_": 10,
+ "initial_supply": 12345,
+ "recipient": account.address,
+ }
+ erc20 = await deploy_v1_contract(
+ account=account,
+ contract_file_name="erc20",
+ class_hash=cairo1_erc20_class_hash,
+ calldata=calldata,
+ )
+
+ erc20_from_address = await Contract.from_address(erc20.address, provider=account)
+
+ assert erc20_from_address.address == erc20.address
+ assert erc20_from_address.account == erc20.account
+ assert erc20_from_address.functions.keys() == erc20.functions.keys()
+ assert erc20_from_address.data == erc20.data
diff --git a/starknet_py/tests/e2e/declare/declare_test.py b/starknet_py/tests/e2e/declare/declare_test.py
index 87250ded1..bea65eae7 100644
--- a/starknet_py/tests/e2e/declare/declare_test.py
+++ b/starknet_py/tests/e2e/declare/declare_test.py
@@ -16,42 +16,6 @@ async def test_declare_tx(account, map_compiled_contract):
@pytest.mark.asyncio
-async def test_declare_v2_tx_full_node_client(
- full_node_account, sierra_minimal_compiled_contract_and_class_hash
-):
- (
- compiled_contract,
- compiled_class_hash,
- ) = sierra_minimal_compiled_contract_and_class_hash
-
- declare_tx = await full_node_account.sign_declare_v2_transaction(
- compiled_contract=compiled_contract,
- compiled_class_hash=compiled_class_hash,
- max_fee=MAX_FEE,
- )
- assert declare_tx.version == 2
- declare_result = await full_node_account.client.declare(declare_tx)
- await full_node_account.client.wait_for_tx(
- tx_hash=declare_result.transaction_hash, wait_for_accept=True
- )
-
-
-@pytest.mark.asyncio
-async def test_declare_v2_tx_gateway_client(
- gateway_account, another_sierra_minimal_compiled_contract_and_class_hash
-):
- (
- compiled_contract,
- compiled_class_hash,
- ) = another_sierra_minimal_compiled_contract_and_class_hash
-
- declare_tx = await gateway_account.sign_declare_v2_transaction(
- compiled_contract=compiled_contract,
- compiled_class_hash=compiled_class_hash,
- max_fee=MAX_FEE,
- )
- assert declare_tx.version == 2
- declare_result = await gateway_account.client.declare(declare_tx)
- await gateway_account.client.wait_for_tx(
- tx_hash=declare_result.transaction_hash, wait_for_accept=True
- )
+async def test_declare_v2_tx(cairo1_minimal_contract_class_hash: int):
+ assert isinstance(cairo1_minimal_contract_class_hash, int)
+ assert cairo1_minimal_contract_class_hash != 0
diff --git a/starknet_py/tests/e2e/docs/code_examples/test_full_node_client.py b/starknet_py/tests/e2e/docs/code_examples/test_full_node_client.py
index 70c09927f..5b60483bb 100644
--- a/starknet_py/tests/e2e/docs/code_examples/test_full_node_client.py
+++ b/starknet_py/tests/e2e/docs/code_examples/test_full_node_client.py
@@ -158,3 +158,21 @@ async def test_get_contract_nonce(full_node_client, contract_address):
contract_address=address, block_number="latest"
)
# docs-end: get_contract_nonce
+
+
+@pytest.mark.asyncio
+async def test_get_events(full_node_client, contract_address):
+ # docs-start: get_events
+ address = 0x1 or 1 or "0x1"
+ # docs-end: get_events
+ address = contract_address
+ # docs-start: get_events
+ events_response = await full_node_client.get_events(
+ address=address,
+ keys=[[1, 2], [], [3]],
+ from_block_number=0,
+ to_block_number="latest",
+ follow_continuation_token=True,
+ chunk_size=47,
+ )
+ # docs-end: get_events
diff --git a/starknet_py/tests/e2e/docs/guide/test_cairo1_contract.py b/starknet_py/tests/e2e/docs/guide/test_cairo1_contract.py
index 863cb6509..168e0ba22 100644
--- a/starknet_py/tests/e2e/docs/guide/test_cairo1_contract.py
+++ b/starknet_py/tests/e2e/docs/guide/test_cairo1_contract.py
@@ -1,3 +1,5 @@
+import json
+
import pytest
from starknet_py.net.client_models import CasmClass
@@ -64,25 +66,26 @@ async def test_cairo1_contract(
assert sierra_class_hash != 0
# START OF DEPLOY SECTION
- raw_calldata = []
+ calldata = []
salt = _get_random_salt()
+ abi = json.loads(compiled_contract)["abi"]
# docs-deploy: start
from starknet_py.net.udc_deployer.deployer import Deployer
# Use Universal Deployer Contract (UDC) to deploy the Cairo1 contract
deployer = Deployer()
- # Create a ContractDeployment, optionally passing salt and raw_calldata
- contract_deployment = deployer.create_contract_deployment_raw(
- class_hash=sierra_class_hash, raw_calldata=raw_calldata, salt=salt
+ # Create a ContractDeployment, optionally passing salt and calldata
+ contract_deployment = deployer.create_contract_deployment(
+ class_hash=sierra_class_hash,
+ abi=abi,
+ cairo_version=1,
+ calldata=calldata,
+ salt=salt,
)
- # Using the call, create an Invoke transaction to the UDC
- deploy_invoke_transaction = await account.sign_invoke_transaction(
- calls=contract_deployment.call, max_fee=MAX_FEE
- )
- resp = await account.client.send_transaction(deploy_invoke_transaction)
- await account.client.wait_for_tx(resp.transaction_hash)
+ res = await account.execute(calls=contract_deployment.call, max_fee=MAX_FEE)
+ await account.client.wait_for_tx(res.transaction_hash)
# The contract has been deployed and can be found at contract_deployment.address
# docs-deploy: end
diff --git a/starknet_py/tests/e2e/docs/guide/test_custom_nonce.py b/starknet_py/tests/e2e/docs/guide/test_custom_nonce.py
index 2f8aa4e4f..092cc77c8 100644
--- a/starknet_py/tests/e2e/docs/guide/test_custom_nonce.py
+++ b/starknet_py/tests/e2e/docs/guide/test_custom_nonce.py
@@ -1,8 +1,8 @@
-from typing import Optional
+from typing import Optional, Union
import pytest
-from starknet_py.net.client_models import Call
+from starknet_py.net.client_models import Call, Hash, Tag
@pytest.mark.asyncio
@@ -39,7 +39,12 @@ def __init__(
# Create a simple counter that will store a nonce
self.nonce_counter = 0
- async def get_nonce(self) -> int:
+ async def get_nonce(
+ self,
+ *,
+ block_hash: Optional[Union[Hash, Tag]] = None,
+ block_number: Optional[Union[int, Tag]] = None,
+ ) -> int:
# Increment the counter and return the nonce.
# This is just an example custom nonce logic and is not meant
# to be a recommended solution.
diff --git a/starknet_py/tests/e2e/docs/guide/test_simple_declare_and_deploy_cairo1.py b/starknet_py/tests/e2e/docs/guide/test_simple_declare_and_deploy_cairo1.py
new file mode 100644
index 000000000..ce9b110d9
--- /dev/null
+++ b/starknet_py/tests/e2e/docs/guide/test_simple_declare_and_deploy_cairo1.py
@@ -0,0 +1,43 @@
+import pytest
+
+from starknet_py.tests.e2e.fixtures.constants import CONTRACTS_COMPILED_V1_DIR
+from starknet_py.tests.e2e.fixtures.misc import read_contract
+
+
+@pytest.mark.asyncio
+async def test_simple_declare_and_deploy(account):
+ # pylint: disable=import-outside-toplevel
+ # docs: start
+ from starknet_py.contract import Contract
+
+ # docs: end
+ compiled_contract = read_contract(
+ "account_compiled.json", directory=CONTRACTS_COMPILED_V1_DIR
+ )
+ compiled_contract_casm = read_contract(
+ "account_compiled.casm", directory=CONTRACTS_COMPILED_V1_DIR
+ )
+ constructor_args = {"public_key_": 0x123}
+
+ # docs: start
+ declare_result = await Contract.declare(
+ account=account,
+ compiled_contract=compiled_contract,
+ compiled_contract_casm=compiled_contract_casm,
+ max_fee=int(1e16),
+ )
+ await declare_result.wait_for_acceptance()
+
+ deploy_result = await declare_result.deploy(
+ constructor_args=constructor_args, max_fee=int(1e16)
+ )
+ await deploy_result.wait_for_acceptance()
+
+ contract = deploy_result.deployed_contract
+ # docs: end
+
+ assert isinstance(declare_result.hash, int)
+ assert isinstance(declare_result.class_hash, int)
+ assert declare_result.compiled_contract == compiled_contract
+ assert contract.address != 0
+ assert len(contract.functions) > 0
diff --git a/starknet_py/tests/e2e/docs/guide/test_simple_deploy.py b/starknet_py/tests/e2e/docs/guide/test_simple_deploy.py
index 333e973b2..3348211b3 100644
--- a/starknet_py/tests/e2e/docs/guide/test_simple_deploy.py
+++ b/starknet_py/tests/e2e/docs/guide/test_simple_deploy.py
@@ -4,9 +4,7 @@
@pytest.mark.asyncio
-async def test_simple_declare_and_deploy(
- account, map_class_hash, map_compiled_contract
-):
+async def test_simple_deploy(account, map_class_hash, map_compiled_contract):
# pylint: disable=import-outside-toplevel
# docs: start
from starknet_py.contract import Contract
diff --git a/starknet_py/tests/e2e/docs/guide/test_simple_deploy_cairo1.py b/starknet_py/tests/e2e/docs/guide/test_simple_deploy_cairo1.py
new file mode 100644
index 000000000..3d71710c1
--- /dev/null
+++ b/starknet_py/tests/e2e/docs/guide/test_simple_deploy_cairo1.py
@@ -0,0 +1,48 @@
+import json
+
+import pytest
+
+from starknet_py.tests.e2e.fixtures.constants import CONTRACTS_COMPILED_V1_DIR
+from starknet_py.tests.e2e.fixtures.misc import read_contract
+
+
+@pytest.mark.asyncio
+async def test_simple_deploy_cairo1(account, cairo1_erc20_class_hash):
+ # pylint: disable=import-outside-toplevel
+ # docs: start
+ from starknet_py.cairo.felt import encode_shortstring
+ from starknet_py.common import create_sierra_compiled_contract
+ from starknet_py.contract import Contract
+
+ # docs: end
+
+ compiled_contract = read_contract(
+ "erc20_compiled.json", directory=CONTRACTS_COMPILED_V1_DIR
+ )
+ class_hash = cairo1_erc20_class_hash
+
+ # docs: start
+ abi = create_sierra_compiled_contract(compiled_contract=compiled_contract).abi
+
+ constructor_args = {
+ "name_": encode_shortstring("erc20_basic"),
+ "symbol_": encode_shortstring("ERC20B"),
+ "decimals_": 10,
+ "initial_supply": 12345,
+ "recipient": account.address,
+ }
+
+ deploy_result = await Contract.deploy_contract(
+ account=account,
+ class_hash=class_hash,
+ abi=json.loads(abi),
+ constructor_args=constructor_args,
+ max_fee=int(1e16),
+ cairo_version=1, # note the `cairo_version` parameter
+ )
+
+ await deploy_result.wait_for_acceptance()
+ contract = deploy_result.deployed_contract
+ # docs: end
+
+ assert contract.address != 0
diff --git a/starknet_py/tests/e2e/fixtures/abi_structures.py b/starknet_py/tests/e2e/fixtures/abi_structures.py
index 5e5a1e479..13016d313 100644
--- a/starknet_py/tests/e2e/fixtures/abi_structures.py
+++ b/starknet_py/tests/e2e/fixtures/abi_structures.py
@@ -38,6 +38,17 @@
{"name": "pool_id", "offset": 3, "type": "PoolId"},
],
}
+user_missing_offset_dict = {
+ "type": "struct",
+ "name": "User",
+ "size": 4,
+ "members": [
+ {"name": "id", "type": "Uint256"},
+ {"name": "name_len", "type": "felt"},
+ {"name": "name", "type": "felt*"},
+ {"name": "pool_id", "type": "PoolId"},
+ ],
+}
user_struct = StructType(
"User",
OrderedDict(
@@ -48,6 +59,27 @@
),
)
+user_partial_missing_offset_dict = {
+ "type": "struct",
+ "name": "User",
+ "size": 4,
+ "members": [
+ {"name": "id", "offset": 0, "type": "Uint256"},
+ {"name": "name_len", "type": "felt"},
+ {"name": "name", "type": "felt*"},
+ {"name": "pool_id", "offset": 1, "type": "PoolId"},
+ ],
+}
+user_partial_missing_offset_struct = StructType(
+ "User",
+ OrderedDict(
+ id=uint256_struct,
+ pool_id=pool_id_struct,
+ name_len=FeltType(),
+ name=ArrayType(FeltType()),
+ ),
+)
+
user_added_dict = {
"type": "event",
"name": "UserAdded",
diff --git a/starknet_py/tests/e2e/fixtures/abi_v1_structures.py b/starknet_py/tests/e2e/fixtures/abi_v1_structures.py
new file mode 100644
index 000000000..46bb8a66f
--- /dev/null
+++ b/starknet_py/tests/e2e/fixtures/abi_v1_structures.py
@@ -0,0 +1,172 @@
+# data from cairo repository: crates/cairo-lang-starknet/src/abi_test.rs
+from collections import OrderedDict
+
+from starknet_py.abi.v1.model import Abi
+from starknet_py.cairo.data_types import (
+ ArrayType,
+ EnumType,
+ FeltType,
+ StructType,
+ UintType,
+)
+
+core_structures = {
+ "core::starknet::eth_address::EthAddress": StructType(
+ name="core::starknet::eth_address::EthAddress",
+ types=OrderedDict([("address", FeltType())]),
+ )
+}
+
+
+pool_id_dict = {
+ "type": "struct",
+ "name": "PoolId",
+ "members": [
+ {"name": "value", "type": "core::integer::u256"},
+ ],
+}
+pool_id_struct = StructType("PoolId", OrderedDict(value=UintType(256)))
+
+user_dict = {
+ "type": "struct",
+ "name": "User",
+ "members": [
+ {"name": "id", "type": "core::integer::u256"},
+ {"name": "name_len", "type": "core::felt252"},
+ {"name": "name", "type": "core::array::Array::"},
+ {"name": "pool_id", "type": "PoolId"},
+ ],
+}
+user_struct = StructType(
+ "User",
+ OrderedDict(
+ id=UintType(256),
+ name_len=FeltType(),
+ name=ArrayType(FeltType()),
+ pool_id=pool_id_struct,
+ ),
+)
+
+user_added_dict = {
+ "type": "event",
+ "name": "UserAdded",
+ "inputs": [
+ {"name": "user", "type": "User"},
+ ],
+}
+user_added_event: Abi.Event = Abi.Event(
+ "UserAdded",
+ OrderedDict(user=user_struct),
+)
+
+pool_id_added_dict = {
+ "type": "event",
+ "name": "PoolIdAdded",
+ "inputs": [
+ {"name": "pool_id", "type": "PoolId"},
+ ],
+}
+pool_id_added_event: Abi.Event = Abi.Event(
+ "PoolIdAdded",
+ OrderedDict(pool_id=pool_id_struct),
+)
+
+get_user_dict = {
+ "type": "function",
+ "name": "get_user",
+ "inputs": [
+ {
+ "name": "id",
+ "type": "core::integer::u256",
+ }
+ ],
+ "outputs": [{"type": "User"}],
+}
+get_user_fn = Abi.Function("get_user", OrderedDict(id=UintType(256)), [user_struct])
+
+delete_pool_dict = {
+ "type": "function",
+ "name": "delete_pool",
+ "inputs": [
+ {
+ "name": "id",
+ "type": "PoolId",
+ },
+ {"name": "user_id", "type": "core::integer::u256"},
+ ],
+ "outputs": [],
+}
+delete_pool_fn = Abi.Function(
+ "delete_pool", OrderedDict(id=pool_id_struct, user_id=UintType(256)), []
+)
+
+
+my_struct_dict = {
+ "type": "struct",
+ "name": "test::MyStruct::",
+ "members": [
+ {"name": "a", "type": "core::integer::u256"},
+ {"name": "b", "type": "core::felt252"},
+ ],
+}
+my_struct = StructType(
+ name="test::MyStruct::",
+ types=OrderedDict(a=UintType(256), b=FeltType()),
+)
+
+my_enum_dict = {
+ "type": "enum",
+ "name": "test::MyEnum::",
+ "variants": [
+ {"name": "a", "type": "core::integer::u256"},
+ {"name": "b", "type": "test::MyStruct::"},
+ ],
+}
+my_enum = EnumType(
+ name="test::MyEnum::",
+ variants=OrderedDict(a=UintType(256), b=my_struct),
+)
+
+foo_event_dict = {
+ "type": "event",
+ "name": "foo_event",
+ "inputs": [
+ {"name": "a", "type": "core::felt252"},
+ {"name": "b", "type": "core::integer::u128"},
+ ],
+}
+foo_event = Abi.Event(
+ name="foo_event", inputs=OrderedDict(a=FeltType(), b=UintType(128))
+)
+
+foo_external_dict = {
+ "type": "function",
+ "name": "foo_external",
+ "inputs": [
+ {"name": "a", "type": "core::felt252"},
+ {"name": "b", "type": "core::integer::u128"},
+ ],
+ "outputs": [{"type": "test::MyStruct::"}],
+ "state_mutability": "external",
+}
+foo_external = Abi.Function(
+ name="foo_external",
+ inputs=OrderedDict(a=FeltType(), b=UintType(128)),
+ outputs=[my_struct],
+)
+
+foo_view_dict = {
+ "type": "function",
+ "name": "foo_view",
+ "inputs": [
+ {"name": "a", "type": "core::felt252"},
+ {"name": "b", "type": "core::integer::u128"},
+ ],
+ "outputs": [{"type": "test::MyEnum::"}],
+ "state_mutability": "view",
+}
+foo_view = Abi.Function(
+ name="foo_view",
+ inputs=OrderedDict(a=FeltType(), b=UintType(128)),
+ outputs=[my_enum],
+)
diff --git a/starknet_py/tests/e2e/fixtures/accounts.py b/starknet_py/tests/e2e/fixtures/accounts.py
index 5936580f6..7b56c6307 100644
--- a/starknet_py/tests/e2e/fixtures/accounts.py
+++ b/starknet_py/tests/e2e/fixtures/accounts.py
@@ -143,6 +143,11 @@ def full_node_account(
def net_to_base_accounts() -> List[str]:
+ if "--client=gateway" in sys.argv:
+ return ["gateway_account"]
+ if "--client=full_node" in sys.argv:
+ return ["full_node_account"]
+
accounts = ["gateway_account"]
nets = ["--net=integration", "--net=testnet", "testnet", "integration"]
diff --git a/starknet_py/tests/e2e/fixtures/clients.py b/starknet_py/tests/e2e/fixtures/clients.py
index ceb6ecd75..dab831b25 100644
--- a/starknet_py/tests/e2e/fixtures/clients.py
+++ b/starknet_py/tests/e2e/fixtures/clients.py
@@ -28,6 +28,11 @@ def net_to_clients() -> List[str]:
"""
Return client fixture names based on network in sys.argv.
"""
+ if "--client=gateway" in sys.argv:
+ return ["gateway_client"]
+ if "--client=full_node" in sys.argv:
+ return ["full_node_client"]
+
clients = ["gateway_client"]
nets = ["--net=integration", "--net=testnet", "testnet", "integration"]
diff --git a/starknet_py/tests/e2e/fixtures/contracts.py b/starknet_py/tests/e2e/fixtures/contracts.py
index a3131a03c..cf4aeeb41 100644
--- a/starknet_py/tests/e2e/fixtures/contracts.py
+++ b/starknet_py/tests/e2e/fixtures/contracts.py
@@ -1,14 +1,20 @@
# pylint: disable=redefined-outer-name
-from typing import List, Tuple
+import json
+from typing import Any, Dict, List, Optional, Tuple
import pytest
import pytest_asyncio
-from starknet_py.common import create_casm_class, create_compiled_contract
+from starknet_py.common import (
+ create_casm_class,
+ create_compiled_contract,
+ create_sierra_compiled_contract,
+)
from starknet_py.constants import FEE_CONTRACT_ADDRESS
from starknet_py.contract import Contract
from starknet_py.hash.casm_class_hash import compute_casm_class_hash
from starknet_py.net.account.base_account import BaseAccount
+from starknet_py.net.udc_deployer.deployer import Deployer
from starknet_py.tests.e2e.fixtures.constants import (
CONTRACTS_COMPILED_V1_DIR,
CONTRACTS_DIR,
@@ -110,6 +116,42 @@ async def deploy_contract(account: BaseAccount, class_hash: int, abi: List) -> C
return deployment_result.deployed_contract
+async def deploy_v1_contract(
+ account: BaseAccount,
+ contract_file_name: str,
+ class_hash: int,
+ calldata: Optional[Dict[str, Any]] = None,
+) -> Contract:
+ """
+ Deploys Cairo1.0 contract.
+
+ :param account: An account which will be used to deploy the Contract.
+ :param contract_file_name: Name of the file with code (e.g. `erc20` if filename is `erc20.cairo`).
+ :param class_hash: class_hash of the contract to be deployed.
+ :param calldata: Dict with constructor arguments (can be empty).
+ :returns: Instance of the deployed contract.
+ """
+ contract_sierra = read_contract(
+ contract_file_name + "_compiled.json", directory=CONTRACTS_COMPILED_V1_DIR
+ )
+ sierra_compiled_contract = create_sierra_compiled_contract(
+ compiled_contract=contract_sierra
+ )
+ abi = json.loads(sierra_compiled_contract.abi)
+
+ deployer = Deployer()
+ deploy_call, address = deployer.create_contract_deployment(
+ class_hash=class_hash,
+ abi=abi,
+ calldata=calldata,
+ cairo_version=1,
+ )
+ res = await account.execute(calls=deploy_call, max_fee=MAX_FEE)
+ await account.client.wait_for_tx(res.transaction_hash)
+
+ return Contract(address, abi, provider=account, cairo_version=1)
+
+
@pytest_asyncio.fixture(scope="package")
async def deployed_balance_contract(
gateway_account: BaseAccount,
diff --git a/starknet_py/tests/e2e/fixtures/contracts_v1.py b/starknet_py/tests/e2e/fixtures/contracts_v1.py
new file mode 100644
index 000000000..fb783d22e
--- /dev/null
+++ b/starknet_py/tests/e2e/fixtures/contracts_v1.py
@@ -0,0 +1,144 @@
+# pylint: disable=redefined-outer-name
+from typing import Tuple
+
+import pytest
+import pytest_asyncio
+
+from starknet_py.common import create_casm_class
+from starknet_py.hash.casm_class_hash import compute_casm_class_hash
+from starknet_py.net.account.base_account import BaseAccount
+from starknet_py.net.models import DeclareV2
+from starknet_py.tests.e2e.fixtures.constants import CONTRACTS_COMPILED_V1_DIR, MAX_FEE
+from starknet_py.tests.e2e.fixtures.misc import read_contract
+
+
+async def declare_cairo1_contract(
+ account: BaseAccount, compiled_contract: str, compiled_contract_casm: str
+) -> Tuple[int, int]:
+ casm_class_hash = compute_casm_class_hash(create_casm_class(compiled_contract_casm))
+
+ declare_tx = await account.sign_declare_v2_transaction(
+ compiled_contract=compiled_contract,
+ compiled_class_hash=casm_class_hash,
+ max_fee=MAX_FEE,
+ )
+ assert declare_tx.version == 2
+
+ resp = await account.client.declare(declare_tx)
+ await account.client.wait_for_tx(resp.transaction_hash, wait_for_accept=True)
+
+ return resp.class_hash, resp.transaction_hash
+
+
+@pytest_asyncio.fixture(scope="package")
+async def cairo1_erc20_class_hash(account: BaseAccount) -> int:
+ class_hash, _ = await declare_cairo1_contract(
+ account,
+ read_contract("erc20_compiled.json", directory=CONTRACTS_COMPILED_V1_DIR),
+ read_contract("erc20_compiled.casm", directory=CONTRACTS_COMPILED_V1_DIR),
+ )
+ return class_hash
+
+
+@pytest_asyncio.fixture(scope="package")
+async def declare_v2_hello_starknet(account: BaseAccount) -> DeclareV2:
+ compiled_contract = read_contract(
+ "hello_starknet_compiled.json", directory=CONTRACTS_COMPILED_V1_DIR
+ )
+ compiled_contract_casm = read_contract(
+ "hello_starknet_compiled.casm", directory=CONTRACTS_COMPILED_V1_DIR
+ )
+ casm_class_hash = compute_casm_class_hash(create_casm_class(compiled_contract_casm))
+
+ declare_tx = await account.sign_declare_v2_transaction(
+ compiled_contract, casm_class_hash, max_fee=MAX_FEE
+ )
+ return declare_tx
+
+
+@pytest_asyncio.fixture(scope="package")
+async def cairo1_hello_starknet_class_hash_tx_hash(
+ account: BaseAccount, declare_v2_hello_starknet: DeclareV2
+) -> Tuple[int, int]:
+ resp = await account.client.declare(declare_v2_hello_starknet)
+ await account.client.wait_for_tx(resp.transaction_hash)
+
+ return resp.class_hash, resp.transaction_hash
+
+
+@pytest.fixture(scope="package")
+def cairo1_hello_starknet_class_hash(
+ cairo1_hello_starknet_class_hash_tx_hash: Tuple[int, int]
+) -> int:
+ class_hash, _ = cairo1_hello_starknet_class_hash_tx_hash
+ return class_hash
+
+
+@pytest.fixture(scope="package")
+def cairo1_hello_starknet_tx_hash(
+ cairo1_hello_starknet_class_hash_tx_hash: Tuple[int, int]
+) -> int:
+ _, tx_hash = cairo1_hello_starknet_class_hash_tx_hash
+ return tx_hash
+
+
+@pytest_asyncio.fixture(scope="package")
+async def cairo1_minimal_contract_class_hash(account: BaseAccount) -> int:
+ class_hash, _ = await declare_cairo1_contract(
+ account,
+ read_contract(
+ "minimal_contract_compiled.json", directory=CONTRACTS_COMPILED_V1_DIR
+ ),
+ read_contract(
+ "minimal_contract_compiled.casm", directory=CONTRACTS_COMPILED_V1_DIR
+ ),
+ )
+ return class_hash
+
+
+@pytest_asyncio.fixture(scope="package")
+async def cairo1_test_contract_class_hash(account: BaseAccount) -> int:
+ class_hash, _ = await declare_cairo1_contract(
+ account,
+ read_contract(
+ "test_contract_compiled.json", directory=CONTRACTS_COMPILED_V1_DIR
+ ),
+ read_contract(
+ "test_contract_compiled.casm", directory=CONTRACTS_COMPILED_V1_DIR
+ ),
+ )
+ return class_hash
+
+
+@pytest_asyncio.fixture(scope="package")
+async def cairo1_test_enum_class_hash(account: BaseAccount) -> int:
+ class_hash, _ = await declare_cairo1_contract(
+ account,
+ read_contract("test_enum_compiled.json", directory=CONTRACTS_COMPILED_V1_DIR),
+ read_contract("test_enum_compiled.casm", directory=CONTRACTS_COMPILED_V1_DIR),
+ )
+ return class_hash
+
+
+@pytest_asyncio.fixture(scope="package")
+async def cairo1_test_option_class_hash(account: BaseAccount) -> int:
+ class_hash, _ = await declare_cairo1_contract(
+ account,
+ read_contract("test_option_compiled.json", directory=CONTRACTS_COMPILED_V1_DIR),
+ read_contract("test_option_compiled.casm", directory=CONTRACTS_COMPILED_V1_DIR),
+ )
+ return class_hash
+
+
+@pytest_asyncio.fixture(scope="package")
+async def cairo1_token_bridge_class_hash(account: BaseAccount) -> int:
+ class_hash, _ = await declare_cairo1_contract(
+ account,
+ read_contract(
+ "token_bridge_compiled.json", directory=CONTRACTS_COMPILED_V1_DIR
+ ),
+ read_contract(
+ "token_bridge_compiled.casm", directory=CONTRACTS_COMPILED_V1_DIR
+ ),
+ )
+ return class_hash
diff --git a/starknet_py/tests/e2e/fixtures/misc.py b/starknet_py/tests/e2e/fixtures/misc.py
index 0096d2e17..eb6d5fd33 100644
--- a/starknet_py/tests/e2e/fixtures/misc.py
+++ b/starknet_py/tests/e2e/fixtures/misc.py
@@ -19,6 +19,12 @@ def pytest_addoption(parser):
default="devnet",
help="Network to run tests on: possible 'testnet', 'devnet', 'all'",
)
+ parser.addoption(
+ "--client",
+ action="store",
+ default="",
+ help="Client to run tests with: possible 'gateway', 'full_node'",
+ )
def pytest_collection_modifyitems(config, items):
diff --git a/starknet_py/tests/e2e/mock/contracts_v1/hello_starknet.cairo b/starknet_py/tests/e2e/mock/contracts_v1/hello_starknet.cairo
index 41c0e8780..e7152c2dc 100644
--- a/starknet_py/tests/e2e/mock/contracts_v1/hello_starknet.cairo
+++ b/starknet_py/tests/e2e/mock/contracts_v1/hello_starknet.cairo
@@ -1,7 +1,7 @@
#[contract]
mod HelloStarknet {
struct Storage {
- balance: felt252,
+ balance: felt252,
}
// Increases the balance by the given amount.
diff --git a/starknet_py/tests/e2e/mock/contracts_v1/test_contract_declare.cairo b/starknet_py/tests/e2e/mock/contracts_v1/test_contract_declare.cairo
new file mode 100644
index 000000000..4120b5aea
--- /dev/null
+++ b/starknet_py/tests/e2e/mock/contracts_v1/test_contract_declare.cairo
@@ -0,0 +1,5 @@
+#[contract]
+mod TestContractDeclare {
+ #[view]
+ fn empty2() {}
+}
diff --git a/starknet_py/tests/e2e/mock/contracts_v1/test_enum.cairo b/starknet_py/tests/e2e/mock/contracts_v1/test_enum.cairo
new file mode 100644
index 000000000..ebc2f35bc
--- /dev/null
+++ b/starknet_py/tests/e2e/mock/contracts_v1/test_enum.cairo
@@ -0,0 +1,32 @@
+use serde::Serde;
+
+#[derive(Copy, Drop, Serde)]
+enum MyEnum {
+ a: u256,
+ b: u128,
+ c: ()
+}
+
+#[contract]
+mod TestEnum {
+ use super::MyEnum;
+
+ #[view]
+ fn receive_and_send_enum(my_enum: MyEnum) -> MyEnum {
+ my_enum
+ }
+
+ #[view]
+ fn get_enum() -> MyEnum {
+ let my_enum = MyEnum::a(u256{low: 100, high: 0});
+
+ my_enum
+ }
+
+ #[view]
+ fn get_enum_without_value() -> MyEnum {
+ let my_enum = MyEnum::c(());
+
+ my_enum
+ }
+}
diff --git a/starknet_py/tests/e2e/mock/contracts_v1/test_option.cairo b/starknet_py/tests/e2e/mock/contracts_v1/test_option.cairo
new file mode 100644
index 000000000..7e9a5359c
--- /dev/null
+++ b/starknet_py/tests/e2e/mock/contracts_v1/test_option.cairo
@@ -0,0 +1,38 @@
+use serde::Serde;
+use array::SpanTrait;
+
+
+#[derive(Copy, Drop, Serde)]
+struct OptionStruct {
+ first_field: felt252,
+ second_field: Option::,
+ third_field: Option::,
+ fourth_field: felt252
+}
+
+#[contract]
+mod HelloStarknet {
+ use super::OptionStruct;
+
+ #[view]
+ fn receive_and_send_option_struct(option_struct: OptionStruct) -> OptionStruct {
+ option_struct
+ }
+
+ #[view]
+ fn get_option_struct() -> OptionStruct {
+ let option_struct = OptionStruct {
+ first_field: 1,
+ second_field: Option::Some(u256{low: 2, high: 0}),
+ third_field: Option::None(()),
+ fourth_field: 4
+ };
+
+ option_struct
+ }
+
+ #[view]
+ fn get_empty_option() -> Option::<()> {
+ Option::Some(())
+ }
+}
diff --git a/starknet_py/utils/contructor_args_translator.py b/starknet_py/utils/contructor_args_translator.py
index 5d099018f..94f126e3a 100644
--- a/starknet_py/utils/contructor_args_translator.py
+++ b/starknet_py/utils/contructor_args_translator.py
@@ -1,16 +1,24 @@
from typing import List, Optional, Union
from starknet_py.abi.parser import AbiParser
-from starknet_py.serialization import serializer_for_function
+from starknet_py.abi.v1.parser import AbiParser as AbiV1Parser
+from starknet_py.serialization import (
+ FunctionSerializationAdapter,
+ serializer_for_function,
+)
+from starknet_py.serialization.factory import serializer_for_function_v1
def translate_constructor_args(
- abi: List, constructor_args: Optional[Union[List, dict]]
+ abi: List, constructor_args: Optional[Union[List, dict]], *, cairo_version: int = 0
) -> List[int]:
- parsed = AbiParser(abi).parse()
+ serializer = (
+ _get_constructor_serializer_v1(abi)
+ if cairo_version == 1
+ else _get_constructor_serializer_v0(abi)
+ )
- # Constructor might not accept any arguments
- if not parsed.constructor or not parsed.constructor.inputs:
+ if serializer is None:
return []
if not constructor_args:
@@ -23,4 +31,25 @@ def translate_constructor_args(
if isinstance(constructor_args, dict)
else (constructor_args, {})
)
- return serializer_for_function(parsed.constructor).serialize(*args, **kwargs)
+ return serializer.serialize(*args, **kwargs)
+
+
+def _get_constructor_serializer_v1(abi: List) -> Optional[FunctionSerializationAdapter]:
+ parsed = AbiV1Parser(abi).parse()
+ constructor = parsed.functions.get("constructor", None)
+
+ # Constructor might not accept any arguments
+ if constructor is None or not constructor.inputs:
+ return None
+
+ return serializer_for_function_v1(constructor)
+
+
+def _get_constructor_serializer_v0(abi: List) -> Optional[FunctionSerializationAdapter]:
+ parsed = AbiParser(abi).parse()
+
+ # Constructor might not accept any arguments
+ if not parsed.constructor or not parsed.constructor.inputs:
+ return None
+
+ return serializer_for_function(parsed.constructor)