From d0cae75d667ce9d6e5e5dad6b3030178a30bb74b Mon Sep 17 00:00:00 2001 From: Lyno Ferraz Date: Mon, 14 Oct 2024 13:59:34 -0300 Subject: [PATCH 01/21] feat: upgrade to node v2. Remove dapp rellay, change dapp->app --- README.md | 127 ++++++++++++++------------------ cartesi/__init__.py | 2 +- cartesi/{dapp.py => app.py} | 2 +- cartesi/models.py | 6 +- cartesi/rollup.py | 7 +- cartesi/router/__init__.py | 1 - cartesi/router/dapp_address.py | 18 ----- cartesi/testclient.py | 8 +- cartesi/vouchers.py | 34 +++++---- cartesi/wallet/ether.py | 11 ++- examples/echo.py | 10 +-- examples/ether_wallet.py | 10 +-- examples/json_handler.py | 10 +-- examples/url_router.py | 8 +- pyproject.toml | 2 +- tests/test_echo.py | 14 ++-- tests/test_ether_wallet.py | 16 ++-- tests/test_json_handler_app.py | 33 +++++++++ tests/test_json_handler_dapp.py | 33 --------- tests/test_url_router_app.py | 65 ++++++++++++++++ tests/test_url_router_dapp.py | 65 ---------------- 21 files changed, 234 insertions(+), 248 deletions(-) rename cartesi/{dapp.py => app.py} (99%) delete mode 100644 cartesi/router/dapp_address.py create mode 100644 tests/test_json_handler_app.py delete mode 100644 tests/test_json_handler_dapp.py create mode 100644 tests/test_url_router_app.py delete mode 100644 tests/test_url_router_dapp.py diff --git a/README.md b/README.md index d68d5ff..a604a99 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,13 @@ ## Overview -The Cartesi HLF is a framework developing DApps that run inside the [Cartesi](https://cartesi.io/) machine. +The Cartesi HLF is a framework developing Apps that run inside the [Cartesi](https://cartesi.io/) machine. The main goals of the framework are: - **Pythonic**: Offer a idiomatic way of writing the code and specifying the interactions. - **Easy to understand**: Inspired on widely used web frameworks, and have a clear to use interface. -- **Testability**: Have test as a first class citizen, giving the developer tools to write tests for the DApps that run on your local Python environment. +- **Testability**: Have test as a first class citizen, giving the developer tools to write tests for the Apps that run on your local Python environment. - **Flexibility**: You're free to take full control of the inputs and outputs for cases where the given high level tools are not enough. ## Installation @@ -19,23 +19,23 @@ To install the framework you just have to do a simple: pip install python-cartesi ``` -Although this is a pure Python library, it depends on PyCryptodome, which will need to compile some source code. You are advised to either include `build-essential` in the `apt-get install` command of your DApp's Dockerfile or include the line `--find-links https://prototyp3-dev.github.io/pip-wheels-riscv/wheels/` in the beginning of your requirements.txt file in order to use a pre-built binary for RiscV. +Although this is a pure Python library, it depends on PyCryptodome, which will need to compile some source code. You are advised to either include `build-essential` in the `apt-get install` command of your App's Dockerfile or include the line `--find-links https://prototyp3-dev.github.io/pip-wheels-riscv/wheels/` in the beginning of your requirements.txt file in order to use a pre-built binary for RiscV. ## Getting Started -A very simple DApp that simply echoes in a notice whatever input is sent to it can be seen below: +A very simple App that simply echoes in a notice whatever input is sent to it can be seen below: ```python import logging -from cartesi import DApp, Rollup, RollupData +from cartesi import App, Rollup, RollupData LOGGER = logging.getLogger(__name__) logging.basicConfig(level=logging.DEBUG) -dapp = DApp() +app = App() -@dapp.advance() +@app.advance() def handle_advance(rollup: Rollup, data: RollupData) -> bool: payload = data.str_payload() LOGGER.debug("Echoing '%s'", payload) @@ -44,10 +44,10 @@ def handle_advance(rollup: Rollup, data: RollupData) -> bool: if __name__ == '__main__': - dapp.run() + app.run() ``` -The handle_advance function will be registered as the DApp's default route of advance state requests by the `dapp.advance()` decorator. The framework also supplies several routers that will offer you convenient ways of handling inputs of commonly used formats. These routers will be discussed in the [Routers](#routers) section. +The handle_advance function will be registered as the App's default route of advance state requests by the `app.advance()` decorator. The framework also supplies several routers that will offer you convenient ways of handling inputs of commonly used formats. These routers will be discussed in the [Routers](#routers) section. The handler function also receives two inputs: an instance of a `Rollup` object, that will allow you to interact with the Rollup Server, and an instance of `RollupData`, that contains all the inputs and metadata for the current transaction. @@ -78,21 +78,21 @@ Adds a new [voucher](https://docs.cartesi.io/cartesi-rollups/main-concepts/#vouc Routers simplify the coding experience by identifying the request type using common patterns in the input data, and calling your handler only when several conditions are met. -Once an input is received by either an advance-state or inspect request, the DApp will go through the list of registered handlers, find the first match and execute it. Each handler should return a boolean indicating whether the transaction was successful or not. If a handler for an advance state request returns false, the state of the DApp will be reverted to what it was before the transaction was received. +Once an input is received by either an advance-state or inspect request, the App will go through the list of registered handlers, find the first match and execute it. Each handler should return a boolean indicating whether the transaction was successful or not. If a handler for an advance state request returns false, the state of the App will be reverted to what it was before the transaction was received. -To use a router, it must be explicitly instantiated and added to the DApp. For example, to use a JSON Router, you should adapt your DApp code to include the `add_router()` call, like the snippet below: +To use a router, it must be explicitly instantiated and added to the App. For example, to use a JSON Router, you should adapt your App code to include the `add_router()` call, like the snippet below: ```python -from cartesi import DApp, JSONRouter +from cartesi import App, JSONRouter -# Create a DApp instance -dapp = DApp() +# Create a App instance +app = App() # Instantiate the JSON Router json_router = JSONRouter() -# Register the JSON Router into the DApp -dapp.add_router(json_router) +# Register the JSON Router into the App +app.add_router(json_router) ``` ### JSON Router @@ -107,11 +107,11 @@ The JSON Router expose two decorators methods: `advance(route_dict)` and `inspec For example, a route that handles the creation of a profile could be coded as below: ```python -from cartesi import DApp, Rollup, RollupData, JSONRouter +from cartesi import App, Rollup, RollupData, JSONRouter -dapp = DApp() +app = App() json_router = JSONRouter() -dapp.add_router(json_router) +app.add_router(json_router) @json_router.advance({"op": "create-profile"}) def handle_create_profile(rollup: Rollup, data: RollupData): @@ -121,10 +121,10 @@ def handle_create_profile(rollup: Rollup, data: RollupData): return True if __name__ == '__main__': - dapp.run() + app.run() ``` -For this DApp, if the data incoming from the Cartesi input is the equivalent to the JSON `{"op": "create-profile", "name": "John Doe"}`, router will match due to the presence of the `"op":"create-profile"` key-value pair, and the handler should generate a report containing the string "John Doe". +For this App, if the data incoming from the Cartesi input is the equivalent to the JSON `{"op": "create-profile", "name": "John Doe"}`, router will match due to the presence of the `"op":"create-profile"` key-value pair, and the handler should generate a report containing the string "John Doe". ### ABI Router @@ -142,11 +142,11 @@ To match with headers, you should pass an instance of a subclass of `ABIHeader` Matches with a literal header supplied by the developer in the `header` attribute, as bytes. For example, the following handler matches with inputs starting with the bytes `0x01020304`: ```python -from cartesi import DApp, Rollup, RollupData, ABIRouter, ABILiteralHeader +from cartesi import App, Rollup, RollupData, ABIRouter, ABILiteralHeader -dapp = DApp() +app = App() abi_router = ABIRouter() -dapp.add_router(abi_router) +app.add_router(abi_router) @abi_router.advance(header=ABILiteralHeader(header=bytes.fromhex('01020304'))) def handle_input_1234(rollup: Rollup, data: RollupData): @@ -187,11 +187,11 @@ class MyCustomHeader(ABIHeader): The `msg_sender` parameter for the advance decorator method of the `ABIRouter` will match not with the contents but with the sender of the message. For example, to match with the Cartesi's Ether Portal, you can declare a route like the code below: ```python -from cartesi import DApp, Rollup, RollupData, ABIRouter, ABILiteralHeader +from cartesi import App, Rollup, RollupData, ABIRouter, ABILiteralHeader -dapp = DApp() +app = App() abi_router = ABIRouter() -dapp.add_router(abi_router) +app.add_router(abi_router) ETHER_PORTAL = '0xffdbe43d4c855bf7e0f105c400a50857f53ab044' @@ -200,7 +200,7 @@ def handle_deposit(rollup: Rollup, data: RollupData): ... ``` -In this example, the `handle_deposit` function will be called whenever the Ether portal sends an input to the DApp. +In this example, the `handle_deposit` function will be called whenever the Ether portal sends an input to the App. Both the `msg_sender` and `header` parameters can be set at the same time. In this case, the message must match with both criteria to trigger the execution of the handler. @@ -221,14 +221,14 @@ The handler can receive a third argument of the `URLParameter` type. This object > [!IMPORTANT] > It is mandatory to correctly annotate the handler's parameters with type hints. The URLHandler will use this information to dynamically determine what information to send to the handler. -The code fragment for the DApp below, for example, will return a report containing the string 'Hello World' when the user send an input `hello/world`. When running with sunodo, this can be achieved by sending an HTTP GET request to `http://localhost:8000/inspect/hello/world`. +The code fragment for the App below, for example, will return a report containing the string 'Hello World' when the user send an input `hello/world`. When running with sunodo, this can be achieved by sending an HTTP GET request to `http://localhost:8000/inspect/hello/world`. ```python -from cartesi import DApp, Rollup, URLRouter, URLParameters +from cartesi import App, Rollup, URLRouter, URLParameters -dapp = DApp() +app = App() url_router = URLRouter() -dapp.add_router(url_router) +app.add_router(url_router) @url_router.inspect('hello/{name}') def hello_world_inspect_params(rollup: Rollup, params: URLParameters) -> bool: @@ -237,34 +237,18 @@ def hello_world_inspect_params(rollup: Rollup, params: URLParameters) -> bool: return True ``` -### DApp Relay Router +### The App default Router -This is a very simple router which will receive and accumulate the DApp's contract address, as reported by the DApp address relay contact. The router itself only exposes an attribute called `address`, that will be initialized as None and set to the address reported by the relay contract once it is received. +The App object itself exposes two decorators: `advance()` and `inspect()`. The handled decorated with these methods will be called if none of the available routes match. They act, therefore, as a default handler for each type of request. This can be used to both create more specific error handlers for your application, or to handle specific cases not covered by a generic router. -```python -from cartesi import DApp -from cartesi.router import DAppAddressRouter - -ADDRESS_RELAY_ADDRESS = '0xf5de34d6bbc0446e2a45719e718efebaae179dae' - -dapp = DApp() - -dapp_address = DAppAddressRouter(relay_address=ADDRESS_RELAY_ADDRESS) -dapp.add_router(dapp_address) -``` - -### The DApp default Router - -The DApp object itself exposes two decorators: `advance()` and `inspect()`. The handled decorated with these methods will be called if none of the available routes match. They act, therefore, as a default handler for each type of request. This can be used to both create more specific error handlers for your application, or to handle specific cases not covered by a generic router. - -For example, given the following DApp: +For example, given the following App: ```python -from cartesi import DApp, Rollup, RollupData, JSONRouter +from cartesi import App, Rollup, RollupData, JSONRouter -dapp = DApp() +app = App() json_router = JSONRouter() -dapp.add_router(json_router) +app.add_router(json_router) @json_router.advance({"op": "create-profile"}) def handle_create_profile(rollup: Rollup, data: RollupData): @@ -273,22 +257,22 @@ def handle_create_profile(rollup: Rollup, data: RollupData): rollup.report('0x' + name.encode('utf-8').hex()) return True -@dapp.advance() +@app.advance() def default_handler(rollup: Rollup, data: RollupData): rollup.report('0x' + 'Unknown Operation'.encode('utf-8').hex()) return True if __name__ == '__main__': - dapp.run() + app.run() ``` If the user passes an invalid JSON or a document that does not contain the `"op":"create-profile"` key-value pair, the `handle_create_profile` route will not match and the framework will call the `default_handler` function with the input. ## Testing -Testing is an important part of the development of complex software. The framework provides a TestClient that can be used to interact a DApp inside automated tests. The constructor of the `TestClient` class expects a fully configured instance of the `DApp` class, and expose methods for sending advance and inspect requests. +Testing is an important part of the development of complex software. The framework provides a TestClient that can be used to interact a App inside automated tests. The constructor of the `TestClient` class expects a fully configured instance of the `App` class, and expose methods for sending advance and inspect requests. -For example, supposing we have an `echo.py` file with an implementation of an echo DApp, we could write the following file for automated tests using pytest: +For example, supposing we have an `echo.py` file with an implementation of an echo App, we could write the following file for automated tests using pytest: ```python from cartesi.testclient import TestClient @@ -297,18 +281,18 @@ import pytest import echo @pytest.fixture -def dapp_client() -> TestClient: - client = TestClient(echo.dapp) +def app_client() -> TestClient: + client = TestClient(echo.app) return client -def test_simple_echo(dapp_client: TestClient): +def test_simple_echo(app_client: TestClient): hex_payload = '0x' + 'hello'.encode('utf-8').hex() - dapp_client.send_advance(hex_payload=hex_payload) + app_client.send_advance(hex_payload=hex_payload) - assert dapp_client.rollup.status - assert len(dapp_client.rollup.notices) > 0 - assert dapp_client.rollup.notices[-1]['data']['payload'] == hex_payload + assert app_client.rollup.status + assert len(app_client.rollup.notices) > 0 + assert app_client.rollup.notices[-1]['data']['payload'] == hex_payload ``` Although the example above was written using pytest, the TestClient makes no assumption about the testing framework, so it should work equally well using the python's builtin unittest module or other automated test frameworks. @@ -326,11 +310,11 @@ Sends an **advance state** input, such as one being received from the underlying Sends an **inspect** input, such as one being received from the [Inspect dApp state REST API](https://docs.cartesi.io/cartesi-rollups/api/inspect/inspect/). It only expects a hex encoded string, starting with `0x`, with the inspect payload. -For example, if you are running your DApp with sunodo, and want to simulate a the effects of a call to `http://localhost:8000/inspect/hello/world`, the value that should be passed in the hex_payload is `'0x68656c6c6f2f776f726c64'`, which is the hex encoded representation of `hello/world`. +For example, if you are running your App with sunodo, and want to simulate a the effects of a call to `http://localhost:8000/inspect/hello/world`, the value that should be passed in the hex_payload is `'0x68656c6c6f2f776f726c64'`, which is the hex encoded representation of `hello/world`. **`TestClient.rollup`** -This is an instance of a test double implementation of the rollup server. This object will contain attributes holding all the notices, reports and vouchers emitted by the DApp, together with the state of the last transaction. The individual attributes are listed below. +This is an instance of a test double implementation of the rollup server. This object will contain attributes holding all the notices, reports and vouchers emitted by the App, together with the state of the last transaction. The individual attributes are listed below. **`TestClient.rollup.status`** @@ -348,7 +332,7 @@ For notices and reports, the payload will be a hex encoded string, starting with ## Generating Vouchers -A voucher is an output that your DApp can generate to perform a transaction in the base layer blockchain. Once emitted, and finalized, the voucher can be retrieved by an external agent through the GraphQL API and then submitted to the DApp on-chain contract so that the desired transaction take place. Since it represents a full transaction, the voucher payload should be a full function call encoded according to the Solidity [Contract ABI Specification](https://docs.soliditylang.org/en/latest/abi-spec.html). +A voucher is an output that your App can generate to perform a transaction in the base layer blockchain. Once emitted, and finalized, the voucher can be retrieved by an external agent through the GraphQL API and then submitted to the App on-chain contract so that the desired transaction take place. Since it represents a full transaction, the voucher payload should be a full function call encoded according to the Solidity [Contract ABI Specification](https://docs.soliditylang.org/en/latest/abi-spec.html). This framework offers a pythonic way for representing the function calls and generating the voucher payload. The high level way of generating a voucher involves creating a [Pydantic](https://docs.pydantic.dev/1.10/) model with specially annotated type hints that will allow the encoder to understand which Solidity type should be used when encoding the values. @@ -380,7 +364,8 @@ my_withdrawal = TransferArgs(to=receiver_address, value=value) voucher = create_voucher_from_model( destination=erc20_contract_address, function_name='transfer', - args_model=my_withdrawal + args_model=my_withdrawal, + value=0 ) ``` @@ -422,11 +407,11 @@ Generate a voucher for transferring Ethers from the contract to the receiver. Th - **`receiver`**: Hex encoded address, starting with `0x`, of the receiver of Ethers - **`amount`**: Amount of ethers to transfer -**`withdraw_erc20(rollup_address, token, receiver, amount)`** +**`withdraw_erc20(app_contract, token, receiver, amount)`** Generate a voucher for transferring ERC20 tokens owned by the contract to a receiver. The parameters are -- **`rollup_address`**: The hex encoded address, starting with `0x`, of the current DApp. See the `DAppAddressRouter` above for a programmatic way of obtaining this value. +- **`app_contract`**: The hex encoded address, starting with `0x`, of the current App. All inputs inform this address the `app_contract` above for a programmatic way of obtaining this value. - **`token`**: The hex encoded address, starting with `0x`, of the ERC20 token contract - **`receiver`**: The hex encoded address, starting with `0x`, of the receiver of tokens - **`amount`**: Amount of tokens to transfer diff --git a/cartesi/__init__.py b/cartesi/__init__.py index d92c011..0a4cde4 100644 --- a/cartesi/__init__.py +++ b/cartesi/__init__.py @@ -1,6 +1,6 @@ """Framework for building distributed applications for Cartesi Rollups""" -from .dapp import DApp # noqa +from .app import App # noqa from .models import ( # noqa ABIFunctionSelectorHeader, ABILiteralHeader, diff --git a/cartesi/dapp.py b/cartesi/app.py similarity index 99% rename from cartesi/dapp.py rename to cartesi/app.py index 011c04a..29a0669 100644 --- a/cartesi/dapp.py +++ b/cartesi/app.py @@ -9,7 +9,7 @@ ROLLUP_SERVER = os.environ.get('ROLLUP_HTTP_SERVER_URL') -class DApp: +class App: def __init__(self): self.routers: list[Router] = [] diff --git a/cartesi/models.py b/cartesi/models.py index 2870747..6844679 100644 --- a/cartesi/models.py +++ b/cartesi/models.py @@ -20,11 +20,13 @@ def _str2hex(str): class RollupMetadata(BaseModel): + chain_id: int + app_contract: str msg_sender: str - epoch_index: int input_index: int block_number: int - timestamp: int + block_timestamp: int + prev_randao: str class RollupData(BaseModel): diff --git a/cartesi/rollup.py b/cartesi/rollup.py index ab05d55..55e0b06 100644 --- a/cartesi/rollup.py +++ b/cartesi/rollup.py @@ -27,13 +27,16 @@ def main_loop(self): pass @abstractmethod - def notice(self, payload) -> str: + def notice(self, payload: str) -> str: pass @abstractmethod - def report(self, payload) -> str: + def report(self, payload: str) -> str: pass + @abstractmethod + def voucher(self, payload: dict) -> str: + pass class HTTPRollupServer(Rollup): """HTTP Communication with Rollup Server based on Requests""" diff --git a/cartesi/router/__init__.py b/cartesi/router/__init__.py index c73727a..ae0e746 100644 --- a/cartesi/router/__init__.py +++ b/cartesi/router/__init__.py @@ -3,4 +3,3 @@ from .url import URLRouter, URLParameters # noqa from .abi import ABIRouter # noqa from .multi import MultiRouter # noqa -from .dapp_address import DAppAddressRouter # noqa diff --git a/cartesi/router/dapp_address.py b/cartesi/router/dapp_address.py deleted file mode 100644 index 652f4af..0000000 --- a/cartesi/router/dapp_address.py +++ /dev/null @@ -1,18 +0,0 @@ -from .abi import ABIRouter - -from ..models import RollupData -from ..rollup import Rollup - - -class DAppAddressRouter(ABIRouter): - - def __init__(self, relay_address: str): - super().__init__() - - self.address = None - - @self.advance(msg_sender=relay_address) - def set_dapp_address(rollup: Rollup, data: RollupData) -> bool: - addr_bytes = data.bytes_payload() - self.address = '0x' + addr_bytes.hex() - return True diff --git a/cartesi/testclient.py b/cartesi/testclient.py index d776dea..0a3c505 100644 --- a/cartesi/testclient.py +++ b/cartesi/testclient.py @@ -18,6 +18,8 @@ def __init__(self): self.input = 0 self.block = 0 self.status = None + self.chain_id = 31337 + self.app_contract = f"{1:#042x}" def main_loop(self): """There is no main loop for test rollup.""" @@ -66,11 +68,13 @@ def send_advance( 'request_type': 'advance_state', 'data': { 'metadata': { + 'chain_id': self.chain_id, + 'app_contract': self.app_contract, 'msg_sender': msg_sender, - 'epoch_index': self.epoch, 'input_index': self.input, 'block_number': self.block, - 'timestamp': timestamp, + 'block_timestamp': timestamp, + 'prev_randao': f"{self.block:#066x}" }, 'payload': hex_payload, } diff --git a/cartesi/vouchers.py b/cartesi/vouchers.py index 662f910..c7da810 100644 --- a/cartesi/vouchers.py +++ b/cartesi/vouchers.py @@ -9,8 +9,9 @@ def create_voucher_from_model( destination: abi.Address, - function_name: str, - args_model: BaseModel + function_name: str | None = None, + args_model: BaseModel | None = None, + value: abi.UInt256 | None = None ): """ Generates a voucher for a given contract, function and arguments. @@ -32,17 +33,24 @@ def create_voucher_from_model( dict Dictionary ready to be passed to rollup.voucher(). """ - args_types = abi.get_abi_types_from_model(args_model) - signature = f'{function_name}({",".join(args_types)})' - sig_hash = keccak.new(digest_bits=256) - sig_hash.update(signature.encode('utf-8')) + selector = "" + args = "" + if args_model is not None and function_name is not None : + args_types = abi.get_abi_types_from_model(args_model) + signature = f'{function_name}({",".join(args_types)})' + sig_hash = keccak.new(digest_bits=256) + sig_hash.update(signature.encode('utf-8')) - selector = sig_hash.digest()[:4].hex() + selector = sig_hash.digest()[:4].hex() - args = abi.encode_model(args_model).hex() + args = abi.encode_model(args_model).hex() + + if value is None: + value = 0 voucher = { 'destination': destination, + 'value': f"{value:#066}x", 'payload': '0x' + selector + args } return voucher @@ -54,7 +62,6 @@ class WithdrawEtherParams(BaseModel): def withdraw_ether( - rollup_address, receiver_address: abi.Address, amount: abi.UInt256 ): @@ -63,9 +70,8 @@ def withdraw_ether( amount=amount, ) return create_voucher_from_model( - destination=rollup_address, - function_name='withdrawEther', - args_model=params, + destination=params.receiver, + value=params.amount ) @@ -76,7 +82,7 @@ class WithdrawERC20Params(BaseModel): def withdraw_erc20( - rollup_address, + app_contract, token, receiver_address: abi.Address, amount: abi.UInt256 @@ -87,7 +93,7 @@ def withdraw_erc20( amount=amount, ) return create_voucher_from_model( - destination=rollup_address, + destination=app_contract, function_name='withdrawERC20Tokens', args_model=params, ) diff --git a/cartesi/wallet/ether.py b/cartesi/wallet/ether.py index f3d2fbe..5624676 100644 --- a/cartesi/wallet/ether.py +++ b/cartesi/wallet/ether.py @@ -6,7 +6,8 @@ from .. import abi from ..models import RollupData, ABIFunctionSelectorHeader from ..rollup import Rollup -from ..router import MultiRouter, ABIRouter, URLRouter, DAppAddressRouter +from ..router import MultiRouter, ABIRouter, URLRouter +from ..vouchers import create_voucher_from_model LOGGER = logging.getLogger(__name__) @@ -35,13 +36,11 @@ class EtherWallet(MultiRouter): def __init__( self, portal_address: str, - dapp_address_router: DAppAddressRouter, default_withdraw_route: bool = True, ): super().__init__() self.balance: dict[str, int] = {} self.portal_address = portal_address - self.dapp_address_router = dapp_address_router self.on_deposit = None @@ -129,5 +128,11 @@ def _withdraw_ether( wallet.balance[address] -= withdrawal.amount # Generate Voucher + rollup.voucher( + create_voucher_from_model( + destination=address, + value=withdrawal.amount + ) + ) return True diff --git a/examples/echo.py b/examples/echo.py index a73bbd4..85cd8aa 100644 --- a/examples/echo.py +++ b/examples/echo.py @@ -1,10 +1,10 @@ import logging -from cartesi import DApp, Rollup, RollupData +from cartesi import App, Rollup, RollupData LOGGER = logging.getLogger(__name__) logging.basicConfig(level=logging.DEBUG) -dapp = DApp() +app = App() def str2hex(str): @@ -12,7 +12,7 @@ def str2hex(str): return "0x" + str.encode("utf-8").hex() -@dapp.advance() +@app.advance() def handle_advance(rollup: Rollup, data: RollupData) -> bool: payload = data.str_payload() LOGGER.debug("Echoing '%s'", payload) @@ -20,7 +20,7 @@ def handle_advance(rollup: Rollup, data: RollupData) -> bool: return True -@dapp.inspect() +@app.inspect() def handle_inspect(rollup: Rollup, data: RollupData) -> bool: payload = data.str_payload() LOGGER.debug("Echoing '%s'", payload) @@ -29,4 +29,4 @@ def handle_inspect(rollup: Rollup, data: RollupData) -> bool: if __name__ == '__main__': - dapp.run() + app.run() diff --git a/examples/ether_wallet.py b/examples/ether_wallet.py index 4ef1142..f6b2a9e 100644 --- a/examples/ether_wallet.py +++ b/examples/ether_wallet.py @@ -1,18 +1,18 @@ import logging -from cartesi import DApp +from cartesi import App from cartesi.wallet.ether import EtherWallet LOGGER = logging.getLogger(__name__) logging.basicConfig(level=logging.DEBUG) -dapp = DApp() +app = App() -ETHER_PORTAL_ADDRESS = '0xffdbe43d4c855bf7e0f105c400a50857f53ab044' +ETHER_PORTAL_ADDRESS = '0x1733b13aAbcEcf3464157Bd7954Bd7e4Cf91Ce22' ether_wallet = EtherWallet(portal_address=ETHER_PORTAL_ADDRESS) -dapp.add_router(ether_wallet) +app.add_router(ether_wallet) if __name__ == '__main__': - dapp.run() + app.run() diff --git a/examples/json_handler.py b/examples/json_handler.py index a7a24c3..2899558 100644 --- a/examples/json_handler.py +++ b/examples/json_handler.py @@ -1,16 +1,16 @@ import json import logging -from cartesi import DApp, Rollup, RollupData, JSONRouter +from cartesi import App, Rollup, RollupData, JSONRouter LOGGER = logging.getLogger(__name__) logging.basicConfig(level=logging.DEBUG) -dapp = DApp() +app = App() json_router = JSONRouter() -dapp.add_router(json_router) +app.add_router(json_router) -# This dapp will read and write from this global state dict +# This app will read and write from this global state dict STATE = {} @@ -63,4 +63,4 @@ def handle_inspect_get(rollup: Rollup, data: RollupData): if __name__ == '__main__': - dapp.run() + app.run() diff --git a/examples/url_router.py b/examples/url_router.py index be285fe..e6a920d 100644 --- a/examples/url_router.py +++ b/examples/url_router.py @@ -1,12 +1,12 @@ import logging -from cartesi import DApp, Rollup, RollupData, URLRouter, URLParameters +from cartesi import App, Rollup, RollupData, URLRouter, URLParameters LOGGER = logging.getLogger(__name__) logging.basicConfig(level=logging.DEBUG) -dapp = DApp() +app = App() url_router = URLRouter() -dapp.add_router(url_router) +app.add_router(url_router) def str2hex(str): @@ -37,4 +37,4 @@ def hello_world_inspect_parms(rollup: Rollup, params: URLParameters) -> bool: if __name__ == '__main__': - dapp.run() + app.run() diff --git a/pyproject.toml b/pyproject.toml index 8c175d3..891a888 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "python-cartesi" -version = "0.1.1" +version = "0.2.0" authors = [ { name="Felipe Fink Grael", email="felipefg@prototyp3.dev" }, ] diff --git a/tests/test_echo.py b/tests/test_echo.py index c2823e3..c84077d 100644 --- a/tests/test_echo.py +++ b/tests/test_echo.py @@ -6,16 +6,16 @@ @pytest.fixture -def dapp_client() -> TestClient: - client = TestClient(examples.echo.dapp) +def app_client() -> TestClient: + client = TestClient(examples.echo.app) return client -def test_simple_echo(dapp_client: TestClient): +def test_simple_echo(app_client: TestClient): hex_payload = '0x' + 'hello'.encode('utf-8').hex() - dapp_client.send_advance(hex_payload=hex_payload) + app_client.send_advance(hex_payload=hex_payload) - assert dapp_client.rollup.status - assert len(dapp_client.rollup.notices) > 0 - assert dapp_client.rollup.notices[-1]['data']['payload'] == hex_payload + assert app_client.rollup.status + assert len(app_client.rollup.notices) > 0 + assert app_client.rollup.notices[-1]['data']['payload'] == hex_payload diff --git a/tests/test_ether_wallet.py b/tests/test_ether_wallet.py index e029b5d..0e57937 100644 --- a/tests/test_ether_wallet.py +++ b/tests/test_ether_wallet.py @@ -9,8 +9,8 @@ @pytest.fixture -def dapp_client() -> TestClient: - client = TestClient(examples.ether_wallet.dapp) +def app_client() -> TestClient: + client = TestClient(examples.ether_wallet.app) return client @@ -26,24 +26,24 @@ def deposit_payload() -> str: return payload -def test_should_handle_deposit(dapp_client: TestClient, deposit_payload: str): +def test_should_handle_deposit(app_client: TestClient, deposit_payload: str): # Send the Deposit - dapp_client.send_advance( + app_client.send_advance( hex_payload=deposit_payload, msg_sender=examples.ether_wallet.ETHER_PORTAL_ADDRESS, ) # Deposit should succeed - assert dapp_client.rollup.status + assert app_client.rollup.status # Send the inspect path = 'balance/ether' inspect_payload = '0x' + path.encode('ascii').hex() - dapp_client.send_inspect(hex_payload=inspect_payload) + app_client.send_inspect(hex_payload=inspect_payload) - assert dapp_client.rollup.status + assert app_client.rollup.status - report = dapp_client.rollup.reports[-1]['data']['payload'] + report = app_client.rollup.reports[-1]['data']['payload'] report = bytes.fromhex(report[2:]) report = json.loads(report.decode('utf-8')) print(json.dumps(report, indent=4)) diff --git a/tests/test_json_handler_app.py b/tests/test_json_handler_app.py new file mode 100644 index 0000000..5d34fff --- /dev/null +++ b/tests/test_json_handler_app.py @@ -0,0 +1,33 @@ +import pytest + +from cartesi.testclient import TestClient + +import examples.json_handler +from examples.json_handler import to_jsonhex + + +@pytest.fixture +def app_client() -> TestClient: + client = TestClient(examples.json_handler.app) + return client + + +def test_simple_set_get(app_client: TestClient): + + set_payload = to_jsonhex( + {'op': 'set', 'key': 'key_1', 'value': 'value_1'} + ) + app_client.send_advance(hex_payload=set_payload) + + assert app_client.rollup.status + assert len(app_client.rollup.notices) == 0 + + get_payload = to_jsonhex( + {'op': 'get', 'key': 'key_1'} + ) + app_client.send_advance(hex_payload=get_payload) + assert app_client.rollup.status + expected_payload = to_jsonhex( + {'key': 'key_1', 'value': 'value_1'} + ) + assert app_client.rollup.notices[-1]['data']['payload'] == expected_payload diff --git a/tests/test_json_handler_dapp.py b/tests/test_json_handler_dapp.py deleted file mode 100644 index 93fcbc2..0000000 --- a/tests/test_json_handler_dapp.py +++ /dev/null @@ -1,33 +0,0 @@ -import pytest - -from cartesi.testclient import TestClient - -import examples.json_handler -from examples.json_handler import to_jsonhex - - -@pytest.fixture -def dapp_client() -> TestClient: - client = TestClient(examples.json_handler.dapp) - return client - - -def test_simple_set_get(dapp_client: TestClient): - - set_payload = to_jsonhex( - {'op': 'set', 'key': 'key_1', 'value': 'value_1'} - ) - dapp_client.send_advance(hex_payload=set_payload) - - assert dapp_client.rollup.status - assert len(dapp_client.rollup.notices) == 0 - - get_payload = to_jsonhex( - {'op': 'get', 'key': 'key_1'} - ) - dapp_client.send_advance(hex_payload=get_payload) - assert dapp_client.rollup.status - expected_payload = to_jsonhex( - {'key': 'key_1', 'value': 'value_1'} - ) - assert dapp_client.rollup.notices[-1]['data']['payload'] == expected_payload diff --git a/tests/test_url_router_app.py b/tests/test_url_router_app.py new file mode 100644 index 0000000..11f4aa7 --- /dev/null +++ b/tests/test_url_router_app.py @@ -0,0 +1,65 @@ +import pytest + +from cartesi.testclient import TestClient + +import examples.url_router + + +def str2hex(str): + """Encodes a string as a hex string""" + return "0x" + str.encode("utf-8").hex() + + +@pytest.fixture +def app_client() -> TestClient: + client = TestClient(examples.url_router.app) + return client + + +def test_hello_world_advance(app_client: TestClient): + + payload = str2hex('hello/') + app_client.send_advance(hex_payload=payload) + + assert app_client.rollup.status + assert len(app_client.rollup.notices) > 0 + + response = str2hex('Hello World') + assert app_client.rollup.notices[-1]['data']['payload'] == response + + +def test_hello_world_inspect(app_client: TestClient): + + payload = str2hex('hello/') + app_client.send_inspect(hex_payload=payload) + + assert app_client.rollup.status + assert len(app_client.rollup.reports) > 0 + + response = str2hex('Hello World') + assert app_client.rollup.reports[-1]['data']['payload'] == response + + +def test_hello_world_inspect_parms_1(app_client: TestClient): + + payload = str2hex('hello/Earth') + app_client.send_inspect(hex_payload=payload) + + assert app_client.rollup.status + assert len(app_client.rollup.reports) > 0 + + response = str2hex('Hello Earth') + assert app_client.rollup.reports[-1]['data']['payload'] == response + + +def test_hello_world_inspect_parms_2(app_client: TestClient): + + payload = str2hex('hello/Earth?suffix=%21') + app_client.send_inspect(hex_payload=payload) + + assert app_client.rollup.status + assert len(app_client.rollup.reports) > 0 + + response = str2hex('Hello Earth!') + print(app_client.rollup.reports[-1]['data']['payload']) + assert app_client.rollup.reports[-1]['data']['payload'] == response diff --git a/tests/test_url_router_dapp.py b/tests/test_url_router_dapp.py deleted file mode 100644 index 826da53..0000000 --- a/tests/test_url_router_dapp.py +++ /dev/null @@ -1,65 +0,0 @@ -import pytest - -from cartesi.testclient import TestClient - -import examples.url_router - - -def str2hex(str): - """Encodes a string as a hex string""" - return "0x" + str.encode("utf-8").hex() - - -@pytest.fixture -def dapp_client() -> TestClient: - client = TestClient(examples.url_router.dapp) - return client - - -def test_hello_world_advance(dapp_client: TestClient): - - payload = str2hex('hello/') - dapp_client.send_advance(hex_payload=payload) - - assert dapp_client.rollup.status - assert len(dapp_client.rollup.notices) > 0 - - response = str2hex('Hello World') - assert dapp_client.rollup.notices[-1]['data']['payload'] == response - - -def test_hello_world_inspect(dapp_client: TestClient): - - payload = str2hex('hello/') - dapp_client.send_inspect(hex_payload=payload) - - assert dapp_client.rollup.status - assert len(dapp_client.rollup.reports) > 0 - - response = str2hex('Hello World') - assert dapp_client.rollup.reports[-1]['data']['payload'] == response - - -def test_hello_world_inspect_parms_1(dapp_client: TestClient): - - payload = str2hex('hello/Earth') - dapp_client.send_inspect(hex_payload=payload) - - assert dapp_client.rollup.status - assert len(dapp_client.rollup.reports) > 0 - - response = str2hex('Hello Earth') - assert dapp_client.rollup.reports[-1]['data']['payload'] == response - - -def test_hello_world_inspect_parms_2(dapp_client: TestClient): - - payload = str2hex('hello/Earth?suffix=%21') - dapp_client.send_inspect(hex_payload=payload) - - assert dapp_client.rollup.status - assert len(dapp_client.rollup.reports) > 0 - - response = str2hex('Hello Earth!') - print(dapp_client.rollup.reports[-1]['data']['payload']) - assert dapp_client.rollup.reports[-1]['data']['payload'] == response From b783393a9d7c5f6bc0aba193b9d24f00e157f93e Mon Sep 17 00:00:00 2001 From: Lyno Ferraz Date: Mon, 14 Oct 2024 16:26:49 -0300 Subject: [PATCH 02/21] feat: use eth_abi_lite instead of eth_abi --- cartesi/_eth_abi_packed.py | 8 ++++---- cartesi/abi.py | 24 ++++++++++++------------ cartesi/app.py | 10 +++++----- cartesi/rollup.py | 18 +++++++++--------- cartesi/router/url.py | 22 +++++++++++----------- examples/echo.py | 6 +++--- examples/ether_wallet.py | 6 +++--- examples/json_handler.py | 10 +++++----- examples/url_router.py | 6 +++--- pyproject.toml | 2 +- requirements.txt | 34 ++++++++-------------------------- 11 files changed, 64 insertions(+), 82 deletions(-) diff --git a/cartesi/_eth_abi_packed.py b/cartesi/_eth_abi_packed.py index c484912..d983687 100644 --- a/cartesi/_eth_abi_packed.py +++ b/cartesi/_eth_abi_packed.py @@ -1,11 +1,11 @@ -from eth_abi.codec import ( +from eth_abi_lite.codec import ( ABICodec, ) -from eth_abi.registry import ( +from eth_abi_lite.registry import ( registry_packed, BaseEquals ) -from eth_abi.decoding import ( +from eth_abi_lite.decoding import ( BooleanDecoder, AddressDecoder, UnsignedIntegerDecoder, @@ -64,4 +64,4 @@ def read_data_from_stream(self, stream): default_codec_packed = ABICodec(registry_packed) -decode_packed = default_codec_packed.decode +decode_packed = default_codec_packed.decode_abi diff --git a/cartesi/abi.py b/cartesi/abi.py index a6a377d..8a763aa 100644 --- a/cartesi/abi.py +++ b/cartesi/abi.py @@ -4,9 +4,9 @@ from typing import Annotated, get_type_hints, TypeVar from dataclasses import dataclass -import eth_abi -import eth_abi.packed -import pydantic +from eth_abi_lite import decode_abi, encode_abi +import eth_abi_lite.packed +from pydantic import BaseModel from . import _eth_abi_packed @@ -66,7 +66,7 @@ def _get_abi_for_type(field_type): """ -def get_abi_types_from_model(model: pydantic.BaseModel) -> list[str]: +def get_abi_types_from_model(model: BaseModel) -> list[str]: """Return a list of types representing the Pydantic Model Parameters @@ -107,7 +107,7 @@ def get_abi_types_from_model(model: pydantic.BaseModel) -> list[str]: return types -def encode_model(obj: pydantic.BaseModel, packed: bool = False) -> bytes: +def encode_model(obj: BaseModel, packed: bool = False) -> bytes: """Serialize the model using ABI encoding. Parameters @@ -124,18 +124,18 @@ def encode_model(obj: pydantic.BaseModel, packed: bool = False) -> bytes: Serialized version of the model """ if packed: - encode = eth_abi.packed.encode_packed + encode_fn = eth_abi_lite.packed.encode_packed else: - encode = eth_abi.encode + encode_fn = encode_abi fields = obj.__fields__.keys() data = [getattr(obj, x) for x in fields] types = get_abi_types_from_model(obj) - return encode(types, data) + return encode_fn(types, data) -M = TypeVar('M', bound=pydantic.BaseModel) +M = TypeVar('M', bound=BaseModel) def decode_to_model(data: bytes, model: M, packed: bool = False) -> M: @@ -156,13 +156,13 @@ def decode_to_model(data: bytes, model: M, packed: bool = False) -> M: Object containing decoded data """ if packed: - decode = _eth_abi_packed.decode_packed + decode_fn = eth_abi_lite.packed.decode_packed else: - decode = eth_abi.decode + decode_fn = decode_abi fields = model.__fields__.keys() types = get_abi_types_from_model(model) - decoded = decode(types, data) + decoded = decode_fn(types, data) kwargs = dict(zip(fields, decoded)) return model.parse_obj(kwargs) diff --git a/cartesi/app.py b/cartesi/app.py index 29a0669..47a2123 100644 --- a/cartesi/app.py +++ b/cartesi/app.py @@ -1,12 +1,12 @@ -import os -import logging +from os import environ +from logging import getLogger, debug from .models import RollupResponse from .rollup import Rollup, HTTPRollupServer from .router import Router -LOGGER = logging.getLogger(__name__) -ROLLUP_SERVER = os.environ.get('ROLLUP_HTTP_SERVER_URL') +LOGGER = getLogger(__name__) +ROLLUP_SERVER = environ.get('ROLLUP_HTTP_SERVER_URL') class App: @@ -59,7 +59,7 @@ def _handle(self, request: RollupResponse) -> bool: if handler is None: handler = self._get_default_handler(request) - logging.debug("Handler: %s", repr(handler)) + debug("Handler: %s", repr(handler)) try: status = handler(self.rollup, request.data) except Exception: diff --git a/cartesi/rollup.py b/cartesi/rollup.py index 55e0b06..d9bfa4e 100644 --- a/cartesi/rollup.py +++ b/cartesi/rollup.py @@ -1,13 +1,13 @@ from abc import ABC, abstractmethod from collections.abc import Callable -import os -import logging +from os import environ +from logging import getLogger -import requests +from requests import post from .models import RollupResponse -LOGGER = logging.getLogger(__name__) +LOGGER = getLogger(__name__) DEFAULT_ROLLUP_URL = 'http://127.0.0.1:5004' @@ -44,7 +44,7 @@ class HTTPRollupServer(Rollup): def __init__(self, address: str = None): super().__init__() if address is None: - address = os.environ.get( + address = environ.get( 'ROLLUP_HTTP_SERVER_URL', DEFAULT_ROLLUP_URL ) @@ -56,7 +56,7 @@ def main_loop(self): while True: LOGGER.info("Sending finish") - response = requests.post(self.address + "/finish", json=finish) + response = post(self.address + "/finish", json=finish) LOGGER.info(f"Received finish status {response.status_code}") if response.status_code == 202: @@ -80,7 +80,7 @@ def notice(self, payload: str): data = { 'payload': payload } - response = requests.post(self.address + "/notice", json=data) + response = post(self.address + "/notice", json=data) LOGGER.info(f"Received notice status {response.status_code} " f"body {response.content}") return response.content @@ -90,14 +90,14 @@ def report(self, payload: str): data = { 'payload': payload } - response = requests.post(self.address + "/report", json=data) + response = post(self.address + "/report", json=data) LOGGER.info(f"Received report status {response.status_code} " f"body {response.content}") return response.content def voucher(self, payload: dict): LOGGER.info("Adding voucher") - response = requests.post(self.address + '/voucher', json=payload) + response = post(self.address + '/voucher', json=payload) LOGGER.info(f"Received report status {response.status_code} " f"body {response.content}") return response.content diff --git a/cartesi/router/url.py b/cartesi/router/url.py index 21b0606..99adf32 100644 --- a/cartesi/router/url.py +++ b/cartesi/router/url.py @@ -1,9 +1,9 @@ from collections.abc import Callable -import inspect +from inspect import getfullargspec from itertools import chain -import logging -import re -import typing +from logging import getLogger +from re import Pattern, compile, escape +from typing import Pattern as typing_Pattern from urllib.parse import parse_qs from pydantic import BaseModel @@ -12,7 +12,7 @@ from ..models import RollupResponse, RollupData from ..rollup import Rollup -LOGGER = logging.getLogger(__name__) +LOGGER = getLogger(__name__) class URLParameters(BaseModel): @@ -22,7 +22,7 @@ class URLParameters(BaseModel): class URLOperation(BaseModel): path: str - path_regex: re.Pattern + path_regex: Pattern handler: Callable operationId: str requestType: str @@ -141,7 +141,7 @@ def _create_handler(route_handler, url_params: URLParameters): Return a handler with the default router arguments, but applies additional args to the user's function according to introspection """ - args = inspect.getfullargspec(route_handler) + args = getfullargspec(route_handler) def _handler(rollup: Rollup, data: RollupData): kwargs = {} @@ -181,10 +181,10 @@ def _match_url(pattern, request_path: str): return (True, params) -PARAM_REGEX = re.compile("{([a-zA-Z_][a-zA-Z0-9_]*)}") +PARAM_REGEX = compile("{([a-zA-Z_][a-zA-Z0-9_]*)}") -def compile_path(path: str) -> typing.Pattern: +def compile_path(path: str) -> typing_Pattern: """ Given a path string like "/{operation}", returns a corresponding regex. @@ -195,10 +195,10 @@ def compile_path(path: str) -> typing.Pattern: for match in PARAM_REGEX.finditer(path): param_name = match.groups()[0] - path_regex += re.escape(path[idx: match.start()]) + path_regex += escape(path[idx: match.start()]) path_regex += f"(?P<{param_name}>[^/]+)" idx = match.end() path_regex += path[idx:] + "$" - return re.compile(path_regex) + return compile(path_regex) diff --git a/examples/echo.py b/examples/echo.py index 85cd8aa..1ac3127 100644 --- a/examples/echo.py +++ b/examples/echo.py @@ -1,9 +1,9 @@ -import logging +from logging import getLogger, basicConfig, DEBUG from cartesi import App, Rollup, RollupData -LOGGER = logging.getLogger(__name__) -logging.basicConfig(level=logging.DEBUG) +LOGGER = getLogger(__name__) +basicConfig(level=DEBUG) app = App() diff --git a/examples/ether_wallet.py b/examples/ether_wallet.py index f6b2a9e..ac8f84a 100644 --- a/examples/ether_wallet.py +++ b/examples/ether_wallet.py @@ -1,11 +1,11 @@ -import logging +from logging import getLogger, basicConfig, DEBUG from cartesi import App from cartesi.wallet.ether import EtherWallet -LOGGER = logging.getLogger(__name__) -logging.basicConfig(level=logging.DEBUG) +LOGGER = getLogger(__name__) +basicConfig(level=DEBUG) app = App() diff --git a/examples/json_handler.py b/examples/json_handler.py index 2899558..02f4414 100644 --- a/examples/json_handler.py +++ b/examples/json_handler.py @@ -1,10 +1,10 @@ -import json -import logging +from json import dumps +from logging import getLogger, basicConfig, DEBUG from cartesi import App, Rollup, RollupData, JSONRouter -LOGGER = logging.getLogger(__name__) -logging.basicConfig(level=logging.DEBUG) +LOGGER = getLogger(__name__) +basicConfig(level=DEBUG) app = App() json_router = JSONRouter() @@ -21,7 +21,7 @@ def str2hex(str): def to_jsonhex(data): """Encode as a JSON hex""" - return str2hex(json.dumps(data)) + return str2hex(dumps(data)) @json_router.advance({"op": "set"}) diff --git a/examples/url_router.py b/examples/url_router.py index e6a920d..195e7be 100644 --- a/examples/url_router.py +++ b/examples/url_router.py @@ -1,9 +1,9 @@ -import logging +from logging import getLogger, basicConfig, DEBUG from cartesi import App, Rollup, RollupData, URLRouter, URLParameters -LOGGER = logging.getLogger(__name__) -logging.basicConfig(level=logging.DEBUG) +LOGGER = getLogger(__name__) +basicConfig(level=DEBUG) app = App() url_router = URLRouter() app.add_router(url_router) diff --git a/pyproject.toml b/pyproject.toml index 891a888..08b37cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ dependencies = [ "requests >= 2.31.0", "pydantic < 2", "pytest ~= 7.4.0", - "eth-abi ~= 4.2.1", + "eth_abi_lite ~= 3.2.0", "pycryptodome ~= 3.19.0", ] diff --git a/requirements.txt b/requirements.txt index 6158406..88399ce 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.11 +# This file is autogenerated by pip-compile with Python 3.12 # by the following command: # # pip-compile @@ -8,43 +8,25 @@ certifi==2023.7.22 # via requests charset-normalizer==3.2.0 # via requests -cytoolz==0.12.2 - # via eth-utils -eth-abi==4.2.1 - # via cartesi (pyproject.toml) -eth-hash==0.5.2 - # via eth-utils -eth-typing==3.5.2 - # via - # eth-abi - # eth-utils -eth-utils==2.3.1 - # via eth-abi +eth-abi-lite==3.2.0 + # via python-cartesi (pyproject.toml) idna==3.4 # via requests iniconfig==2.0.0 # via pytest packaging==23.1 # via pytest -parsimonious==0.9.0 - # via eth-abi pluggy==1.2.0 # via pytest pycryptodome==3.19.0 - # via cartesi (pyproject.toml) + # via python-cartesi (pyproject.toml) pydantic==1.10.12 - # via cartesi (pyproject.toml) + # via python-cartesi (pyproject.toml) pytest==7.4.0 - # via cartesi (pyproject.toml) -regex==2023.10.3 - # via parsimonious + # via python-cartesi (pyproject.toml) requests==2.31.0 - # via cartesi (pyproject.toml) -toolz==0.12.0 - # via cytoolz + # via python-cartesi (pyproject.toml) typing-extensions==4.7.1 - # via - # eth-typing - # pydantic + # via pydantic urllib3==2.0.4 # via requests From b49372b1336f6562ac323f901af74c4bb3c572b2 Mon Sep 17 00:00:00 2001 From: Lyno Ferraz Date: Fri, 18 Oct 2024 15:27:54 -0300 Subject: [PATCH 03/21] feat: Use 2.0.0-rc.10 portal address --- examples/ether_wallet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ether_wallet.py b/examples/ether_wallet.py index ac8f84a..9ea5cb3 100644 --- a/examples/ether_wallet.py +++ b/examples/ether_wallet.py @@ -9,7 +9,7 @@ app = App() -ETHER_PORTAL_ADDRESS = '0x1733b13aAbcEcf3464157Bd7954Bd7e4Cf91Ce22' +ETHER_PORTAL_ADDRESS = '0xfa2292f6D85ea4e629B156A4f99219e30D12EE17' ether_wallet = EtherWallet(portal_address=ETHER_PORTAL_ADDRESS) app.add_router(ether_wallet) From 22829cdbfa2361d27426206ac790d170c3796054 Mon Sep 17 00:00:00 2001 From: Lyno Ferraz Date: Wed, 12 Feb 2025 12:33:53 -0300 Subject: [PATCH 04/21] feat: Add delegated vouchers and fixes --- cartesi/_eth_abi_packed.py | 2 +- cartesi/abi.py | 6 +++--- cartesi/rollup.py | 19 +++++++++++++++---- cartesi/testclient.py | 26 ++++++++++++++------------ cartesi/vouchers.py | 2 +- pyproject.toml | 2 +- tests/test_ether_wallet.py | 1 - 7 files changed, 35 insertions(+), 23 deletions(-) diff --git a/cartesi/_eth_abi_packed.py b/cartesi/_eth_abi_packed.py index d983687..c2dc07b 100644 --- a/cartesi/_eth_abi_packed.py +++ b/cartesi/_eth_abi_packed.py @@ -64,4 +64,4 @@ def read_data_from_stream(self, stream): default_codec_packed = ABICodec(registry_packed) -decode_packed = default_codec_packed.decode_abi +decode_abi_packed = default_codec_packed.decode_abi diff --git a/cartesi/abi.py b/cartesi/abi.py index 8a763aa..7c513cb 100644 --- a/cartesi/abi.py +++ b/cartesi/abi.py @@ -8,7 +8,7 @@ import eth_abi_lite.packed from pydantic import BaseModel -from . import _eth_abi_packed +from cartesi._eth_abi_packed import decode_abi_packed # Type Aliases for ABI encoding @@ -124,7 +124,7 @@ def encode_model(obj: BaseModel, packed: bool = False) -> bytes: Serialized version of the model """ if packed: - encode_fn = eth_abi_lite.packed.encode_packed + encode_fn = eth_abi_lite.packed.encode_abi_packed else: encode_fn = encode_abi @@ -156,7 +156,7 @@ def decode_to_model(data: bytes, model: M, packed: bool = False) -> M: Object containing decoded data """ if packed: - decode_fn = eth_abi_lite.packed.decode_packed + decode_fn = decode_abi_packed else: decode_fn = decode_abi diff --git a/cartesi/rollup.py b/cartesi/rollup.py index d9bfa4e..f189a36 100644 --- a/cartesi/rollup.py +++ b/cartesi/rollup.py @@ -27,15 +27,19 @@ def main_loop(self): pass @abstractmethod - def notice(self, payload: str) -> str: + def notice(self, payload: str) -> bytes | None: pass @abstractmethod - def report(self, payload: str) -> str: + def report(self, payload: str) -> bytes | None: pass @abstractmethod - def voucher(self, payload: dict) -> str: + def voucher(self, payload: dict) -> bytes | None: + pass + + @abstractmethod + def delegate_call_voucher(self, payload: dict) -> bytes | None: pass class HTTPRollupServer(Rollup): @@ -98,6 +102,13 @@ def report(self, payload: str): def voucher(self, payload: dict): LOGGER.info("Adding voucher") response = post(self.address + '/voucher', json=payload) - LOGGER.info(f"Received report status {response.status_code} " + LOGGER.info(f"Received voucher status {response.status_code} " + f"body {response.content}") + return response.content + + def delegate_call_voucher(self, payload: dict): + LOGGER.info("Adding voucher") + response = post(self.address + '/delegate-call-voucher', json=payload) + LOGGER.info(f"Received delegate call voucher status {response.status_code} " f"body {response.content}") return response.content diff --git a/cartesi/testclient.py b/cartesi/testclient.py index 0a3c505..a476c31 100644 --- a/cartesi/testclient.py +++ b/cartesi/testclient.py @@ -14,7 +14,6 @@ def __init__(self): self.notices = [] self.reports = [] self.vouchers = [] - self.epoch = 0 self.input = 0 self.block = 0 self.status = None @@ -27,7 +26,6 @@ def main_loop(self): def notice(self, payload: str): data = { - 'epoch_index': self.epoch, 'input_index': self.input, 'data': { 'payload': payload, @@ -37,7 +35,6 @@ def notice(self, payload: str): def report(self, payload: str): data = { - 'epoch_index': self.epoch, 'input_index': self.input, 'data': { 'payload': payload, @@ -45,12 +42,23 @@ def report(self, payload: str): } self.reports.append(data) - def voucher(self, payload: str): + def voucher(self, payload: dict): data = { - 'epoch_index': self.epoch, 'input_index': self.input, 'data': { - 'payload': payload, + 'destination': payload.get('destination'), + 'value': payload.get('value'), + 'payload': payload.get('payload'), + } + } + self.vouchers.append(data) + + def delegate_call_voucher(self, payload: dict): + data = { + 'input_index': self.input, + 'data': { + 'destination': payload.get('destination'), + 'payload': payload.get('payload'), } } self.vouchers.append(data) @@ -91,9 +99,6 @@ def send_advance( self.input += 1 def send_inspect(self, hex_payload: str): - - self.block += 1 - data = { 'request_type': 'inspect_state', 'data': { @@ -108,9 +113,6 @@ def send_inspect(self, hex_payload: str): LOGGER.error("No handler found for message.") status = False self.status = status - if status: - self.input += 1 - class TestClient: __test__ = False diff --git a/cartesi/vouchers.py b/cartesi/vouchers.py index c7da810..89215a2 100644 --- a/cartesi/vouchers.py +++ b/cartesi/vouchers.py @@ -50,7 +50,7 @@ def create_voucher_from_model( voucher = { 'destination': destination, - 'value': f"{value:#066}x", + 'value': f"{value:#066x}", 'payload': '0x' + selector + args } return voucher diff --git a/pyproject.toml b/pyproject.toml index 08b37cf..98a7ed1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "python-cartesi" version = "0.2.0" authors = [ - { name="Felipe Fink Grael", email="felipefg@prototyp3.dev" }, + { name="Felipe Fink Grael", email="felipefg@prototyp3.dev" },{ name="Lyno Ferraz", email="lyno@prototyp3.dev" } ] description = "A Python Framework for Cartesi Distributed Applications" readme = "README.md" diff --git a/tests/test_ether_wallet.py b/tests/test_ether_wallet.py index 0e57937..7870cab 100644 --- a/tests/test_ether_wallet.py +++ b/tests/test_ether_wallet.py @@ -17,7 +17,6 @@ def app_client() -> TestClient: @pytest.fixture def deposit_payload() -> str: deposit = DepositEtherPayload( - success=True, sender="0x721be000f6054b5e0e57aaab791015b53f0a18f4", depositAmount=int(1e18), execLayerData=b'', From e905dfd0625cb87c3ff1a501e145abd7c332d3b2 Mon Sep 17 00:00:00 2001 From: Lyno Ferraz Date: Wed, 12 Feb 2025 12:49:00 -0300 Subject: [PATCH 05/21] fix: delegated call logs --- cartesi/rollup.py | 2 +- cartesi/testclient.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cartesi/rollup.py b/cartesi/rollup.py index f189a36..421bd5e 100644 --- a/cartesi/rollup.py +++ b/cartesi/rollup.py @@ -107,7 +107,7 @@ def voucher(self, payload: dict): return response.content def delegate_call_voucher(self, payload: dict): - LOGGER.info("Adding voucher") + LOGGER.info("Adding delegate call voucher") response = post(self.address + '/delegate-call-voucher', json=payload) LOGGER.info(f"Received delegate call voucher status {response.status_code} " f"body {response.content}") diff --git a/cartesi/testclient.py b/cartesi/testclient.py index a476c31..009aed3 100644 --- a/cartesi/testclient.py +++ b/cartesi/testclient.py @@ -14,6 +14,8 @@ def __init__(self): self.notices = [] self.reports = [] self.vouchers = [] + self.delegate_call_vouchers = [] + self.gios = [] self.input = 0 self.block = 0 self.status = None @@ -61,7 +63,7 @@ def delegate_call_voucher(self, payload: dict): 'payload': payload.get('payload'), } } - self.vouchers.append(data) + self.delegate_call_vouchers.append(data) def send_advance( self, From fd9b5d055da481a8fb4f0830771d9aa8c7baf525 Mon Sep 17 00:00:00 2001 From: Lyno Ferraz Date: Wed, 12 Feb 2025 12:56:35 -0300 Subject: [PATCH 06/21] feat: Add gio request --- cartesi/rollup.py | 11 +++++++++++ cartesi/testclient.py | 10 ++++++++++ 2 files changed, 21 insertions(+) diff --git a/cartesi/rollup.py b/cartesi/rollup.py index 421bd5e..df773dc 100644 --- a/cartesi/rollup.py +++ b/cartesi/rollup.py @@ -42,6 +42,10 @@ def voucher(self, payload: dict) -> bytes | None: def delegate_call_voucher(self, payload: dict) -> bytes | None: pass + @abstractmethod + def gio(self, payload: dict) -> bytes | None: + pass + class HTTPRollupServer(Rollup): """HTTP Communication with Rollup Server based on Requests""" @@ -112,3 +116,10 @@ def delegate_call_voucher(self, payload: dict): LOGGER.info(f"Received delegate call voucher status {response.status_code} " f"body {response.content}") return response.content + + def gio(self, payload: dict): + LOGGER.info("Adding gio") + response = post(self.address + '/gio', json=payload) + LOGGER.info(f"Received gio status {response.status_code} " + f"body {response.content}") + return response.content diff --git a/cartesi/testclient.py b/cartesi/testclient.py index 009aed3..01a6b49 100644 --- a/cartesi/testclient.py +++ b/cartesi/testclient.py @@ -65,6 +65,16 @@ def delegate_call_voucher(self, payload: dict): } self.delegate_call_vouchers.append(data) + def gio(self, payload: dict): + data = { + 'input_index': self.input, + 'data': { + 'domain': payload.get('domain'), + 'id': payload.get('id'), + } + } + self.gios.append(data) + def send_advance( self, hex_payload: str, From d18e27993e7fc3d5c89931e8bc4f736e4f5c987c Mon Sep 17 00:00:00 2001 From: Lyno Ferraz Date: Thu, 13 Feb 2025 15:44:18 -0300 Subject: [PATCH 07/21] feat: decode raw evm advance --- cartesi/app.py | 5 +++-- cartesi/models.py | 16 ++++++++++++++++ cartesi/rollup.py | 23 +++++++++++++++++++++-- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/cartesi/app.py b/cartesi/app.py index 47a2123..5e2f254 100644 --- a/cartesi/app.py +++ b/cartesi/app.py @@ -11,11 +11,12 @@ class App: - def __init__(self): + def __init__(self, raw_input = False): self.routers: list[Router] = [] self.default_advance_handler = lambda rollup, data: False self.default_inspect_handler = lambda rollup, data: False self.rollup: Rollup | None = None + self.raw_input = raw_input def advance(self): """Decorator for inserting handle advance""" @@ -73,6 +74,6 @@ def add_router(self, router: Router): def run(self): if self.rollup is None: - self.rollup = HTTPRollupServer() + self.rollup = HTTPRollupServer(raw_input=self.raw_input) self.rollup.set_handler(self._handle) self.rollup.main_loop() diff --git a/cartesi/models.py b/cartesi/models.py index 6844679..f3874dc 100644 --- a/cartesi/models.py +++ b/cartesi/models.py @@ -3,6 +3,7 @@ from Crypto.Hash import keccak from pydantic import BaseModel +from .abi import UInt256, Bytes, Address, get_abi_types_from_model def _hex2str(hex): @@ -76,3 +77,18 @@ def to_bytes(self) -> bytes: selector = sig_hash.digest()[:4] return selector + +class EvmAdvance(BaseModel): + chain_id: UInt256 + app_contract: Address + msg_sender: Address + block_number: UInt256 + block_timestamp: UInt256 + prev_randao: UInt256 + input_index: UInt256 + payload: Bytes + +evm_advance_header = ABIFunctionSelectorHeader( + function=EvmAdvance.__name__, + argument_types=get_abi_types_from_model(EvmAdvance) +) diff --git a/cartesi/rollup.py b/cartesi/rollup.py index df773dc..b608877 100644 --- a/cartesi/rollup.py +++ b/cartesi/rollup.py @@ -5,7 +5,8 @@ from requests import post -from .models import RollupResponse +from .models import RollupResponse, EvmAdvance, evm_advance_header +from .abi import decode_to_model LOGGER = getLogger(__name__) @@ -49,7 +50,7 @@ def gio(self, payload: dict) -> bytes | None: class HTTPRollupServer(Rollup): """HTTP Communication with Rollup Server based on Requests""" - def __init__(self, address: str = None): + def __init__(self, address: str = None, raw_input: bool = False): super().__init__() if address is None: address = environ.get( @@ -57,6 +58,7 @@ def __init__(self, address: str = None): DEFAULT_ROLLUP_URL ) self.address = address + self.raw_input = raw_input def main_loop(self): @@ -72,7 +74,24 @@ def main_loop(self): continue rollup_response = response.json() + if self.raw_input: + if rollup_response.get('data') is not None and \ + rollup_response['data'].get('payload') is not None and \ + rollup_response['data']['payload'][2:10] == evm_advance_header.to_bytes().hex(): + advance_data = decode_to_model(data=bytes.fromhex(rollup_response['data']['payload'][10:]),model=EvmAdvance) + if rollup_response['data'].get('metadata') is not None: + rollup_response['data']['metadata'] = { + 'chain_id': advance_data.chain_id, + 'app_contract': advance_data.app_contract, + 'msg_sender': advance_data.msg_sender, + 'block_number': advance_data.block_number, + 'block_timestamp': advance_data.block_timestamp, + 'prev_randao': advance_data.prev_randao, + 'input_index': advance_data.input_index + } + rollup_response['data']['payload'] = f"0x{advance_data.payload.hex()}" # TODO: Error handling for this model creation + LOGGER.debug(f"Finish response body {rollup_response}") rollup_response = RollupResponse.parse_obj(rollup_response) handler = self.handler From df60a05aa68f4d8759d95a0a7b73647036921574 Mon Sep 17 00:00:00 2001 From: Lyno Ferraz Date: Wed, 7 May 2025 13:54:15 -0300 Subject: [PATCH 08/21] fix: fix testing and type declarations --- cartesi/models.py | 4 ++-- cartesi/testclient.py | 2 +- examples/json_handler.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cartesi/models.py b/cartesi/models.py index f3874dc..66bfcc2 100644 --- a/cartesi/models.py +++ b/cartesi/models.py @@ -40,7 +40,7 @@ def bytes_payload(self) -> bytes: def str_payload(self, encoding='utf-8') -> str: return bytes.fromhex(self.payload[2:]).decode(encoding) - def json_payload(self) -> bytes: + def json_payload(self) -> dict: return json.loads(self.str_payload()) @@ -52,7 +52,7 @@ class RollupResponse(BaseModel): class ABIHeader(BaseModel, abc.ABC): @abc.abstractmethod - def to_bytes(self): + def to_bytes(self) -> bytes: """Get the bytes representation for this header""" pass diff --git a/cartesi/testclient.py b/cartesi/testclient.py index 01a6b49..1d09966 100644 --- a/cartesi/testclient.py +++ b/cartesi/testclient.py @@ -49,7 +49,7 @@ def voucher(self, payload: dict): 'input_index': self.input, 'data': { 'destination': payload.get('destination'), - 'value': payload.get('value'), + 'value': int(payload.get('value') or "0x0",0), 'payload': payload.get('payload'), } } diff --git a/examples/json_handler.py b/examples/json_handler.py index 02f4414..b1d2074 100644 --- a/examples/json_handler.py +++ b/examples/json_handler.py @@ -25,8 +25,8 @@ def to_jsonhex(data): @json_router.advance({"op": "set"}) -def handle_advance_set(rollup: Rollup, data: RollupData): - data = data.json_payload() +def handle_advance_set(rollup: Rollup, raw_data: RollupData): + data = raw_data.json_payload() key = data['key'] value = data['value'] From 13ad6c0eaa7d291e5f781a70294eaff0bcff2b0e Mon Sep 17 00:00:00 2001 From: Joao Garcia Date: Mon, 19 May 2025 19:33:22 +0100 Subject: [PATCH 09/21] Added OO approach to create outputs --- README.md | 87 ++++++++++++++++++++++++++ cartesi/__init__.py | 5 +- cartesi/outputs.py | 144 ++++++++++++++++++++++++++++++++++++++++++++ cartesi/util.py | 68 +++++++++++++++++++++ examples/outputs.py | 48 +++++++++++++++ 5 files changed, 351 insertions(+), 1 deletion(-) create mode 100644 cartesi/outputs.py create mode 100644 cartesi/util.py create mode 100644 examples/outputs.py diff --git a/README.md b/README.md index a604a99..d65a4f5 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,93 @@ Adds a new [voucher](https://docs.cartesi.io/cartesi-rollups/main-concepts/#vouc - **destination**: The address of the destination contract as a string, starting with the `'0x'` prefix. - **payload**: The payload for the transaction in Ethereum hex binary format. The contents of this field will be the transaction's data field. +## Using the `Output` classes directly + +In addition to calling `rollup.notice()`, `rollup.report()`, and `rollup.voucher()` inside your handler, the Cartesi High-Level Framework also supports an object-oriented approach using specialized classes: `Notice`, `Report`, and `Voucher`. These classes implement a shared `Output` interface and offer more flexible and modular ways to emit outputs. + +Each of these classes can be used in multiple ways and support different helpers for common use cases: + +### Notice + +You can instantiate a notice directly using a hex-encoded string or use helper methods to generate one from a string or a JSON object. + +```python +Notice("0x68656c6c6f20776f726c64").create() +Notice.from_hex("0x68656c6c6f20776f726c64").create() +Notice.from_string("hello world").create() +Notice.from_json({"foo": "bar"}).create() +``` + +### Report + +Similar to notices, reports can also be created directly or from strings and JSON: + +```python +Report("0x68656c6c6f20776f726c64").create() +Report.from_hex("0x68656c6c6f20776f726c64").create() +Report.from_string("This is a log message").create() +Report.from_json({"event": "user_login", "user": "Alice"}).create() +``` + +### Voucher + +Vouchers represent actual transactions on the base layer. You can generate them from: + +* Hex strings of function selector + encoded payload +* Function selectors with ABI types and values + +```python +# From raw hex +Voucher.from_hex("0xRecipientAddress", "0xpayload").create() + +# From function selector +Voucher.from_function_selector( + destination="0xRecipientAddress", + selector="mint(address,string,uint256,uint256)", + types=["address", "string", "uint256", "uint256"], + values=[data["metadata"]["msg_sender"], "text_value", 100, 2000] +).create() +``` + +### Example usage in an advance handler + +```python +@dapp.advance() +def handle_advance(rollup: Rollup, data: RollupData) -> bool: + payload = data.str_payload() + + # Still using the Rollup object + rollup.notice("0x" + payload.encode('utf-8').hex()) + + # Also using the object-oriented style + Report.from_string("Received: " + payload).create() + Notice.from_json({"echoed": payload}).create() + + Voucher.from_function_selector( + destination="0x7b693356646348D27A745bFb3E3f9CC82B893a6c", + selector="registerPublicKey", + types=["bytes"], + values=[bytes.fromhex("ba57...")] + ).create() + + return True +``` + +This approach improves modularity, decouples your logic from the Rollup object, and can help with mocking or extending output behavior for testing or advanced scenarios. + +### Overriding the Rollup Server address + +By default, the `create()` method of `Notice`, `Report`, and `Voucher` will send the output to the Rollup Server address defined in the environment variable `ROLLUP_HTTP_SERVER_URL`. If that environment variable is not set, it falls back to `http://127.0.0.1:5004`. + +However, if you want to explicitly specify a different Rollup Server address—such as in testing environments or custom setups—you can override it by passing the `rollup_server_address` argument to the `create()` method: + +```python +custom_address = "http://localhost:7000" + +# Notice with overridden address +Notice.from_string("hello world").create(rollup_server_address=custom_address) +``` + ## Routers Routers simplify the coding experience by identifying the request type using common patterns in the input data, and calling your handler only when several conditions are met. diff --git a/cartesi/__init__.py b/cartesi/__init__.py index 0a4cde4..851ef74 100644 --- a/cartesi/__init__.py +++ b/cartesi/__init__.py @@ -6,9 +6,12 @@ ABILiteralHeader, RollupData, RollupMetadata, - RollupResponse + RollupResponse, ) from .rollup import Rollup, HTTPRollupServer # noqa + +from .outputs import Notice, Report, Voucher # noqa + from .router import ( # noqa Router, JSONRouter, diff --git a/cartesi/outputs.py b/cartesi/outputs.py new file mode 100644 index 0000000..c3df2ed --- /dev/null +++ b/cartesi/outputs.py @@ -0,0 +1,144 @@ +from abc import ABC, abstractmethod +from os import environ +from logging import getLogger +from requests import post + +import json +from typing import List +from .util import encode_values, encode_function_call + +LOGGER = getLogger(__name__) + +DEFAULT_ROLLUP_URL = 'http://127.0.0.1:5004' + +def encode(d): + return "0x" + d.encode("utf-8").hex() + +class Output(ABC): + """ + Base class representing a result generated by processing an input. + + It is resposible for converting its payload to the format expected by + Cartesi rollups. + + Parameters: + payload(str): the actual data generated after processing the input + """ + + def __init__(self, payload: str, address: str = None): + self.endpoint = "/output" + if payload[:2] == "0x": + self.payload = payload + else: + self.payload = "0x" + payload + + if address is None: + address = environ.get( + 'ROLLUP_HTTP_SERVER_URL', + DEFAULT_ROLLUP_URL + ) + self.address = address + + @classmethod + def from_hex(cls, hex_value:str): + return cls(hex_value) + + def create(self, rollup_server_address=None): + rollup_server = self.address + if rollup_server_address is not None: + rollup_server = rollup_server_address + + return post(rollup_server + self.endpoint, json={"payload": self.payload}) + +class Voucher(Output): + """ + A Voucher is an `Output` representing a transaction + that can be carried out on the base layer blockchain, + such as a transfer of assets. + + Parameters: + destination(str): destination of the contract who will execute the payload + payload(bytes): an ABI encoded contract function call + """ + + def __init__(self, destination: str, payload: bytes|str, value = None, address: str = None): + self.endpoint = "/voucher" + self.destination = destination + self.value = value + + if value is None: + self.value = '0x' + encode_values(["uint256"], [0]).hex() + + if payload[:2] == "0x": + super().__init__(payload, address=address) + else: + hexpayload = "0x" + payload.hex() + super().__init__(hexpayload, address=address) + + @classmethod + def from_hex(cls, destination:str, hex_value:str, value:int=None): + return cls(destination, hex_value, value) + + @classmethod + def from_function_selector(cls, destination:str, selector:str, types:List[str], values:List[int]): + transfer_payload = encode_function_call(selector, types, values) + return cls(destination, transfer_payload) + + def create(self, rollup_server_address=None): + rollup_server = self.address + + if rollup_server_address is not None: + rollup_server = rollup_server_address + + return post(rollup_server + "/voucher", json={"payload": self.payload, "destination": self.destination, "value": self.value}) + +class Notice(Output): + """ + A Notice is an `Output` representing an informational statement + that can be validated in the base layer blockchain. + + Parameters: + payload(str): a string containing arbitrary data + """ + + def __init__(self, payload: str): + super().__init__(payload) + self.endpoint = "/notice" + + @classmethod + def from_json(cls, json_object): + if (isinstance(json_object, str)): + return cls.from_string(json_object) + + j_string = json.dumps(json_object) + return cls.from_string(j_string) + + @classmethod + def from_string(cls, string:str): + hex_encoded = encode(string) + return cls(hex_encoded) + +class Report(Output): + """ + A Report is an `Output` representing an application log. + + Parameters: + payload(str): a string containing arbitrary data + """ + + def __init__(self, payload: str): + super().__init__(payload) + self.endpoint = "/report" + + @classmethod + def from_json(cls, json_object): + if (isinstance(json_object, str)): + return cls.from_string(json_object) + + j_string = json.dumps(json_object) + return cls.from_string(j_string) + + @classmethod + def from_string(cls, string:str): + hex_encoded = encode(string) + return cls(hex_encoded) \ No newline at end of file diff --git a/cartesi/util.py b/cartesi/util.py new file mode 100644 index 0000000..09fe894 --- /dev/null +++ b/cartesi/util.py @@ -0,0 +1,68 @@ +# Copyright 2022 Cartesi Pte. Ltd. +# +# SPDX-License-Identifier: Apache-2.0 +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use +# this file except in compliance with the License. You may obtain a copy of the +# License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +from eth_utils import function_signature_to_4byte_selector +from eth_abi import decode, encode +from typing import List, Tuple, Any + + +def hex_to_str(hex): + """Decode a hex string prefixed with "0x" into a UTF-8 string""" + return bytes.fromhex(hex[2:]).decode("utf-8") + + +def str_to_hex(str): + """Encode a string as a hex string, adding the "0x" prefix""" + return "0x" + str.encode("utf-8").hex() + +def decode_payload(types: List[str], payload: str, ) -> Tuple[Any, ...]: + """ + Decodes an ABI-encoded payload. + + Args: + types (List[str]): A list of ABI types as strings. + payload (str): The hex-encoded payload to decode. + + Returns: + Tuple[Any, ...]: A tuple containing the decoded values. + """ + # Ensure the payload is in bytes format + payload_bytes = bytes.fromhex(payload[2:]) # Remove '0x' prefix and convert to bytes + return decode(types, payload_bytes) + +def decode_id_val(payload: str) -> Tuple[Any, ...]: + return decode(['uint256[]', 'uint256[]'], payload) + + +def encode_function_call(function_signature: str, types: List[str], values: List) -> str: + """ + Encodes a function call with the given signature and parameters. + + Args: + function_signature (str): The signature of the function (e.g., 'transfer(address,uint256)'). + types (List[str]): A list of ABI types as strings. + values (List): A list of values corresponding to the types. + + Returns: + str: The hex-encoded function call data. + """ + # Get the 4-byte selector from the function signature + selector = function_signature_to_4byte_selector(function_signature) + + # Encode the values based on the provided types + encoded_params = encode(types, values) + + # Concatenate the selector and the encoded parameters + return "0x" + (selector + encoded_params).hex() + +def encode_values(types: List[str], values: List): + return encode(types, values) \ No newline at end of file diff --git a/examples/outputs.py b/examples/outputs.py new file mode 100644 index 0000000..e5e39cd --- /dev/null +++ b/examples/outputs.py @@ -0,0 +1,48 @@ +import logging + +from cartesi import App, Rollup, RollupData, Notice, Voucher, Report + +LOGGER = logging.getLogger(__name__) +logging.basicConfig(level=logging.DEBUG) +dapp = App() + + +def str2hex(str): + """Encodes a string as a hex string""" + return "0x" + str.encode("utf-8").hex() + + +@dapp.advance() +def handle_advance(rollup: Rollup, data: RollupData) -> bool: + payload = data.str_payload() + LOGGER.debug("Echoing '%s'", payload) + rollup.notice(str2hex(payload)) + + Report("0x68656c6c6f20776f726c64").create() + Report.from_hex("0x68656c6c6f20776f726c64").create() + Report.from_string("hello world").create() + Report.from_json({"foo":"bar"}).create() + + Notice("0x68656c6c6f20776f726c64").create() + Notice.from_hex("0x68656c6c6f20776f726c64").create() + Notice.from_string("hello world").create() + Notice.from_json({"foo":"bar"}).create() + + return True + + +@dapp.inspect() +def handle_inspect(rollup: Rollup, data: RollupData) -> bool: + payload = data.str_payload() + LOGGER.debug("Echoing '%s'", payload) + rollup.report(str2hex(payload)) + Report("0x68656c6c6f20776f726c64").create() + Report.from_hex("0x68656c6c6f20776f726c64").create() + Report.from_string("hello world").create() + Report.from_json({"foo":"bar"}).create() + + return True + + +if __name__ == '__main__': + dapp.run() From 8ea583676f9c1c8fcf58d9864e97e234c99d8b99 Mon Sep 17 00:00:00 2001 From: Lyno Ferraz Date: Thu, 29 May 2025 17:11:18 -0300 Subject: [PATCH 10/21] feat: Use 2.0.0-rc.18 ether portal address --- examples/ether_wallet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ether_wallet.py b/examples/ether_wallet.py index 9ea5cb3..3549b81 100644 --- a/examples/ether_wallet.py +++ b/examples/ether_wallet.py @@ -9,7 +9,7 @@ app = App() -ETHER_PORTAL_ADDRESS = '0xfa2292f6D85ea4e629B156A4f99219e30D12EE17' +ETHER_PORTAL_ADDRESS = '0xC700e916E5c4DE0C41F410Fb05ab5337DcD20051' ether_wallet = EtherWallet(portal_address=ETHER_PORTAL_ADDRESS) app.add_router(ether_wallet) From efd043c2bcc61dcfd966968047c83511de4dd985 Mon Sep 17 00:00:00 2001 From: Lyno Ferraz Date: Tue, 1 Jul 2025 10:34:08 -0300 Subject: [PATCH 11/21] fix: utils imports --- cartesi/util.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/cartesi/util.py b/cartesi/util.py index 09fe894..81e3b0f 100644 --- a/cartesi/util.py +++ b/cartesi/util.py @@ -10,8 +10,8 @@ # CONDITIONS OF ANY KIND, either express or implied. See the License for the # specific language governing permissions and limitations under the License. -from eth_utils import function_signature_to_4byte_selector -from eth_abi import decode, encode +from Crypto.Hash import keccak +from eth_abi_lite import decode_abi, encode_abi from typing import List, Tuple, Any @@ -37,10 +37,10 @@ def decode_payload(types: List[str], payload: str, ) -> Tuple[Any, ...]: """ # Ensure the payload is in bytes format payload_bytes = bytes.fromhex(payload[2:]) # Remove '0x' prefix and convert to bytes - return decode(types, payload_bytes) + return decode_abi(types, payload_bytes) def decode_id_val(payload: str) -> Tuple[Any, ...]: - return decode(['uint256[]', 'uint256[]'], payload) + return decode_abi(['uint256[]', 'uint256[]'], payload) def encode_function_call(function_signature: str, types: List[str], values: List) -> str: @@ -56,13 +56,16 @@ def encode_function_call(function_signature: str, types: List[str], values: List str: The hex-encoded function call data. """ # Get the 4-byte selector from the function signature - selector = function_signature_to_4byte_selector(function_signature) - + sig_hash = keccak.new(digest_bits=256) + sig_hash.update(function_signature.encode('utf-8')) + + selector = sig_hash.digest()[:4].hex() + # Encode the values based on the provided types - encoded_params = encode(types, values) - + encoded_params = encode_abi(types, values) + # Concatenate the selector and the encoded parameters return "0x" + (selector + encoded_params).hex() def encode_values(types: List[str], values: List): - return encode(types, values) \ No newline at end of file + return encode_abi(types, values) From 77a2e0db0e831723c4b5fb731202f3a2f1aee2dc Mon Sep 17 00:00:00 2001 From: Lyno Ferraz Date: Wed, 28 Jan 2026 16:03:42 -0300 Subject: [PATCH 12/21] feat: Add cmpy rollup --- cartesi/app.py | 11 +++-- cartesi/cmpy_rollup.py | 99 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 cartesi/cmpy_rollup.py diff --git a/cartesi/app.py b/cartesi/app.py index 5e2f254..6531aba 100644 --- a/cartesi/app.py +++ b/cartesi/app.py @@ -2,7 +2,7 @@ from logging import getLogger, debug from .models import RollupResponse -from .rollup import Rollup, HTTPRollupServer +from .rollup import Rollup from .router import Router LOGGER = getLogger(__name__) @@ -11,7 +11,7 @@ class App: - def __init__(self, raw_input = False): + def __init__(self, raw_input = False, use_cmpy = False): self.routers: list[Router] = [] self.default_advance_handler = lambda rollup, data: False self.default_inspect_handler = lambda rollup, data: False @@ -74,6 +74,11 @@ def add_router(self, router: Router): def run(self): if self.rollup is None: - self.rollup = HTTPRollupServer(raw_input=self.raw_input) + if self.use_cmpy: + from .cmpy_rollup import CmpyRollupApp + self.rollup = CmpyRollupApp(raw_input=self.raw_input) + else: + from .rollup import HTTPRollupServer + self.rollup = HTTPRollupServer(raw_input=self.raw_input) self.rollup.set_handler(self._handle) self.rollup.main_loop() diff --git a/cartesi/cmpy_rollup.py b/cartesi/cmpy_rollup.py new file mode 100644 index 0000000..31f4d09 --- /dev/null +++ b/cartesi/cmpy_rollup.py @@ -0,0 +1,99 @@ +from logging import getLogger +from cmpy import Rollup as CmpyRollup +import re + +from .rollup import Rollup +from .models import RollupResponse, EvmAdvance, evm_advance_header +from .abi import decode_to_model + +LOGGER = getLogger(__name__) + +def to_bytes(payload): + if isinstance(payload,bytes): + return payload + if isinstance(payload,str): + if payload.startswith('0x'): + return bytes.fromhex(payload[2:]) + if bool(re.fullmatch(r"[0-9a-fA-F]+", payload)) and len(payload) % 2 == 0: + return bytes.fromhex(payload) + return payload.encode('utf-8') + return bytes(payload) + +class CmpyRollupApp(Rollup): + """Libcma and Libcmt Rollup based""" + _rollup: CmpyRollup + + def __init__(self): + super().__init__() + self._rollup = CmpyRollup() + + def main_loop(self): + accept_previous_request = True + + finish = {'status': 'accept'} + while True: + + LOGGER.info("Sending finish") + next_request_type = self._rollup.finish(accept_previous_request) + + rollup_response = {} + if next_request_type == 'advance': + advance = rollup.read_advance_state() + rollup_response = { + 'metadata': { + 'chain_id': advance['chain_id'], + 'app_contract': "0x" + advance['app_contract'].hex(), + 'msg_sender': "0x" + advance['msg_sender'].hex(), + 'input_index': advance['input_index'], + 'block_number': advance['block_number'], + 'block_timestamp': advance['block_timestamp'], + 'prev_randao': "0x" + advance['prev_randao'].hex() + }, + 'payload': "0x" + advance['payload']['data'].hex() + } + elif next_request_type == 'inspect': + inspect = rollup.read_inspect_state() + rollup_response = { + 'payload': "0x" + inspect['payload']['data'].hex() + } + else: + LOGGER.error("Invalid request type.") + accept_previous_request = False + continue + + rollup_response = RollupResponse.parse_obj(rollup_response) + + handler = self.handler + if handler is not None: + accept_previous_request = handler(rollup_response) + else: + LOGGER.error("No handler found for message.") + accept_previous_request = False + + def notice(self, payload: str): + LOGGER.info("Adding notice") + payload_bytes = to_bytes(payload) + rollup.emit_notice(payload_bytes) + return b'' + + def report(self, payload): + LOGGER.info("Adding report") + payload_bytes = to_bytes(payload) + self._rollup.emit_report(payload_bytes) + return b'' + + def voucher(self, payload: dict): + LOGGER.info("Adding voucher") + payload_bytes = to_bytes(payload['payload']) + self._rollup.emit_voucher(payload['destination'], int(payload['value'],16), payload_bytes) + return b'' + + def delegate_call_voucher(self, payload: dict): + LOGGER.info("Adding delegate call voucher") + payload_bytes = to_bytes(payload) + self._rollup.emit_delegate_call_voucher(payload['destination'], payload_bytes) + return b'' + + def gio(self, payload: dict): + LOGGER.error("Gio not supported") + return b'' From 427438dc34594ae47123492a468ae0f6e57186ed Mon Sep 17 00:00:00 2001 From: Lyno Ferraz Date: Wed, 4 Feb 2026 12:08:15 -0300 Subject: [PATCH 13/21] feat: use pycmt --- cartesi/app.py | 5 ++- cartesi/cmpy_rollup.py | 99 ------------------------------------------ 2 files changed, 3 insertions(+), 101 deletions(-) delete mode 100644 cartesi/cmpy_rollup.py diff --git a/cartesi/app.py b/cartesi/app.py index 6531aba..dce2475 100644 --- a/cartesi/app.py +++ b/cartesi/app.py @@ -17,6 +17,7 @@ def __init__(self, raw_input = False, use_cmpy = False): self.default_inspect_handler = lambda rollup, data: False self.rollup: Rollup | None = None self.raw_input = raw_input + self.use_cmpy = use_cmpy def advance(self): """Decorator for inserting handle advance""" @@ -75,8 +76,8 @@ def add_router(self, router: Router): def run(self): if self.rollup is None: if self.use_cmpy: - from .cmpy_rollup import CmpyRollupApp - self.rollup = CmpyRollupApp(raw_input=self.raw_input) + from .pycmt_rollup import CmtRollupApp + self.rollup = CmtRollupApp() else: from .rollup import HTTPRollupServer self.rollup = HTTPRollupServer(raw_input=self.raw_input) diff --git a/cartesi/cmpy_rollup.py b/cartesi/cmpy_rollup.py deleted file mode 100644 index 31f4d09..0000000 --- a/cartesi/cmpy_rollup.py +++ /dev/null @@ -1,99 +0,0 @@ -from logging import getLogger -from cmpy import Rollup as CmpyRollup -import re - -from .rollup import Rollup -from .models import RollupResponse, EvmAdvance, evm_advance_header -from .abi import decode_to_model - -LOGGER = getLogger(__name__) - -def to_bytes(payload): - if isinstance(payload,bytes): - return payload - if isinstance(payload,str): - if payload.startswith('0x'): - return bytes.fromhex(payload[2:]) - if bool(re.fullmatch(r"[0-9a-fA-F]+", payload)) and len(payload) % 2 == 0: - return bytes.fromhex(payload) - return payload.encode('utf-8') - return bytes(payload) - -class CmpyRollupApp(Rollup): - """Libcma and Libcmt Rollup based""" - _rollup: CmpyRollup - - def __init__(self): - super().__init__() - self._rollup = CmpyRollup() - - def main_loop(self): - accept_previous_request = True - - finish = {'status': 'accept'} - while True: - - LOGGER.info("Sending finish") - next_request_type = self._rollup.finish(accept_previous_request) - - rollup_response = {} - if next_request_type == 'advance': - advance = rollup.read_advance_state() - rollup_response = { - 'metadata': { - 'chain_id': advance['chain_id'], - 'app_contract': "0x" + advance['app_contract'].hex(), - 'msg_sender': "0x" + advance['msg_sender'].hex(), - 'input_index': advance['input_index'], - 'block_number': advance['block_number'], - 'block_timestamp': advance['block_timestamp'], - 'prev_randao': "0x" + advance['prev_randao'].hex() - }, - 'payload': "0x" + advance['payload']['data'].hex() - } - elif next_request_type == 'inspect': - inspect = rollup.read_inspect_state() - rollup_response = { - 'payload': "0x" + inspect['payload']['data'].hex() - } - else: - LOGGER.error("Invalid request type.") - accept_previous_request = False - continue - - rollup_response = RollupResponse.parse_obj(rollup_response) - - handler = self.handler - if handler is not None: - accept_previous_request = handler(rollup_response) - else: - LOGGER.error("No handler found for message.") - accept_previous_request = False - - def notice(self, payload: str): - LOGGER.info("Adding notice") - payload_bytes = to_bytes(payload) - rollup.emit_notice(payload_bytes) - return b'' - - def report(self, payload): - LOGGER.info("Adding report") - payload_bytes = to_bytes(payload) - self._rollup.emit_report(payload_bytes) - return b'' - - def voucher(self, payload: dict): - LOGGER.info("Adding voucher") - payload_bytes = to_bytes(payload['payload']) - self._rollup.emit_voucher(payload['destination'], int(payload['value'],16), payload_bytes) - return b'' - - def delegate_call_voucher(self, payload: dict): - LOGGER.info("Adding delegate call voucher") - payload_bytes = to_bytes(payload) - self._rollup.emit_delegate_call_voucher(payload['destination'], payload_bytes) - return b'' - - def gio(self, payload: dict): - LOGGER.error("Gio not supported") - return b'' From f4391f86f08129fc60dbaeb03039b02a99504133 Mon Sep 17 00:00:00 2001 From: Lyno Ferraz Date: Wed, 4 Feb 2026 12:41:05 -0300 Subject: [PATCH 14/21] feat: use pycmt fix --- cartesi/app.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cartesi/app.py b/cartesi/app.py index dce2475..427f54b 100644 --- a/cartesi/app.py +++ b/cartesi/app.py @@ -11,13 +11,13 @@ class App: - def __init__(self, raw_input = False, use_cmpy = False): + def __init__(self, raw_input = False, use_pycmt = False): self.routers: list[Router] = [] self.default_advance_handler = lambda rollup, data: False self.default_inspect_handler = lambda rollup, data: False self.rollup: Rollup | None = None self.raw_input = raw_input - self.use_cmpy = use_cmpy + self.use_pycmt = use_pycmt def advance(self): """Decorator for inserting handle advance""" @@ -75,7 +75,7 @@ def add_router(self, router: Router): def run(self): if self.rollup is None: - if self.use_cmpy: + if self.use_pycmt: from .pycmt_rollup import CmtRollupApp self.rollup = CmtRollupApp() else: From 33c9ecf6b6d5e2429ef1ed974b45f7b7b53d10fb Mon Sep 17 00:00:00 2001 From: Lyno Ferraz Date: Wed, 4 Feb 2026 12:44:31 -0300 Subject: [PATCH 15/21] feat: added pycmt file, updated ether wallet --- cartesi/pycmt_rollup.py | 96 ++++++++++++++++++++++++++++++++++++++++ examples/ether_wallet.py | 2 +- pyproject.toml | 2 +- 3 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 cartesi/pycmt_rollup.py diff --git a/cartesi/pycmt_rollup.py b/cartesi/pycmt_rollup.py new file mode 100644 index 0000000..a22296d --- /dev/null +++ b/cartesi/pycmt_rollup.py @@ -0,0 +1,96 @@ +from logging import getLogger +from pycmt import Rollup as CmtRollup +import re + +from .rollup import Rollup +from .models import RollupResponse + +LOGGER = getLogger(__name__) + +def to_bytes(payload): + if isinstance(payload,bytes): + return payload + if isinstance(payload,str): + if payload.startswith('0x'): + return bytes.fromhex(payload[2:]) + if bool(re.fullmatch(r"[0-9a-fA-F]+", payload)) and len(payload) % 2 == 0: + return bytes.fromhex(payload) + return payload.encode('utf-8') + return bytes(payload) + +class CmtRollupApp(Rollup): + """Libcma and Libcmt Rollup based""" + _rollup: CmtRollup + + def __init__(self): + super().__init__() + self._rollup = CmtRollup() + + def main_loop(self): + accept_previous_request = True + + while True: + LOGGER.info("Sending finish") + next_request_type = self._rollup.finish(accept_previous_request) + + rollup_response = {} + if next_request_type == 'advance': + advance = self._rollup.read_advance_state() + rollup_response = { + 'metadata': { + 'chain_id': advance['chain_id'], + 'app_contract': "0x" + advance['app_contract'].hex(), + 'msg_sender': "0x" + advance['msg_sender'].hex(), + 'input_index': advance['input_index'], + 'block_number': advance['block_number'], + 'block_timestamp': advance['block_timestamp'], + 'prev_randao': "0x" + advance['prev_randao'].hex() + }, + 'payload': "0x" + advance['payload']['data'].hex() + } + elif next_request_type == 'inspect': + inspect = self._rollup.read_inspect_state() + rollup_response = { + 'payload': "0x" + inspect['payload']['data'].hex() + } + else: + LOGGER.error("Invalid request type.") + accept_previous_request = False + continue + + rollup_response = RollupResponse.parse_obj(rollup_response) + + handler = self.handler + if handler is not None: + accept_previous_request = handler(rollup_response) + else: + LOGGER.error("No handler found for message.") + accept_previous_request = False + + def notice(self, payload: str): + LOGGER.info("Adding notice") + payload_bytes = to_bytes(payload) + self._rollup.emit_notice(payload_bytes) + return b'' + + def report(self, payload): + LOGGER.info("Adding report") + payload_bytes = to_bytes(payload) + self._rollup.emit_report(payload_bytes) + return b'' + + def voucher(self, payload: dict): + LOGGER.info("Adding voucher") + payload_bytes = to_bytes(payload['payload']) + self._rollup.emit_voucher(payload['destination'], int(payload['value'],16), payload_bytes) + return b'' + + def delegate_call_voucher(self, payload: dict): + LOGGER.info("Adding delegate call voucher") + payload_bytes = to_bytes(payload) + self._rollup.emit_delegate_call_voucher(payload['destination'], payload_bytes) + return b'' + + def gio(self, payload: dict): + LOGGER.error("Gio not supported") + return b'' diff --git a/examples/ether_wallet.py b/examples/ether_wallet.py index 3549b81..19f7b1d 100644 --- a/examples/ether_wallet.py +++ b/examples/ether_wallet.py @@ -9,7 +9,7 @@ app = App() -ETHER_PORTAL_ADDRESS = '0xC700e916E5c4DE0C41F410Fb05ab5337DcD20051' +ETHER_PORTAL_ADDRESS = '0xA632c5c05812c6a6149B7af5C56117d1D2603828' ether_wallet = EtherWallet(portal_address=ETHER_PORTAL_ADDRESS) app.add_router(ether_wallet) diff --git a/pyproject.toml b/pyproject.toml index 98a7ed1..9d96f1a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "python-cartesi" -version = "0.2.0" +version = "0.2.1" authors = [ { name="Felipe Fink Grael", email="felipefg@prototyp3.dev" },{ name="Lyno Ferraz", email="lyno@prototyp3.dev" } ] From 3a2de3825a4bc910717d5d096b6dee4719cd6ab5 Mon Sep 17 00:00:00 2001 From: Lyno Ferraz Date: Wed, 4 Feb 2026 16:17:19 -0300 Subject: [PATCH 16/21] feat: added optional dependencies --- pyproject.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 9d96f1a..d59c566 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,11 @@ dependencies = [ "pycryptodome ~= 3.19.0", ] +[project.optional-dependencies] +machine = [ + "pycmt >= 0.0.1", +] + [project.urls] "Homepage" = "https://github.com/prototyp3-dev/python-cartesi" From 896712ce056fd15daa7117b80fc6defe915edba8 Mon Sep 17 00:00:00 2001 From: Lyno Ferraz Date: Thu, 5 Feb 2026 17:42:57 -0300 Subject: [PATCH 17/21] feat: pycmt rollup fix --- cartesi/app.py | 2 ++ cartesi/pycmt_rollup.py | 43 +++++++++++++++++++++++------------------ 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/cartesi/app.py b/cartesi/app.py index 427f54b..96570f1 100644 --- a/cartesi/app.py +++ b/cartesi/app.py @@ -34,9 +34,11 @@ def inspect(self): """Decorator for inserting handle advance""" def decorator(func): + LOGGER.debug("Adding func %s to inspect_handler", repr(func)) self.default_inspect_handler = func return func + LOGGER.debug('Returning an Inspect Decorator') return decorator def _get_default_handler(self, request: RollupResponse): diff --git a/cartesi/pycmt_rollup.py b/cartesi/pycmt_rollup.py index a22296d..13b7415 100644 --- a/cartesi/pycmt_rollup.py +++ b/cartesi/pycmt_rollup.py @@ -32,27 +32,30 @@ def main_loop(self): while True: LOGGER.info("Sending finish") next_request_type = self._rollup.finish(accept_previous_request) - - rollup_response = {} + rollup_response = { + 'request_type': None, + 'data': {}, + } + LOGGER.debug(f"Received {next_request_type} input") if next_request_type == 'advance': advance = self._rollup.read_advance_state() - rollup_response = { - 'metadata': { - 'chain_id': advance['chain_id'], - 'app_contract': "0x" + advance['app_contract'].hex(), - 'msg_sender': "0x" + advance['msg_sender'].hex(), - 'input_index': advance['input_index'], - 'block_number': advance['block_number'], - 'block_timestamp': advance['block_timestamp'], - 'prev_randao': "0x" + advance['prev_randao'].hex() - }, - 'payload': "0x" + advance['payload']['data'].hex() + rollup_response['data']['metadata'] = { + 'chain_id': advance['chain_id'], + 'app_contract': "0x" + advance['app_contract'].hex(), + 'msg_sender': "0x" + advance['msg_sender'].hex(), + 'input_index': advance['index'], + 'block_number': advance['block_number'], + 'block_timestamp': advance['block_timestamp'], + 'prev_randao': "0x" + advance['prev_randao'].hex() } + LOGGER.debug(f"Advance state {rollup_response}") + rollup_response['data']['payload'] = "0x" + advance['payload']['data'].hex() + rollup_response['request_type'] = 'advance_state' elif next_request_type == 'inspect': inspect = self._rollup.read_inspect_state() - rollup_response = { - 'payload': "0x" + inspect['payload']['data'].hex() - } + rollup_response['data']['payload'] = "0x" + inspect['payload']['data'].hex() + rollup_response['request_type'] = 'inspect_state' + LOGGER.debug(f"Inspect state {rollup_response}") else: LOGGER.error("Invalid request type.") accept_previous_request = False @@ -87,10 +90,12 @@ def voucher(self, payload: dict): def delegate_call_voucher(self, payload: dict): LOGGER.info("Adding delegate call voucher") - payload_bytes = to_bytes(payload) + payload_bytes = to_bytes(payload['payload']) self._rollup.emit_delegate_call_voucher(payload['destination'], payload_bytes) return b'' def gio(self, payload: dict): - LOGGER.error("Gio not supported") - return b'' + LOGGER.info("Adding gio request") + payload_bytes = to_bytes(payload['payload']) + ret = self._rollup.gio_request(payload['domain'], payload_bytes) + return bytes(ret['response_data'][:len(ret['response_data'])]) From 725db0656b4dd75c74727e1f5d0d5f3b4f4920aa Mon Sep 17 00:00:00 2001 From: Lyno Ferraz Date: Tue, 12 May 2026 16:41:29 -0300 Subject: [PATCH 18/21] feat: Add pycma --- cartesi/app.py | 6 ++- cartesi/pycma_rollup.py | 101 ++++++++++++++++++++++++++++++++++++++++ cartesi/pycmt_rollup.py | 2 +- pyproject.toml | 7 ++- 4 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 cartesi/pycma_rollup.py diff --git a/cartesi/app.py b/cartesi/app.py index 96570f1..d1d7ede 100644 --- a/cartesi/app.py +++ b/cartesi/app.py @@ -11,13 +11,14 @@ class App: - def __init__(self, raw_input = False, use_pycmt = False): + def __init__(self, raw_input = False, use_pycmt = False, use_pycma = False): self.routers: list[Router] = [] self.default_advance_handler = lambda rollup, data: False self.default_inspect_handler = lambda rollup, data: False self.rollup: Rollup | None = None self.raw_input = raw_input self.use_pycmt = use_pycmt + self.use_pycma = use_pycma def advance(self): """Decorator for inserting handle advance""" @@ -80,6 +81,9 @@ def run(self): if self.use_pycmt: from .pycmt_rollup import CmtRollupApp self.rollup = CmtRollupApp() + elif self.use_pycma: + from .pycma_rollup import CmaRollupApp + self.rollup = CmaRollupApp() else: from .rollup import HTTPRollupServer self.rollup = HTTPRollupServer(raw_input=self.raw_input) diff --git a/cartesi/pycma_rollup.py b/cartesi/pycma_rollup.py new file mode 100644 index 0000000..1b69a27 --- /dev/null +++ b/cartesi/pycma_rollup.py @@ -0,0 +1,101 @@ +from logging import getLogger +from pycma import RollupCma +import re + +from .rollup import Rollup +from .models import RollupResponse + +LOGGER = getLogger(__name__) + +def to_bytes(payload): + if isinstance(payload,bytes): + return payload + if isinstance(payload,str): + if payload.startswith('0x'): + return bytes.fromhex(payload[2:]) + if bool(re.fullmatch(r"[0-9a-fA-F]+", payload)) and len(payload) % 2 == 0: + return bytes.fromhex(payload) + return payload.encode('utf-8') + return bytes(payload) + +class CmaRollupApp(Rollup): + """Libcma and Libcmt Rollup based""" + _rollup: RollupCma + + def __init__(self): + super().__init__() + self._rollup = RollupCma() + + def main_loop(self): + accept_previous_request = True + + while True: + LOGGER.info("Sending finish") + next_request_type = self._rollup.finish(accept_previous_request) + rollup_response = { + 'request_type': None, + 'data': {}, + } + LOGGER.debug(f"Received {next_request_type} input") + if next_request_type == 'advance': + advance = self._rollup.read_advance_state() + rollup_response['data']['metadata'] = { + 'chain_id': advance['chain_id'], + 'app_contract': "0x" + advance['app_contract'].hex(), + 'msg_sender': "0x" + advance['msg_sender'].hex(), + 'input_index': advance['index'], + 'block_number': advance['block_number'], + 'block_timestamp': advance['block_timestamp'], + 'prev_randao': "0x" + advance['prev_randao'].hex() + } + LOGGER.debug(f"Advance state {rollup_response}") + rollup_response['data']['payload'] = "0x" + advance['payload']['data'].hex() + rollup_response['request_type'] = 'advance_state' + elif next_request_type == 'inspect': + inspect = self._rollup.read_inspect_state() + rollup_response['data']['payload'] = "0x" + inspect['payload']['data'].hex() + rollup_response['request_type'] = 'inspect_state' + LOGGER.debug(f"Inspect state {rollup_response}") + else: + LOGGER.error("Invalid request type.") + accept_previous_request = False + continue + + rollup_response = RollupResponse.parse_obj(rollup_response) + + handler = self.handler + if handler is not None: + accept_previous_request = handler(rollup_response) + else: + LOGGER.error("No handler found for message.") + accept_previous_request = False + + def notice(self, payload: str): + LOGGER.info("Adding notice") + payload_bytes = to_bytes(payload) + self._rollup.emit_notice(payload_bytes) + return b'' + + def report(self, payload): + LOGGER.info("Adding report") + payload_bytes = to_bytes(payload) + self._rollup.emit_report(payload_bytes) + return b'' + + def voucher(self, payload: dict): + LOGGER.info("Adding voucher") + payload_bytes = to_bytes(payload['payload']) + self._rollup.emit_voucher(payload['destination'], int(payload['value'],16), payload_bytes) + return b'' + + def delegate_call_voucher(self, payload: dict): + LOGGER.info("Adding delegate call voucher") + payload_bytes = to_bytes(payload['payload']) + self._rollup.emit_delegate_call_voucher(payload['destination'], payload_bytes) + return b'' + + def gio(self, payload: dict): + LOGGER.info("Adding gio request") + payload_bytes = to_bytes(payload['payload']) + ret = self._rollup.gio_request(payload['domain'], payload_bytes) + return bytes(ret['response_data'][:len(ret['response_data'])]) diff --git a/cartesi/pycmt_rollup.py b/cartesi/pycmt_rollup.py index 13b7415..7db4905 100644 --- a/cartesi/pycmt_rollup.py +++ b/cartesi/pycmt_rollup.py @@ -19,7 +19,7 @@ def to_bytes(payload): return bytes(payload) class CmtRollupApp(Rollup): - """Libcma and Libcmt Rollup based""" + """Libcmt Rollup based""" _rollup: CmtRollup def __init__(self): diff --git a/pyproject.toml b/pyproject.toml index d59c566..b240676 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "python-cartesi" -version = "0.2.1" +version = "0.2.2" authors = [ { name="Felipe Fink Grael", email="felipefg@prototyp3.dev" },{ name="Lyno Ferraz", email="lyno@prototyp3.dev" } ] @@ -24,6 +24,11 @@ dependencies = [ machine = [ "pycmt >= 0.0.1", ] +machine-asset = [ + "pycmt >= 0.0.1", + "pycma >= 0.0.1", +] + [project.urls] "Homepage" = "https://github.com/prototyp3-dev/python-cartesi" From 9cc83d23145f5f29192ac542d78b782b3e4dd245 Mon Sep 17 00:00:00 2001 From: Lyno Ferraz Date: Tue, 23 Jun 2026 20:42:52 -0300 Subject: [PATCH 19/21] chore: bump pycma --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b240676..205644e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "python-cartesi" -version = "0.2.2" +version = "0.2.3" authors = [ { name="Felipe Fink Grael", email="felipefg@prototyp3.dev" },{ name="Lyno Ferraz", email="lyno@prototyp3.dev" } ] @@ -26,7 +26,7 @@ machine = [ ] machine-asset = [ "pycmt >= 0.0.1", - "pycma >= 0.0.1", + "pycma >= 0.0.2", ] From f1af24a22da553c98051725f7c4425ab0fed0aaa Mon Sep 17 00:00:00 2001 From: Lyno Ferraz Date: Fri, 14 Aug 2026 20:16:57 -0300 Subject: [PATCH 20/21] chore: bump pycmt and pycma --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 205644e..0229f71 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,8 +25,8 @@ machine = [ "pycmt >= 0.0.1", ] machine-asset = [ - "pycmt >= 0.0.1", - "pycma >= 0.0.2", + "pycmt >= 0.0.2", + "pycma >= 0.0.3", ] From cc70e62aa04d2ffa5f8258cff2b19b04daa56799 Mon Sep 17 00:00:00 2001 From: Lyno Ferraz Date: Mon, 24 Aug 2026 18:15:18 -0300 Subject: [PATCH 21/21] fix: dep version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0229f71..0a3d4ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ dependencies = [ [project.optional-dependencies] machine = [ - "pycmt >= 0.0.1", + "pycmt >= 0.0.2", ] machine-asset = [ "pycmt >= 0.0.2",