diff --git a/cartesi-rollups_versioned_docs/version-1.5/tutorials/counter.md b/cartesi-rollups_versioned_docs/version-1.5/tutorials/counter.md new file mode 100644 index 00000000..803616a0 --- /dev/null +++ b/cartesi-rollups_versioned_docs/version-1.5/tutorials/counter.md @@ -0,0 +1,245 @@ +--- +id: counter +title: Build a counter Application +resources: + - url: https://github.com/Mugen-Builders/Counter-X-Marketplace-apps + title: Source code for the counter Application +--- + +This tutorial aims to guide you through creating and interacting with a basic Cartesi application, it'll take you through setting up your dev environment, creating a project then finally running and interacting with your application locally. + +We would also be providing the Rust, JavaScript, Python and Go implementation of the application, so you could choose whichever language you're more conversant with. + +## Set up your environment + +To build an application using Cartesi, it's necessary that you have the following tools installed: + +- Cartesi CLI: A simple tool for building applications on Cartesi. [Install Cartesi CLI for your OS of choice](../development/installation.md). + +- Docker Desktop 4.x: The tool you need to run the Cartesi Machine and its dependencies. [Install Docker for your OS of choice](https://www.docker.com/products/docker-desktop/). + +## Create an application template using the Cartesi CLI + +Creating an application template for your application is a generally simple process, to do this, we utilize the Cartesi CLI by running the below command: + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + + +

+
+```shell
+cartesi create counter --template javascript
+```
+
+
+
+ + +

+
+```shell
+cartesi create counter --template python
+```
+
+
+
+ + +

+
+```shell
+cartesi create counter --template rust
+```
+
+
+
+
+ +This command creates a directory called `counter` and depending on your selected language this directory would contain the necessary entry point file to start your application, for Python developers this would be `dapp.py` while for Rust users it would be `src/main.rs`, then finally for JavaScript users, the entry point file would be `src/index.js`. + +This entry point file contains the default template for interacting with the Cartesi Rollups HTTP Server, it also makes available, two function namely `handle_advance()` and `handle_inspect()` which process "advance / write" and "inspect / read" requests to the application. In the next section we would be updating these functions with the implementation for your application. + +## Implement the Application Logic + +We’ll build the counter with a simple object‑oriented design. It defines a Counter object with three methods: a constructor, `increment()` to increase the count, and `get()` to return the current count. + +We also update `handle_advance()` to increment the counter whenever an advance (write) request arrives, ignoring the request payload. And we update `handle_inspect()` to log the current counter value when an inspect (read) request arrives. + +Together, these handlers let you increment the counter and check its value. + +To try it locally, copy the snippet for your language and replace the contents of the entry point file in your `counter/` directory. + +import CounterJS from './snippets/counter-js.md'; +import CounterPY from './snippets/counter-py.md'; +import CounterRS from './snippets/counter-rs.md'; + + + +

+
+
+
+
+
+ + +

+
+
+
+
+
+ + +

+
+
+
+
+
+
+ +## Build and Run your Application + +Once you have your application logic written out, the next step is to build the application, this is done by running the below commands using the Cartesi CLI: + +```shell +cartesi build +``` + +- Expected Logs: + +```shell +user@user-MacBook-Pro counter % cartesi build +(node:4460) ExperimentalWarning: Importing JSON modules is an experimental feature and might change at any time +(Use `node --trace-warnings ...` to show where the warning was created) +✔ Build drives + ✔ Build drive root + + . + / \ + / \ +\---/---\ /----\ + \ X \ + \----/ \---/---\ + \ / CARTESI + \ / MACHINE + ' + +[INFO rollup_http_server] starting http dispatcher service... +[INFO rollup_http_server::http_service] starting http dispatcher http service! +[INFO actix_server::builder] starting 1 workers +[INFO actix_server::server] Actix runtime found; starting in Actix runtime +[INFO actix_server::server] starting service: "actix-web-service-127.0.0.1:5004", workers: 1, listening on: 127.0.0.1:5004 +[INFO rollup_http_server::dapp_process] starting dapp: python3 dapp.py +INFO:__main__:HTTP rollup_server url is http://127.0.0.1:5004 +INFO:__main__:Sending finish + +Manual yield rx-accepted (1) (0x000020 data) +Cycles: 8108719633 +8108719633: 107174e04a294787e22b6864c61fedd845833e5c8bc9a244480f2996ddabb3c7 +Storing machine: please wait +``` + +The build command compiles your application then builds a Cartesi machine that contains your application. + +This recently built machine alongside other necessary service, like an Anvil network, inspect service, etc. wound next be started by running the command: + +```bash +cartesi run +``` + +If the `run` command is successful, you should see logs similar to this: + +```bash +user@user-MacBook-Pro counter % cartesi run +(node:5404) ExperimentalWarning: Importing JSON modules is an experimental feature and might change at any time +(Use `node --trace-warnings ...` to show where the warning was created) +WARNING: default block is set to 'latest', production configuration will likely use 'finalized' +[+] Pulling 4/0 + ✔ database Skipped - Image is already present locally + ✔ rollups-node Skipped - Image is already present locally + ✔ anvil Skipped - Image is already present locally + ✔ proxy Skipped - Image is already present locally +✔ counter starting at http://127.0.0.1:6751 +✔ anvil service ready at http://127.0.0.1:6751/anvil +✔ rpc service ready at http://127.0.0.1:6751/rpc +✔ inspect service ready at http://127.0.0.1:6751/inspect/counter +✔ counter machine hash is 0x107174e04a294787e22b6864c61fedd845833e5c8bc9a244480f2996ddabb3c7 +✔ counter contract deployed at 0x94b32605a405d690934eb4ecc91856febfa747cc +(l) View logs (b) Build and redeploy (q) Quit +``` + +## Interacting with your Counter Application + +Interacting with your Counter application could be achieved either through initiating transactions on the local anvil network which was activated when you ran the `cartesi run` command or more easily through the Cartesi CLI, for this tutorial, we'll be using the Cartesi CLI to send an input to our application which would increase our counter variable. + +### 1. Query current count value + +We start by querying the current count value, this is done by making an inspect request to the counter application running locally, to achieve this we run the below command in a new terminal: + +```bash +curl -X POST http://127.0.0.1:6751/inspect/counter \ + -H "Content-Type: application/json" \ + -d '{""}' +``` + +:::note Inspect endpoint +Please note that if your application is running on a different port or your application is not named `counter` as in the guide, then you'll need to replace the inspect endpoint `http://127.0.0.1:6751/inspect/counter` with the endpoint provided after running the `cartesi run` command. +::: + +On success, we receive a confirmation response from the HTTP server, something similar to `{"status":"Accepted","reports":null,"processed_input_count":0}`, then on the terminal running our application when we press the `l` key to access our application logs we should get a log confirming that our application received the inspect call and should also contain a log of the current count value. + +```bash +[INFO rollup_http_server::http_service] received new request of type INSPECT +inspect_request.payload_length: 4 +[INFO actix_web::middleware::logger] 127.0.0.1 "POST /finish HTTP/1.1" 200 64 "-" "python-requests/2.31.0" 0.020096 +INFO:__main__:Received finish status 200 +INFO:__main__:Received inspect request data {'payload': '0x7b22227d'} +INFO:__main__:Current counter value: 0 +INFO:__main__:Sending finish +2025-11-09T17:47:44.661 INF Request executed service=inspect status=Accepted application=counter +``` + +As seen in the third to last line of our received log, we can see the `count value` returned to be `0` + +### 2. Increase count value + +Now that we've confirmed our count value to be zero (0), we would be sending an advance request using the CLI to increase the value of our counter by running the below command: + +```bash +cartesi send random_text +``` + +The above command sends an advance request with the payload "random_text" to our application, which ignores this payload then proceeds to increase out count value by `one (1)`, if this command is successful and our application process this request graciously, we should get a log similar to what's presented below on the terminal running our application: + +```bash +INFO:__main__:Received advance request data {'metadata': {'chain_id': 13370, 'app_contract': '0x9d40cfc42bb386b531c5d4eb3ade360f4105c4a3', 'msg_sender': '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', 'block_number': 630, 'block_timestamp': 1762711409, 'prev_randao': '0x63a2bb3993d9f9c371624f995a10a6f493e33c2535e62b32fee565f812b4c4ab', 'input_index': 0}, 'payload': '0x72616e646f6d5f74657874'} +INFO:__main__:Counter increment requested, new count value: 1 +INFO:__main__:Sending finish +``` + +The above logs prove that out application received the advance request, increased our count value, logs the updated count value then finishes that request successfully. + +As seen in the second to last line of the log, our count value has been increased from 0 to 1. To confirm this increase, we can run an inspect request once more to verify the current count value, and on running the same inspect command as last time, we obtain the updated logs below. + +```shell +[INFO rollup_http_server::http_service] received new request of type INSPECT +inspect_request.payload_length: 4 +[INFO actix_web::middleware::logger] 127.0.0.1 "POST /finish HTTP/1.1" 200 64 "-" "python-requests/2.31.0" 0.002048 +INFO:__main__:Received finish status 200 +INFO:__main__:Received inspect request data {'payload': '0x7b22227d'} +INFO:__main__:Current counter value: 1 +INFO:__main__:Sending finish +2025-11-09T18:22:45.142 INF Request executed service=inspect status=Accepted application=counter +``` + +From the latest application logs, it's now clear that the application's count value has been increased from 0 to one, and subsequent advance calls would further increase the count value. + +## Conclusion + +Congratulations, you've successfully bootstrapped, implemented, ran and interacted with your first Cartesi Application. + +For a more detailed version of this code, you can check the `counter` folder for your selected language in [this repository](https://github.com/Mugen-Builders/Counter-X-Marketplace-apps) diff --git a/cartesi-rollups_versioned_docs/version-1.5/tutorials/marketplace.md b/cartesi-rollups_versioned_docs/version-1.5/tutorials/marketplace.md new file mode 100644 index 00000000..f6b01be0 --- /dev/null +++ b/cartesi-rollups_versioned_docs/version-1.5/tutorials/marketplace.md @@ -0,0 +1,424 @@ +--- +id: marketplace +title: Build a marketplace Application +resources: + - url: https://github.com/Mugen-Builders/Counter-X-Marketplace-apps + title: Source code for the marketplace Application +--- + +In this tutorial we'll be building a simple NFT Marketplace application, where users are able to deposit a unique token to be sold at a fixed price, then other users are able to purchase and withdraw these purchased tokens to their wallet. + +This Tutorial is built using an Object oriented approach and aims to cover, application creation, Notice, Voucher and Report generation, we'll also be decoding and consuming the payload passed alongside advance and inspect requests. + +## How the Marketplace Works + +Now that the basic setup is done, we can focus on how the marketplace application actually works. +This tutorial uses an Object-Oriented approach. This means we will create a main Marketplace object that stores the application state and provides methods to update or retrieve that state. Then we'll build other handler functions that utilizes other helper functions to decode user requests then call the appropriate method to update the marketplace object. + +For simplicity, our marketplace will support only: + +- One specific ERC-721 (NFT) contract: This is the only collection users can list. + +- One specific ERC-20 token: This is the token users will use to purchase NFTs. + +### Advance Requests + +**1. Depositing / Listing NFTs** + +- A user sends an NFT to the Cartesi application through the ERC-721 Portal. +- When the application receives the deposit payload, it automatically lists the NFT for sale at a fixed price. +- The price is the same for every NFT in this tutorial to keep things simple. + +**2. Depositing Payment Tokens** + +- Users can then send the marketplace’s payment token to the application through the ERC-20 Portal. +- The application keeps track of how many tokens each user has deposited. + +**3. Buying an NFT** + +- When a user decides to buy an NFT listed on the marketplace, the application checks: + - Does the user have enough deposited tokens? + - Is the NFT still listed? + +- If the transaction is valid, the marketplace transfers the payment token to the seller then creates a voucher that sends the purchased NFT to the buyer. + +### Inspect Requests + +The Inspect route will support three simple read-only queries: + +**1. `get_user_erc20_balance`** + +**Description:** Shows how many tokens a user has stored in the marketplace. + +**Input:** User's address + +**Output:** Amount of ERC-20 tokens deposited. + +**2. `get_token_owner`** + +**Description:** Returns the current owner of a specific NFT. + +**Input:** Token ID + +**Output:** Address of current token owner. + +**3. `get_all_listed_tokens`** + +**Description:** Shows all NFTs currently listed for sale. + +**Input:** (none) + +**Output:** Array of Token IDs currently listed for sale. + +## Set up your environment + +To create a template for your project, we run the below command, based on your language of choice: + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + + +

+
+```shell
+cartesi create marketplace --template javascript
+```
+
+
+
+ + +

+
+```shell
+cartesi create marketplace --template python
+```
+
+
+
+ + +

+
+```shell
+cartesi create marketplace --template rust
+```
+
+
+
+
+ +## Install Project Dependencies + +Since this project would be covering hex payload encoding and decoding, as well as Output (Voucher, Notice, Report) generation, it's important that we install the necessary dependencies to aid these processes. + + + +

+
+```shell
+ npm add viem
+```
+
+
+
+ + +

+
+```shell
+cat > requirements.txt << 'EOF'
+--find-links https://prototyp3-dev.github.io/pip-wheels-riscv/wheels/
+requests==2.32.5
+eth-abi==5.2.0
+eth-utils==5.3.1
+regex==2025.11.3
+pycryptodome==3.23.0
+eth-hash==0.7.1
+EOF
+```
+
+
+
+ + +

+
+```shell
+cargo add hex serde ethers-core 
+```
+
+
+
+
+ +## Implement the Application Logic + +Based on the programming language you selected earlier, copy the appropriate code snippet, then paste in your local entry point file (`dapp.py` or `src/main.rs` or `src/index.js`), created in the setup step: + +import MarketplaceJS from './snippets/marketplace-js.md'; +import MarketplacePY from './snippets/marketplace-py.md'; +import MarketplaceRS from './snippets/marketplace-rs.md'; + + + +

+
+
+
+
+
+ + +

+
+
+
+
+
+ + +

+
+
+
+
+
+
+ +## Build and Run your Application + +Once you have your application logic written out, the next step is to build the application, this is done by running the below commands using the Cartesi CLI: + +```shell +cartesi build +``` + +- Expected Logs: + +```shell +user@user-MacBook-Pro marketplace % cartesi build +(node:4460) ExperimentalWarning: Importing JSON modules is an experimental feature and might change at any time +(Use `node --trace-warnings ...` to show where the warning was created) +✔ Build drives + ✔ Build drive root + + . + / \ + / \ +\---/---\ /----\ + \ X \ + \----/ \---/---\ + \ / CARTESI + \ / MACHINE + ' + +[INFO rollup_http_server] starting http dispatcher service... +[INFO rollup_http_server::http_service] starting http dispatcher http service! +[INFO actix_server::builder] starting 1 workers +[INFO actix_server::server] Actix runtime found; starting in Actix runtime +[INFO actix_server::server] starting service: "actix-web-service-127.0.0.1:5004", workers: 1, listening on: 127.0.0.1:5004 +[INFO rollup_http_server::dapp_process] starting dapp: python3 dapp.py +INFO:__main__:HTTP rollup_server url is http://127.0.0.1:5004 +INFO:__main__:Sending finish + +Manual yield rx-accepted (1) (0x000020 data) +Cycles: 8108719633 +8108719633: 107174e04a294787e22b6864c61fedd845833e5c8bc9a244480f2996ddabb3c7 +Storing machine: please wait +``` + +The build command compiles your application then builds a Cartesi machine that contains your application. + +This recently built machine alongside other necessary service, like an Anvil network, inspect service, etc. wound next be started by running the command: + +```bash +cartesi run +``` + +If the `run` command is successful, you should see logs similar to this: + +```bash +user@user-MacBook-Pro marketplace % cartesi run +(node:5404) ExperimentalWarning: Importing JSON modules is an experimental feature and might change at any time +(Use `node --trace-warnings ...` to show where the warning was created) +WARNING: default block is set to 'latest', production configuration will likely use 'finalized' +[+] Pulling 4/0 + ✔ database Skipped - Image is already present locally + ✔ rollups-node Skipped - Image is already present locally + ✔ anvil Skipped - Image is already present locally + ✔ proxy Skipped - Image is already present locally +✔ marketplace starting at http://127.0.0.1:6751 +✔ anvil service ready at http://127.0.0.1:6751/anvil +✔ rpc service ready at http://127.0.0.1:6751/rpc +✔ inspect service ready at http://127.0.0.1:6751/inspect/marketplace +✔ marketplace machine hash is 0x107174e04a294787e22b6864c61fedd845833e5c8bc9a244480f2996ddabb3c7 +✔ marketplace contract deployed at 0x94b32605a405d690934eb4ecc91856febfa747cc +(l) View logs (b) Build and redeploy (q) Quit +``` + +## Interacting with your Marketplace Application + +Once your Marketplace application is up and running (via cartesi run), you can interact with it in two main ways — either by sending on-chain transactions through the local Anvil network (Advance requests) or by making HTTP requests directly to the Rollups HTTP server’s Inspect endpoint (Inspect requests). + +In this section, we’ll focus on using the Cartesi CLI to send Advance requests, since it provides a much simpler and faster way to test your application locally. + +:::note Inspect endpoint +If your application is running on a different port or has a different name from marketplace, remember to replace the inspect endpoint http://127.0.0.1:6751/inspect/marketplace with the one displayed after running the cartesi run command. +Also, ensure that all CLI commands are executed from the root directory of your application. +::: + +### 1. Mint an ERC-721 Token and Grant Approval + +With your Marketplace application now deployed, the first step is to mint the NFT you plan to list and grant approval for it to be transferred via the ERC-721 portal. +Since our app uses the test ERC-721 and ERC-20 contracts automatically deployed by the CLI, you can use the commands below to mint your token and set the necessary approvals. + +- Mint token ID 1: + +```bash +cast send 0xBa46623aD94AB45850c4ecbA9555D26328917c3B \ + "safeMint(address, uint256, string)" \ + 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 1 "" \ + --rpc-url http://127.0.0.1:6751/anvil \ + --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 +``` + +This command calls the `safeMint` function in the `testNFT` contract deployed by the CLI, minting token `ID 1` to the address `0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266`. + +- Grant approval to the ERC721-Portal: + +Before an NFT can be deposited into the application, the portal contract must have permission to transfer it on behalf of the owner. Use the following command to grant that approval: + +```bash +cast send 0xBa46623aD94AB45850c4ecbA9555D26328917c3B \ + "setApprovalForAll(address,bool)" \ + 0xc700d52F5290e978e9CAe7D1E092935263b60051 true \ + --rpc-url http://127.0.0.1:6751/anvil \ + --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 +``` + +### 2. Deposit NFT with ID 1 to the Marketplace [advance request] + +Now that the NFT is minted and approved, it’s time to list it on the marketplace. +We’ll do this by depositing it using the Cartesi CLI: + +```bash +cartesi deposit erc721 +``` + +The CLI will prompt you for the token ID (enter 1) and the token address (press Enter to use the default test token). +Under the hood, the CLI transfers the NFT from your address to the ERC-721 portal, which then sends the deposit payload to your application. + +Once the deposit succeeds, the terminal running your application should show logs similar to: + +```bash +INFO rollup_http_server::http_service] received new request of type ADVANCE +[INFO actix_web::middleware::logger] 127.0.0.1 "POST /finish HTTP/1.1" 200 751 "-" "-" 0.018688 +Received finish status 200 OK +Received advance request data {"request_type":"advance_state","data":{"metadata":{"chain_id":13370,"app_contract":"0xb86306660e0be8e228c0cd14b8a1c5d5eddb8d20","msg_sender":"0xc700d52f5290e978e9cae7d1e092935263b60051","block_number":675,"block_timestamp":1762948794,"prev_randao":"0x3d144a3d5bb3125c92a230e7597e04e82eb5d5acbea185db2b1eadda3530d5c7","input_index":0},"payload":"0xba46623ad94ab45850c4ecba9555d26328917c3bf39fd6e51aad88f6f4ce6ab8827279cfffb9226600000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"}} +Token deposit and listing processed successfully +[INFO actix_web::middleware::logger] 127.0.0.1 "POST /notice HTTP/1.1" 201 11 "-" "-" 0.001088 +Sending finish +``` + +### 3. View All NFTs Listed on the Marketplace [inspect request] + +After depositing, your NFT is automatically listed. +To verify this, you can query the marketplace for all listed tokens using an Inspect request: + +```bash +curl -X POST http://127.0.0.1:6751/inspect/marketplace \ + -H "Content-Type: application/json" \ + -d '{"method":"get_all_listed_tokens"}' +``` + +This call returns a hex payload containing a list of all listed tokens on the marketplace. + +```bash +{"status":"Accepted","reports":[{"payload":"0x416c6c206c697374656420746f6b656e73206172653a205b315d"}],"processed_input_count":6} +``` + +The payload hex `0x416c6...205b315d` when decoded, returns `All listed tokens are: [1]`. Thereby confirming that the token with `Id 1` has successfully been listed. + +### 4. Deposit ERC20 token for making purchases [advance request] + +With the NFT successfully listed for sale, it's time to attempt to purchase this token, but before we do that, we'll need first deposit the required amount of tokens to purchase the listed NFT. Since our marketplace lists all NFT's at the price of `100 testTokens` we'll be transferring 100 tokens to the new address we'll be using to purchase, before proceeding with the purchase. + +- Transfer required tokens to purchase address. + +```bash +cast send 0xFBdB734EF6a23aD76863CbA6f10d0C5CBBD8342C \ +"transfer(address,uint256)" 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 100000000000000000000 \ +--private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \ +--rpc-url http://127.0.0.1:6751/anvil +``` + +- Deposit 100 tokens to the marketplace application. + +```bash +cartesi deposit --from 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 +``` + +The CLI will prompt you for the token address (press Enter to use the default test token), and then the amount of tokens we intend to deposit `(100)`. The CLI would finally proceed to grant the necessary approvals after which it would deposit the tokens to our application. + +On a successful deposit our application should return logs that look like this: + +```bash +[INFO rollup_http_server::http_service] received new request of type ADVANCE +[INFO actix_web::middleware::logger] 127.0.0.1 "POST /finish HTTP/1.1" 200 496 "-" "-" 0.018176 +Received finish status 200 OK +Received advance request data {"request_type":"advance_state","data":{"metadata":{"chain_id":13370,"app_contract":"0xb86306660e0be8e228c0cd14b8a1c5d5eddb8d20","msg_sender":"0xc700d6add016eecd59d989c028214eaa0fcc0051","block_number":2272,"block_timestamp":1762951994,"prev_randao":"0x0992ab8380b23c1c98928a76ae9a79c501ae27625943a53b0fd57455f10e5164","input_index":1},"payload":"0xfbdb734ef6a23ad76863cba6f10d0c5cbbd8342c70997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000000000000000000000000000056bc75e2d63100000"}} +Deposited token: 0xfbdb734ef6a23ad76863cba6f10d0c5cbbd8342c, Receiver: 0x70997970c51812dc3a010c7d01b50e0d17dc79c8, Amount: 100000000000000000000 +Token deposit processed successfully +Sending finish +``` + +### 5. Purchase Token with ID 1 [advance request] + +Now that the buyer has deposited funds, we can proceed to purchase the NFT. To do this we make an advance request to the application using the Cartesi CLI by running the command: + +```bash + cartesi send "{"method": "purchase_token", "token_id": 1}" --from 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 +``` + +This command notifies the marketplace that the address `0x7099797....0d17dc79C8` which initially deposited 100 tokens, would want to purchase token ID 1, The marketplace proceeds to run necessary checks like verifying that the token is for sale, and that the buyer has sufficient tokens to make the purchase, after which it executes the purchase and finally emits a voucher that transfers the tokens to the buyer's address. On a successful purchase, you should get logs similar to the below. + +```bash +[INFO rollup_http_server::http_service] received new request of type ADVANCE +[INFO actix_web::middleware::logger] 127.0.0.1 "POST /finish HTTP/1.1" 200 426 "-" "-" 0.001792 +Received finish status 200 OK +Received advance request data {"request_type":"advance_state","data":{"metadata":{"chain_id":13370,"app_contract":"0xb86306660e0be8e228c0cd14b8a1c5d5eddb8d20","msg_sender":"0x70997970c51812dc3a010c7d01b50e0d17dc79c8","block_number":3576,"block_timestamp":1762954606,"prev_randao":"0xd29cd42cfd3c9ff4f31b39f497fc2f4d0a7add5a67da98be0d7841c37c7ad25f","input_index":2},"payload":"0x7b6d6574686f643a2070757263686173655f746f6b656e2c20746f6b656e5f69643a20317d"}} +PURCHASE SUCCESSFUL, STRUCTURING VOUCHERS!! +[INFO actix_web::middleware::logger] 127.0.0.1 "POST /voucher HTTP/1.1" 201 11 "-" "-" 0.015424 +Voucher generation successful +Token purchased and Withdrawn successfully +``` + +### 6. Recheck NFTs Listed on the Marketplace [inspect request] + +Finally, we can confirm that the purchased NFT has been removed from the listings by running the inspect query again: + +```bash +curl -X POST http://127.0.0.1:6751/inspect/marketplace \ + -H "Content-Type: application/json" \ + -d '{"method":"get_all_listed_tokens"}' +``` + +This call returns a hex payload like below: + +```bash +{"status":"Accepted","reports":[{"payload":"0x416c6c206c697374656420746f6b656e73206172653a205b5d"}],"processed_input_count":7} +``` + +The payload hex `0x416c6...653a205b5d` when decoded, returns `All listed tokens are: []`. Thereby verifying that the token with `Id 1` has successfully been sold and no longer listed for sale in the marketplace. + +## Conclusion + +Congratulations!!! + +You’ve successfully built and interacted with your own Marketplace application on Cartesi. + +This example covered essential Cartesi concepts such as routing, asset management, voucher generation, and the use of both Inspect and Advance routes. + +For a more detailed version of this code, you can check the `marketplace` folder for your selected language in [this repository](https://github.com/Mugen-Builders/Counter-X-Marketplace-apps) \ No newline at end of file diff --git a/cartesi-rollups_versioned_docs/version-1.5/tutorials/snippets/counter-js.md b/cartesi-rollups_versioned_docs/version-1.5/tutorials/snippets/counter-js.md new file mode 100644 index 00000000..d70b32a9 --- /dev/null +++ b/cartesi-rollups_versioned_docs/version-1.5/tutorials/snippets/counter-js.md @@ -0,0 +1,64 @@ +```javascript +const rollup_server = process.env.ROLLUP_HTTP_SERVER_URL; +console.log("HTTP rollup_server url is " + rollup_server); + +class Counter { + constructor() { + this.count = 0; + } + + increment() { + this.count += 1; + return this.count; + } + + get() { + return this.count; + } +} + +var counter = new Counter(); + +async function handle_advance(data) { + console.log("Received advance request data " + JSON.stringify(data)); + var new_val = counter.increment(); + console.log(`Counter increment requested, new count value: ${new_val}`); + return "accept"; +} + +async function handle_inspect(data) { + console.log("Received inspect request data " + JSON.stringify(data)); + console.log(`Current counter value: ${counter.get()}`); + return "accept"; +} + +var handlers = { + advance_state: handle_advance, + inspect_state: handle_inspect, +}; + +var finish = { status: "accept" }; + +(async () => { + while (true) { + const finish_req = await fetch(rollup_server + "/finish", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ status: "accept" }), + }); + + console.log("Received finish status " + finish_req.status); + + if (finish_req.status == 202) { + console.log("No pending rollup request, trying again"); + } else { + const rollup_req = await finish_req.json(); + var handler = handlers[rollup_req["request_type"]]; + finish["status"] = await handler(rollup_req["data"]); + } + } +})(); + +``` diff --git a/cartesi-rollups_versioned_docs/version-1.5/tutorials/snippets/counter-py.md b/cartesi-rollups_versioned_docs/version-1.5/tutorials/snippets/counter-py.md new file mode 100644 index 00000000..9ea9c973 --- /dev/null +++ b/cartesi-rollups_versioned_docs/version-1.5/tutorials/snippets/counter-py.md @@ -0,0 +1,57 @@ +```python +from os import environ +import logging +import requests + +logging.basicConfig(level="INFO") +logger = logging.getLogger(__name__) + +rollup_server = environ["ROLLUP_HTTP_SERVER_URL"] +logger.info(f"HTTP rollup_server url is {rollup_server}") + +class Counter: + def __init__(self): + self.value = 0 + + def increment(self): + self.value += 1 + return self.value + + def get(self): + return self.value + +counter = Counter() + +def handle_advance(data): + logger.info(f"Received advance request data {data}") + new_val = counter.increment() + logger.info(f"Counter increment requested, new count value: {new_val}") + return "accept" + + +def handle_inspect(data): + logger.info(f"Received inspect request data {data}") + logger.info(f"Current counter value: {counter.get()}") + return "accept" + + +handlers = { + "advance_state": handle_advance, + "inspect_state": handle_inspect, +} + +finish = {"status": "accept"} + +while True: + logger.info("Sending finish") + response = requests.post(rollup_server + "/finish", json=finish) + logger.info(f"Received finish status {response.status_code}") + if response.status_code == 202: + logger.info("No pending rollup request, trying again") + else: + rollup_request = response.json() + data = rollup_request["data"] + handler = handlers[rollup_request["request_type"]] + finish["status"] = handler(rollup_request["data"]) + +``` diff --git a/cartesi-rollups_versioned_docs/version-1.5/tutorials/snippets/counter-rs.md b/cartesi-rollups_versioned_docs/version-1.5/tutorials/snippets/counter-rs.md new file mode 100644 index 00000000..3dfd8aa5 --- /dev/null +++ b/cartesi-rollups_versioned_docs/version-1.5/tutorials/snippets/counter-rs.md @@ -0,0 +1,99 @@ +```rust +use json::{object, JsonValue}; +use std::env; + +#[derive(Clone, Debug, Copy)] +pub struct Counter { + count: u64, +} + +impl Counter { + fn new() -> Self { + Counter { count: 0 } + } + + fn increment(&mut self) { + self.count += 1; + } + + fn get_count(&self) -> u64 { + self.count + } +} + +pub async fn handle_advance( + _client: &hyper::Client, + _server_addr: &str, + request: JsonValue, + counter: &mut Counter +) -> Result<&'static str, Box> { + println!("Received advance request data {}", &request); + let _payload = request["data"]["payload"] + .as_str() + .ok_or("Missing payload")?; + + counter.increment(); + println!("Counter increment requested, new count value: {}", counter.get_count()); + Ok("accept") +} + +pub async fn handle_inspect( + _client: &hyper::Client, + _server_addr: &str, + request: JsonValue, + counter: &mut Counter +) -> Result<&'static str, Box> { + println!("Received inspect request data {}", &request); + let _payload = request["data"]["payload"] + .as_str() + .ok_or("Missing payload")?; + + println!("fetching current count value"); + let current_count = counter.get_count(); + println!("Current counter value: {}", current_count); + Ok("accept") +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = hyper::Client::new(); + let server_addr = env::var("ROLLUP_HTTP_SERVER_URL")?; + + let mut counter = Counter::new(); + println!("Initial counter value: {}", counter.get_count()); + + let mut status = "accept"; + loop { + println!("Sending finish"); + let response = object! {"status" => status}; + let request = hyper::Request::builder() + .method(hyper::Method::POST) + .header(hyper::header::CONTENT_TYPE, "application/json") + .uri(format!("{}/finish", &server_addr)) + .body(hyper::Body::from(response.dump()))?; + let response = client.request(request).await?; + println!("Received finish status {}", response.status()); + + if response.status() == hyper::StatusCode::ACCEPTED { + println!("No pending rollup request, trying again"); + } else { + let body = hyper::body::to_bytes(response).await?; + let utf = std::str::from_utf8(&body)?; + let req = json::parse(utf)?; + + let request_type = req["request_type"] + .as_str() + .ok_or("request_type is not a string")?; + status = match request_type { + "advance_state" => handle_advance(&client, &server_addr[..], req, &mut counter).await?, + "inspect_state" => handle_inspect(&client, &server_addr[..], req, &mut counter).await?, + &_ => { + eprintln!("Unknown request type"); + "reject" + } + }; + } + } +} + +``` diff --git a/cartesi-rollups_versioned_docs/version-1.5/tutorials/snippets/marketplace-js.md b/cartesi-rollups_versioned_docs/version-1.5/tutorials/snippets/marketplace-js.md new file mode 100644 index 00000000..d7434c14 --- /dev/null +++ b/cartesi-rollups_versioned_docs/version-1.5/tutorials/snippets/marketplace-js.md @@ -0,0 +1,424 @@ +```javascript +import { encodeFunctionData, toHex, zeroHash } from "viem"; + +const rollup_server = process.env.ROLLUP_HTTP_SERVER_URL; +console.log("HTTP rollup_server url is " + rollup_server); + +const asBigInt = (v) => (typeof v === "bigint" ? v : BigInt(v)); +const normAddr = (a) => a.toLowerCase(); +const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; +const erc721Abi = [ + { + name: "transferFrom", + type: "function", + stateMutability: "nonpayable", + inputs: [ + { name: "from", type: "address" }, + { name: "to", type: "address" }, + { name: "tokenId", type: "uint256" }, + ], + outputs: [], + }, +]; + +class Storage { + constructor(erc721_portal_address, erc20_portal_address, erc721_token, erc20_token, list_price) { + this.erc721_portal_address = erc721_portal_address; + this.erc20_portal_address = erc20_portal_address; + this.erc721_token = erc721_token; + this.erc20_token = erc20_token; + this.application_address = normAddr(ZERO_ADDRESS); + this.list_price = list_price + + this.listed_tokens = []; + this.users_erc20_token_balance = new Map(); + this.user_erc721_token_balance = new Map(); + this.erc721_id_to_owner_address = new Map(); + } + + getListedTokens() { + return this.listed_tokens; + } + + getAppAddress() { + return this.application_address; + } + + setAppAddress(app_addr) { + this.application_address = normAddr(app_addr); + } + + getUserERC721TokenBalance(userAddress) { + return this.user_erc721_token_balance.get(normAddr(userAddress)); + } + + getERC721TokenOwner(tokenId) { + return this.erc721_id_to_owner_address.get(asBigInt(tokenId)); + } + + getUserERC20TokenBalance(userAddress) { + return this.users_erc20_token_balance.get(normAddr(userAddress)) || 0n; + } + + increaseUserBalance(userAddress, amount) { + const addr = normAddr(userAddress); + const current = this.users_erc20_token_balance.get(addr) || 0n; + this.users_erc20_token_balance.set(addr, current + BigInt(amount)); + } + + async reduceUserBalance(userAddress, amount) { + const addr = normAddr(userAddress); + const current = this.users_erc20_token_balance.get(addr); + + if (current === undefined || current < BigInt(amount)) { + await emitReport(`User ${addr} record not found`); + console.log("User balance record not found"); + return; + } + this.users_erc20_token_balance.set(addr, current - BigInt(amount)); + } + + depositERC721Token(userAddress, tokenId) { + const addr = normAddr(userAddress); + const tid = asBigInt(tokenId); + this.erc721_id_to_owner_address.set(tid, addr); + + let previous_owner = this.getERC721TokenOwner(tid); + + if (normAddr(previous_owner) === normAddr(ZERO_ADDRESS)) { + this.changeERC721TokenOwner(tid, addr, normAddr(ZERO_ADDRESS)); + } else { + const tokens = this.user_erc721_token_balance.get(addr) || []; + if (!tokens.some((t) => t === tid)) tokens.push(tid); + this.user_erc721_token_balance.set(addr, tokens); + } + } + + listTokenForSale(tokenId) { + const tid = asBigInt(tokenId); + if (!this.listed_tokens.some((id) => id === tid)) this.listed_tokens.push(tid); + } + + changeERC721TokenOwner(tokenId, newOwner, oldOwner) { + const tid = asBigInt(tokenId); + const newAddr = normAddr(newOwner); + const oldAddr = normAddr(oldOwner); + + this.erc721_id_to_owner_address.set(tid, newAddr); + + const newOwnerTokens = this.user_erc721_token_balance.get(newAddr) || []; + if (!newOwnerTokens.some((id) => id === tid)) newOwnerTokens.push(tid); + this.user_erc721_token_balance.set(newAddr, newOwnerTokens); + + const oldOwnerTokens = this.user_erc721_token_balance.get(oldAddr) || []; + this.user_erc721_token_balance.set(oldAddr, oldOwnerTokens.filter((id) => id !== tid)); + } + + + async purchaseERC721Token(buyerAddress, erc721TokenAddress, tokenId) { + const tid = asBigInt(tokenId); + + if (!storage.listed_tokens.includes(tokenId)) { + await emitReport(`Token ${erc721TokenAddress} with id ${tid} is not for sale`); + console.log("Token is not for sale"); + return; + } + const owner = this.getERC721TokenOwner(tid); + if (!owner) { + await emitReport(`Token owner for token ${erc721TokenAddress} with id ${tid} not found`); + console.log("Token owner not found"); + return; + } + + await this.reduceUserBalance(buyerAddress, storage.list_price); + this.increaseUserBalance(owner, storage.list_price); + this.changeERC721TokenOwner(tid, ZERO_ADDRESS, owner); + this.listed_tokens = this.listed_tokens.filter((id) => id !== tid); + } +} + +async function handleERC20Deposit(depositorAddress, amountDeposited, tokenAddress) { + if (normAddr(tokenAddress) === normAddr(storage.erc20_token)) { + try { + storage.increaseUserBalance(depositorAddress, amountDeposited); + console.log("Token deposit processed successfully"); + } catch (error) { + console.log("error, handing ERC20 deposit ", error) + await emitReport(error.toString()); + } + } else { + console.log("Unsupported token deposited"); + await emitReport("Unsupported token deposited"); + } +} + +async function handleERC721Deposit(depositorAddress, tokenId, tokenAddress) { + if (normAddr(tokenAddress) === normAddr(storage.erc721_token)) { + try { + storage.depositERC721Token(depositorAddress, tokenId); + storage.listTokenForSale(tokenId); + console.log("Token deposit and Listing processed successfully"); + emitNotice("Token ID: " + tokenId + " Deposited by User: " + depositorAddress) + } catch (error) { + console.log("error, handing ERC721 deposit ", error) + await emitReport(error.toString()); + } + } else { + console.log("Unsupported token deposited"); + await emitReport("Unsupported token deposited"); + } +} + + +function tokenDepositParse(payload) { + const hexstr = payload.startsWith("0x") ? payload.slice(2) : payload; + const bytes = Buffer.from(hexstr, "hex"); + + if (bytes.length < 20 + 20 + 32) { + console.log(`payload too short: ${bytes.length} bytes`); + } + + const token = bytes.slice(0, 20); + const receiver = bytes.slice(20, 40); + const amount_be = bytes.slice(40, 72); + + for (let i = 0; i < 16; i++) { + if (amount_be[i] !== 0) { + console.log("amount too large for u128"); + } + } + + const lo = amount_be.slice(16); + let amount = 0n; + for (const b of lo) { + amount = (amount << 8n) + BigInt(b); + } + + return { + token: "0x" + token.toString("hex"), + receiver: "0x" + receiver.toString("hex"), + amount: amount.toString(), + }; +} + +async function extractField(json, field) { + const value = json[field]; + + if (typeof value === "string" && value.trim() !== "") { + return value; + } else { + await emitReport(`Missing or invalid ${field} field in payload`); + console.log(`Missing or invalid ${field} field in payload`); + } +} + + +async function handlePurchaseToken(callerAddress, userInput) { + try { + const erc721TokenAddress = normAddr(storage.erc721_token); + const tokenId = BigInt(await extractField(userInput, "token_id")); + + try { + await storage.purchaseERC721Token(callerAddress, erc721TokenAddress, tokenId); + console.log("Token purchased successfully"); + let voucher = structureVoucher({ + abi: erc721Abi, + functionName: "transferFrom", + args: [storage.application_address, callerAddress, tokenId], + destination: storage.erc721_token, + }) + emitVoucher(voucher); + } catch (e) { + await emitReport(`Failed to purchase token: ${e.message}`); + console.log(`Failed to purchase token: ${e.message}`); + } + } catch (error) { + console.log("error purchasing token: ", error); + await emitReport(error.toString()); + } +} + +function hexToString(hex) { + if (typeof hex !== "string") return ""; + if (hex.startsWith("0x")) hex = hex.slice(2); + return Buffer.from(hex, "hex").toString("utf8"); +} + +async function handle_advance(data) { + console.log("Received advance request data " + JSON.stringify(data)); + + const sender = data["metadata"]["msg_sender"]; + app_contract = normAddr(data["metadata"]["app_contract"]) + + if (normAddr(storage.application_address) == normAddr(ZERO_ADDRESS)) { + storage.setAppAddress(app_contract); + } + + const payload = hexToString(data.payload); + + if (normAddr(sender) == normAddr(storage.erc20_portal_address)) { + let { token, receiver, amount } = tokenDepositParse(data.payload); + await handleERC20Deposit(receiver, amount, token); + } else if (normAddr(sender) == normAddr(storage.erc721_portal_address)) { + let { token, receiver, amount } = tokenDepositParse(data.payload) + await handleERC721Deposit(receiver, asBigInt(amount), token); + } else { + const payload_obj = JSON.parse(payload); + let method = payload_obj["method"]; + switch (method) { + case "purchase_token": { + await handlePurchaseToken(sender, payload_obj); + break; + } + default: {console.log("Unwupported method called!!"); emitReport("Unwupported method called!!")} + } + } + return "accept"; +} + +async function handle_inspect(data) { + console.log("Received inspect request data " + JSON.stringify(data)); + + const payload = hexToString(data.payload); + let payload_obj; + + try { + payload_obj = JSON.parse(payload); + } catch (e) { + await emitReport("Invalid payload JSON"); + return "accept"; + } + + switch (payload_obj.method) { + case "get_user_erc20_balance": { + const user_address = payload_obj["user_address"]; + const bal = storage.getUserERC20TokenBalance(normAddr(user_address)); + await emitReport(`User: ${user_address} Balance: ${bal.toString()}`); + break; + } + case "get_token_owner": { + const token_id = BigInt(payload_obj["token_id"]); + const token_owner = storage.getERC721TokenOwner(token_id); + await emitReport(`Token_id: ${token_id.toString()} owner: ${token_owner ?? "None"}`); + break; + } + case "get_all_listed_tokens": { + const listed_tokens = storage.getListedTokens(); + await emitReport(`All listed tokens are: ${listed_tokens.map(String).join(",")}`); + break; + } + default: { + console.log("Unsupported method called!!"); + await emitReport("Unsupported inspect method"); + } + } + return "accept"; +} + +function stringToHex(str) { + if (typeof str !== "string") { + console.log("stringToHex: input must be a string"); + } + const utf8 = Buffer.from(str, "utf8"); + return "0x" + utf8.toString("hex"); +} + +function structureVoucher({ abi, functionName, args, destination, value = 0n }) { + const payload = encodeFunctionData({ + abi, + functionName, + args, + }); + + const valueHex = value === 0n ? zeroHash : toHex(BigInt(value)); + + return { + destination, + payload, + value: valueHex, + } +} + +const emitVoucher = async (voucher) => { + try { + await fetch(rollup_server + "/voucher", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(voucher), + }); + } catch (error) { + emitReport("error emitting Voucher"); + console.log("error emitting voucher: ", error) + } +}; + +const emitReport = async (payload) => { + let hexPayload = stringToHex(payload); + try { + await fetch(rollup_server + "/report", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ payload: hexPayload }), + }); + } catch (error) { + console.log("error emitting report: ", error) + } +}; + +const emitNotice = async (payload) => { + let hexPayload = stringToHex(payload); + try { + await fetch(rollup_server + "/notice", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ payload: hexPayload }), + }); + } catch (error) { + emitReport("error emitting Notice"); + console.log("error emitting notice: ", error) + } +}; + +var handlers = { + advance_state: handle_advance, + inspect_state: handle_inspect, +}; + +let erc721_portal_address = "0xc700d52F5290e978e9CAe7D1E092935263b60051"; +let erc20_portal_address = "0xc700D6aDd016eECd59d989C028214Eaa0fCC0051"; +let erc20_token = "0xFBdB734EF6a23aD76863CbA6f10d0C5CBBD8342C"; +let erc721_token = "0xBa46623aD94AB45850c4ecbA9555D26328917c3B"; +let list_price = BigInt("100000000000000000000"); + +var storage = new Storage(erc721_portal_address, erc20_portal_address, erc721_token, erc20_token, list_price); + +var finish = { status: "accept" }; + +(async () => { + while (true) { + const finish_req = await fetch(rollup_server + "/finish", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ status: "accept" }), + }); + + console.log("Received finish status " + finish_req.status); + + if (finish_req.status == 202) { + console.log("No pending rollup request, trying again"); + } else { + const rollup_req = await finish_req.json(); + var handler = handlers[rollup_req["request_type"]]; + finish["status"] = await handler(rollup_req["data"]); + } + } +})(); +``` diff --git a/cartesi-rollups_versioned_docs/version-1.5/tutorials/snippets/marketplace-py.md b/cartesi-rollups_versioned_docs/version-1.5/tutorials/snippets/marketplace-py.md new file mode 100644 index 00000000..cbf84ad9 --- /dev/null +++ b/cartesi-rollups_versioned_docs/version-1.5/tutorials/snippets/marketplace-py.md @@ -0,0 +1,379 @@ +```python +from os import environ +import logging +import requests +import binascii +import json +from eth_utils import function_signature_to_4byte_selector +from eth_abi import encode + +logging.basicConfig(level="INFO") +logger = logging.getLogger(__name__) + +rollup_server = environ["ROLLUP_HTTP_SERVER_URL"] +logger.info(f"HTTP rollup_server url is {rollup_server}") + + +def norm_addr(addr: str) -> str: + return addr.lower() + +def as_int(v) -> int: + return v if isinstance(v, int) else int(v) + +def string_to_hex(s: str) -> str: + return "0x" + binascii.hexlify(s.encode("utf-8")).decode() + +def hex_to_string(hexstr: str) -> str: + if not isinstance(hexstr, str): + return "" + if hexstr.startswith("0x"): + hexstr = hexstr[2:] + if hexstr == "": + return "" + try: + return binascii.unhexlify(hexstr).decode("utf-8") + except UnicodeDecodeError: + return "0x" + hexstr + +def token_deposit_parse(payload_hex: str): + hexstr = payload_hex[2:] if payload_hex.startswith("0x") else payload_hex + b = binascii.unhexlify(hexstr) + + if len(b) < 20 + 20 + 32: + logger.error(f"payload too short: {len(b)} bytes") + return + + token = b[0:20] + receiver = b[20:40] + amount_be = b[40:72] + + val = int.from_bytes(amount_be, byteorder="big", signed=False) + + token_hex = "0x" + token.hex() + receiver_hex = "0x" + receiver.hex() + return { + "token": token_hex, + "receiver": receiver_hex, + "amount": str(val), + } + +def extract_field(obj: dict, field: str) -> str: + v = obj.get(field, None) + if isinstance(v, str) and v.strip() != "": + return v + else: + logger.error(f"Missing or invalid {field} field in payload") + return + + +def emitReport(payload: str): + hex_payload = string_to_hex(payload) + try: + response = requests.post( + f"{rollup_server}/report", + json={"payload": hex_payload}, + headers={"Content-Type": "application/json"}, + timeout=5, + ) + logger.info(f"emit_report → status {response.status_code}") + except requests.RequestException as error: + logger.error("Error emitting report: %s", error) + + +def emitNotice(payload: str): + hex_payload = string_to_hex(payload) + try: + response = requests.post( + f"{rollup_server}/notice", + json={"payload": hex_payload}, + headers={"Content-Type": "application/json"}, + timeout=5, + ) + logger.info(f"notice → status {response.status_code}") + except requests.RequestException as error: + logger.error("Error emitting notice: %s", error) + +def structure_voucher(function_signature, destination, types, values, value=0) -> dict: + selector = function_signature_to_4byte_selector(function_signature) + encoded_args = encode(types, values) + payload = "0x" + (selector + encoded_args).hex() + + return { + "destination": destination, + "payload": payload, + "value": f"0x{value:064x}" + } + +def emitVoucher(voucher: dict): + try: + response = requests.post( + f"{rollup_server}/voucher", + json= voucher, + headers={"Content-Type": "application/json"}, + timeout=5, + ) + logger.info(f"emit_voucher → status {response.status_code}") + except requests.RequestException as error: + logger.error("Error emitting voucher: %s", error) + +class Storage: + def __init__(self, erc721_portal_address: str, erc20_portal_address: str, + erc721_token: str, erc20_token: str, list_price: int): + + self.erc721_portal_address = norm_addr(erc721_portal_address) + self.erc20_portal_address = norm_addr(erc20_portal_address) + self.erc721_token = norm_addr(erc721_token) + self.erc20_token = norm_addr(erc20_token) + self.application_address = norm_addr("0x" + "0" * 40) + self.list_price = list_price + + self.listed_tokens: list[int] = [] + self.users_erc20_token_balance: dict[str, int] = {} + self.user_erc721_token_balance: dict[str, list[int]] = {} + self.erc721_id_to_owner_address: dict[int, str] = {} + + def getListedTokens(self): + return self.listed_tokens + + def getAppAddress(self): + return self.application_address + + def setAppAddress(self, app_addr): + self.application_address = norm_addr(app_addr) + + def getERC721TokenOwner(self, tokenId: int): + return self.erc721_id_to_owner_address.get(as_int(tokenId)) + + def getUserERC20TokenBalance(self, userAddress: str) -> int: + addr = norm_addr(userAddress) + return self.users_erc20_token_balance.get(addr, 0) + + def increaseUserBalance(self, userAddress: str, amount): + addr = norm_addr(userAddress) + amt = as_int(amount) + current = self.users_erc20_token_balance.get(addr, 0) + self.users_erc20_token_balance[addr] = current + amt + + def reduceUserBalance(self, userAddress: str, amount): + addr = norm_addr(userAddress) + amt = as_int(amount) + current = self.users_erc20_token_balance.get(addr, None) + + if current is None: + emitReport(f"User {addr} record not found") + logger.error("User balance record not found") + return + + if current < amt: + emitReport(f"User {addr} has insufficient balance") + logger.error("User has insufficient balance") + return + + self.users_erc20_token_balance[addr] = current - amt + + def depositERC721Token(self, userAddress: str, tokenId): + addr = norm_addr(userAddress) + tid = as_int(tokenId) + zero_addr = "0x" + "0" * 40; + + previous_owner: str = self.getERC721TokenOwner(tid); + + if previous_owner and norm_addr(previous_owner) == norm_addr(zero_addr): + self.changeERC721TokenOwner(tid, addr, zero_addr); + else: + self.erc721_id_to_owner_address[tid] = addr + tokens = self.user_erc721_token_balance.get(addr, []) + if tid not in tokens: + tokens.append(tid) + self.user_erc721_token_balance[addr] = tokens + + def listTokenForSale(self, tokenId): + tid = as_int(tokenId) + if tid not in self.listed_tokens: + self.listed_tokens.append(tid) + + def changeERC721TokenOwner(self, tokenId, newOwner: str, oldOwner: str): + tid = as_int(tokenId) + new_addr = norm_addr(newOwner) + old_addr = norm_addr(oldOwner) + + self.erc721_id_to_owner_address[tid] = new_addr + + new_tokens = self.user_erc721_token_balance.get(new_addr, []) + if tid not in new_tokens: + new_tokens.append(tid) + self.user_erc721_token_balance[new_addr] = new_tokens + + old_tokens = self.user_erc721_token_balance.get(old_addr, []) + self.user_erc721_token_balance[old_addr] = [i for i in old_tokens if i != tid] + + def purchaseERC721Token(self, buyerAddress: str, erc721TokenAddress: str, tokenId) -> bool: + tid = as_int(tokenId) + + if not tokenId in self.listed_tokens: + emitReport(f"Token {erc721TokenAddress} with id {tid} is not for sale") + logger.error("Token is not for sale") + return False + + self.reduceUserBalance(buyerAddress, self.list_price) + + owner = self.getERC721TokenOwner(tid) + if not owner: + emitReport(f"Token owner for token {erc721TokenAddress} with id {tid} not found") + logger.error("Token owner not found") + return False + + zero_address = norm_addr("0x" + "0" * 40) + self.increaseUserBalance(owner, self.list_price) + self.listed_tokens = [i for i in self.listed_tokens if i != tid] + self.changeERC721TokenOwner(tid, zero_address, owner) + return True + + +erc_721_portal_address = "0xc700d52F5290e978e9CAe7D1E092935263b60051" +erc20_portal_address = "0xc700D6aDd016eECd59d989C028214Eaa0fCC0051" +erc20_token = "0xFBdB734EF6a23aD76863CbA6f10d0C5CBBD8342C" +erc721_token = "0xBa46623aD94AB45850c4ecbA9555D26328917c3B" +list_price = 100_000_000_000_000_000_000 + +storage = Storage(erc_721_portal_address, erc20_portal_address, erc721_token, erc20_token, list_price) + + +def handle_erc20_deposit(depositor_address: str, amount_deposited, token_address: str): + if norm_addr(token_address) == storage.erc20_token: + try: + storage.increaseUserBalance(depositor_address, as_int(amount_deposited)) + except Exception as e: + logger.error("Error handling ERC20 deposit: %s", e) + emitReport(str(e)) + else: + emitReport("Unsupported token deposited") + + +def handle_erc721_deposit(depositor_address: str, token_id, token_address: str): + if norm_addr(token_address) == storage.erc721_token: + try: + storage.depositERC721Token(depositor_address, as_int(token_id)) + storage.listTokenForSale(token_id) + logger.info("Token Listed Successfully") + emitNotice(f"Token ID: {token_id} Deposited by User: {depositor_address}") + except Exception as e: + logger.error("Error handling ERC721 deposit: %s", e) + emitReport(str(e)) + else: + logger.info("Unsupported token deposited (ERC721)") + emitReport("Unsupported token deposited") + +def handle_purchase_token(caller_address: str, payload_obj: dict): + try: + token_id_str = extract_field(payload_obj, "token_id") + token_id = as_int(token_id_str) + erc721_addr = storage.erc721_token + try: + if storage.purchaseERC721Token(caller_address, erc721_addr, token_id): + logger.info("Token purchased successfully, Processing Voucher....") + voucher = structure_voucher( + "transferFrom(address,address,uint256)", + storage.erc721_token, + ["address", "address", "uint256"], + [ storage.application_address, norm_addr(caller_address), token_id], + ) + emitVoucher(voucher) + else: + emitReport(f"Error purchasing token {token_id}") + return + except Exception as e: + emitReport(f"Failed to purchase token: {e}") + logger.error("Failed to purchase token: %s", e) + except Exception as e: + logger.error("Error purchasing token: %s", e) + emitReport(str(e)) + + +def handle_advance(data): + logger.info(f"Received advance request data {data}") + + sender = norm_addr(data["metadata"]["msg_sender"]) + app_contract = norm_addr(data["metadata"]["app_contract"]) + zero_addr = norm_addr("0x" + "0" * 40) + + if norm_addr(storage.application_address) == zero_addr: + storage.setAppAddress(app_contract) + + payload_hex = data.get("payload", "") + payload_str = hex_to_string(payload_hex) + + if sender == storage.erc20_portal_address: + parsed = token_deposit_parse(payload_hex) + token, receiver, amount = parsed["token"], parsed["receiver"], parsed["amount"] + handle_erc20_deposit(receiver, int(amount), token) + + elif sender == storage.erc721_portal_address: + parsed = token_deposit_parse(payload_hex) + token, receiver, amount = parsed["token"], parsed["receiver"], parsed["amount"] + handle_erc721_deposit(receiver, int(amount), token) + + else: + try: + payload_obj = json.loads(payload_str) + except Exception: + emitReport("Invalid payload JSON") + return "accept" + + method = payload_obj.get("method", "") + if method == "purchase_token": + handle_purchase_token(sender, payload_obj) + else: + logger.info("Unsupported method called") + emitReport("Unsupported method") + return "accept" + + +def handle_inspect(data: dict): + logger.info(f"Received inspect request data {data}") + + payload_str = hex_to_string(data.get("payload", "0x")) + try: + payload_obj = json.loads(payload_str) + except Exception: + emitReport("Invalid payload string") + return "accept" + + method = payload_obj.get("method", "") + if method == "get_user_erc20_balance": + user_address = norm_addr(payload_obj.get("user_address", "")) + bal = storage.getUserERC20TokenBalance(user_address) + emitReport(f"User: {user_address} Balance: {bal}") + elif method == "get_token_owner": + token_id = as_int(payload_obj.get("token_id", 0)) + owner = storage.getERC721TokenOwner(token_id) + emitReport(f"Token_id: {token_id} owner: {owner if owner else 'None'}") + elif method == "get_all_listed_tokens": + listed = storage.getListedTokens() + emitReport("All listed tokens are: " + ",".join(map(str, listed))) + else: + logger.info("Unsupported inspect method") + emitReport("Unsupported inspect method") + return "accept" + + +handlers = { + "advance_state": handle_advance, + "inspect_state": handle_inspect, +} + +finish = {"status": "accept"} + +while True: + logger.info("Sending finish") + response = requests.post(rollup_server + "/finish", json=finish) + logger.info(f"Received finish status {response.status_code}") + if response.status_code == 202: + logger.info("No pending rollup request, trying again") + else: + rollup_request = response.json() + data = rollup_request["data"] + handler = handlers[rollup_request["request_type"]] + finish["status"] = handler(rollup_request["data"]) + +``` \ No newline at end of file diff --git a/cartesi-rollups_versioned_docs/version-1.5/tutorials/snippets/marketplace-rs.md b/cartesi-rollups_versioned_docs/version-1.5/tutorials/snippets/marketplace-rs.md new file mode 100644 index 00000000..14911d7d --- /dev/null +++ b/cartesi-rollups_versioned_docs/version-1.5/tutorials/snippets/marketplace-rs.md @@ -0,0 +1,469 @@ +```rust +use json::{object, JsonValue}; +use std::env; +use ethers_core::abi::{encode, Token}; +use ethers_core::utils::id; +use hex; + +#[derive(Debug, Clone, Default)] +pub struct Storage { + pub erc721_portal_address: String, + pub erc20_portal_address: String, + pub erc721_token: String, + pub erc20_token: String, + pub app_address: String, + pub list_price: u128, + pub listed_tokens: Vec, + + pub users_erc20_token_balance: std::collections::HashMap, + pub user_erc721_token_balance: std::collections::HashMap>, + pub erc721_id_to_owner_address: std::collections::HashMap, +} + +impl Storage { + fn new ( + erc721_portal_address: String, + erc20_portal_address: String, + erc721_token: String, + erc20_token: String, + list_price: u128 + ) -> Self { + Storage{ + erc20_portal_address, + erc721_portal_address, + erc20_token, + erc721_token, + list_price, + app_address: "0x0000000000000000000000000000000000000000".to_string(), + listed_tokens: Vec::new(), + user_erc721_token_balance: std::collections::HashMap::new(), + users_erc20_token_balance: std::collections::HashMap::new(), + erc721_id_to_owner_address: std::collections::HashMap::new(), + } + } + + fn get_listed_tokens(&self) -> Vec<&u128> { + self.listed_tokens.iter().collect() + } + + fn get_erc721_token_owner(&self, token_id: u128) -> Option<&String> { + self.erc721_id_to_owner_address.get(&token_id) + } + + fn get_user_erc20_token_balance(&self, user_address: &str) -> Option<&u128> { + self.users_erc20_token_balance.get(user_address) + } + + fn increase_user_balance(&mut self, user_address: String, amount: u128) { + let balance = self.users_erc20_token_balance.entry(user_address).or_insert(0); + *balance += amount; + } + + fn deposit_erc721_token(&mut self, user_address: String, token_id: u128) { + let zero_address = "0x0000000000000000000000000000000000000000".to_string(); + if self.get_erc721_token_owner(token_id) == Some(&zero_address) { + self.change_erc721_token_owner(token_id, user_address, zero_address); + } else { + self.erc721_id_to_owner_address.insert(token_id, user_address.to_lowercase()); + self.user_erc721_token_balance + .entry(user_address) + .or_insert_with(Vec::new) + .push(token_id); + } + } + + fn list_token_for_sale(&mut self, token_id: u128) { + if !self.listed_tokens.contains(&token_id) { + self.listed_tokens.push(token_id); + } + } + + async fn reduce_user_balance(&mut self, user_address: &str, amount: u128) -> Result<(), String> { + if let Some(balance) = self.users_erc20_token_balance.get_mut(user_address){ + if *balance >= amount { + *balance -= amount; + Ok(()) + } else { + emit_report(format!("User {} has insufficient balance to purchase token", user_address)).await; + Err(String::from("User has insufficient balance")) + } + } else { + emit_report(format!("User {} Record, not found", user_address)).await; + Err(String::from("User balance record not found")) + } + } + + fn change_erc721_token_owner(&mut self, token_id: u128, new_owner: String, old_owner: String) { + if let Some(owner) = self.erc721_id_to_owner_address.get_mut(&token_id) { + *owner = new_owner.clone(); + } + + if let Some(tokens) = self.user_erc721_token_balance.get_mut(&new_owner) { + tokens.push(token_id); + } + + if let Some(tokens) = self.user_erc721_token_balance.get_mut(&old_owner) { + tokens.retain(|token| !(*token == token_id)); + } + } + + async fn purchase_erc721_token(&mut self, buyer_address: &str, token_id: u128) -> Result<(), String> { + self.reduce_user_balance( buyer_address, self.list_price).await?; + if let Some(owner) = self.get_erc721_token_owner( token_id) { + self.increase_user_balance(owner.clone(), self.list_price); + } else { + emit_report(format!("Token owner for token with id {} not found", token_id)).await; + return Err("Token owner not found".to_string()); + } + self.listed_tokens.retain(|token| (*token != token_id)); + let zero_address = "0x0000000000000000000000000000000000000000"; + self.change_erc721_token_owner( token_id, zero_address.to_string(), self.get_erc721_token_owner( token_id).unwrap().to_string()); + Ok(()) + } +} + +async fn handle_erc20_deposit(depositor_address: String, amount_deposited: u128, token_address: String, storage: &mut Storage) { + if token_address.to_lowercase() == storage.erc20_token.to_lowercase() { + storage.increase_user_balance(depositor_address, amount_deposited); + println!("Token deposit processed successfully"); + } else { + println!("Unsupported token deposited"); + emit_report("Unsupported token deposited".into()).await; + } +} + +async fn handle_erc721_deposit(depositor_address: String, token_id: u128, token_address: String, storage: &mut Storage) { + if token_address.to_lowercase() == storage.erc721_token.to_lowercase() { + storage.deposit_erc721_token(depositor_address.clone(), token_id); + storage.list_token_for_sale(token_id); + println!("Token deposit and listing processed successfully"); + emit_notice(format!("Token Id: {}, Deposited by User: {}", token_id, depositor_address)).await; + } else { + println!("Unsupported token deposited"); + emit_report("Unsupported token deposited".into()).await; + } +} + +async fn handle_purchase_token(sender: String, user_input: JsonValue, storage: &mut Storage) { + let token_id: u128 = user_input["token_id"].as_str().unwrap_or("0").parse().unwrap_or(0); + if storage.listed_tokens.contains(&token_id) != true { + emit_report("TOKEN NOT LISTED FOR SALE!!".to_string()).await; + return; + } + match storage.purchase_erc721_token(&sender, token_id).await { + Ok(_) => { + let args = vec![ + Token::Address(storage.app_address.parse().unwrap()), + Token::Address(sender.parse().unwrap()), + Token::Uint(token_id.into()), + ]; + let function_sig = "transferFrom(address,address,uint256)"; + let value: u128 = 0; + let selector = &id(function_sig)[..4]; + let encoded_args = encode(&args); + + let mut payload_bytes = Vec::new(); + payload_bytes.extend_from_slice(selector); + payload_bytes.extend_from_slice(&encoded_args); + + let payload = format!("0x{}", hex::encode(payload_bytes)); + + let voucher = object! { + "destination" => format!("{}", storage.erc721_token), + "payload" => format!("{}", payload), + "value" => format!("0x{}", hex::encode(value.to_be_bytes())), + }; + + emit_voucher(voucher).await; + println!("Token purchased and Withdrawn successfully"); + + }, + Err(e) => {emit_report("Failed to purchase token".into()).await; println!("Failed to purchase token: {}", e);} + } +} + +pub async fn handle_advance( + _client: &hyper::Client, + _server_addr: &str, + request: JsonValue, + storage: &mut Storage +) -> Result<&'static str, Box> { + println!("Received advance request data {}", &request); + let _payload = request["data"]["payload"] + .as_str() + .ok_or("Missing payload")?; + let zero_address = "0x0000000000000000000000000000000000000000".to_string(); + let app_addr = request["data"]["metadata"]["app_contract"] + .as_str() + .ok_or("Missing payload")?; + + if storage.app_address == zero_address { + storage.app_address = app_addr.to_string(); + } + let sender = request["data"]["metadata"]["msg_sender"] + .as_str() + .ok_or("Missing msg_sender in metadata")? + .to_string(); + + match sender.as_str() { + s if s.to_lowercase() == storage.erc20_portal_address.as_str().to_lowercase() => { + let (deposited_token, receiver_address, amount) = token_deposit_parse(&_payload)?; + println!("Deposited token: {}, Receiver: {}, Amount: {}", deposited_token, receiver_address, amount); + handle_erc20_deposit(receiver_address.to_lowercase(), amount, deposited_token.to_lowercase(), storage).await; + }, + s if s.to_lowercase() == storage.erc721_portal_address.as_str().to_lowercase() => { + let (deposited_token, receiver_address, token_id) = token_deposit_parse(&_payload)?; + println!("Deposited and listed token: {}, Receiver: {}, Token ID: {}", deposited_token, receiver_address, token_id); + handle_erc721_deposit(receiver_address.to_lowercase(), token_id, deposited_token.to_lowercase(), storage).await; + }, + _ => { + let payload_str = hex_to_string(_payload)?; + let payload_json ; + match json::parse(&payload_str) { + Err(_) => { + let fixed_payload = try_fix_json_like(&payload_str); + match json::parse(&fixed_payload) { + Err(_) => panic!("Failed to parse decoded payload as JSON even after attempting to fix it"), + Ok(val) => payload_json = val, + }; + } + Ok(val) => payload_json = val + }; + if !payload_json.is_object() { + emit_report("Decoded payload is not a JSON object".into()).await; + println!("Decoded payload is not a JSON object"); + } + if let Some(method_str) = payload_json["method"].as_str() { + match method_str { + "purchase_token" => {handle_purchase_token(sender.to_lowercase(), payload_json.clone(), storage).await; }, + _ => {emit_report("Unknown method in payload".into()).await; println!("Unknown method in payload")}, + } + } else { + emit_report("Missing or invalid method field in payload".into()).await; + println!("Missing or invalid method field in payload"); + } + } + } + Ok("accept") +} + +pub async fn handle_inspect( + _client: &hyper::Client, + _server_addr: &str, + request: JsonValue, + storage: &mut Storage +) -> Result<&'static str, Box> { + println!("Received inspect request data {}", &request); + let zero_address = "0x0000000000000000000000000000000000000000".to_string(); + let _payload = request["data"]["payload"] + .as_str() + .ok_or("Missing payload")?; + let payload_str = hex_to_string(_payload)?; + println!("Decoded payload: {}", payload_str); + + let payload_json ; + + match json::parse(&payload_str) { + Err(_) => { + let fixed_payload = try_fix_json_like(&payload_str); + match json::parse(&fixed_payload) { + Err(_) => panic!("Failed to parse decoded payload as JSON even after attempting to fix it"), + Ok(val) => payload_json = val, + }; + } + Ok(val) => payload_json = val + }; + + if let Some(method_str) = payload_json["method"].as_str() { + match method_str { + "get_user_erc20_balance" => { + let user_address = payload_json["user_address"].as_str().unwrap_or(&zero_address).to_lowercase(); + let user_balance = storage.get_user_erc20_token_balance(user_address.as_str()).unwrap_or(&0); + emit_report(format!("User: {}, balance: {}", user_address, user_balance)).await; + }, + "get_token_owner" => { + let token_id: u128 = payload_json["token_id"].as_u64().unwrap_or(0).into(); + let token_owner = storage.get_erc721_token_owner(token_id).unwrap_or(&zero_address); + emit_report(format!("Token id: {}, owner: {}", token_id, token_owner)).await; + }, + "get_all_listed_tokens" => { + let listed_tokens = storage.get_listed_tokens(); + emit_report(format!("All listed tokens are: {:?}", listed_tokens)).await; + }, + _ => {emit_report("Unknown method in payload".into()).await; println!("Unknown method in payload")}, + } + } else { + emit_report("Missing or invalid method field in payload".into()).await; + println!("Missing or invalid method field in payload"); + } + Ok("accept") +} + +pub fn token_deposit_parse(payload: &str) -> Result<(String, String, u128), String> { + let hexstr = payload.strip_prefix("0x").unwrap_or(payload); + let bytes = hex::decode(hexstr).map_err(|e| format!("hex decode error: {}", e))?; + if bytes.len() < 20 + 20 + 32 { + return Err(format!("payload too short: {} bytes", bytes.len())); + } + let token = &bytes[0..20]; + let receiver = &bytes[20..40]; + let amount_be = &bytes[40..72]; + + if amount_be[..16].iter().any(|&b| b != 0) { + return Err("amount too large for u128".into()); + } + let mut lo = [0u8; 16]; + lo.copy_from_slice(&amount_be[16..]); + let amount = u128::from_be_bytes(lo); + + Ok(( + format!("0x{}", hex::encode(token)), + format!("0x{}", hex::encode(receiver)), + amount, + )) +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = hyper::Client::new(); + let server_addr = env::var("ROLLUP_HTTP_SERVER_URL")?; + + let erc721_portal_address = String::from("0xc700d52F5290e978e9CAe7D1E092935263b60051"); + let erc20_portal_address = String::from("0xc700D6aDd016eECd59d989C028214Eaa0fCC0051"); + let erc20_token = String::from("0xFBdB734EF6a23aD76863CbA6f10d0C5CBBD8342C"); + let erc721_token = String::from("0xBa46623aD94AB45850c4ecbA9555D26328917c3B"); + let list_price: u128 = 100_000_000_000_000_000_000; + let mut storage = Storage::new(erc721_portal_address, erc20_portal_address, erc721_token, erc20_token, list_price); + + let mut status = "accept"; + loop { + println!("Sending finish"); + let response = object! {"status" => status}; + let request = hyper::Request::builder() + .method(hyper::Method::POST) + .header(hyper::header::CONTENT_TYPE, "application/json") + .uri(format!("{}/finish", &server_addr)) + .body(hyper::Body::from(response.dump()))?; + let response = client.request(request).await?; + println!("Received finish status {}", response.status()); + + if response.status() == hyper::StatusCode::ACCEPTED { + println!("No pending rollup request, trying again"); + } else { + let body = hyper::body::to_bytes(response).await?; + let utf = std::str::from_utf8(&body)?; + let req = json::parse(utf)?; + + let request_type = req["request_type"] + .as_str() + .ok_or("request_type is not a string")?; + status = match request_type { + "advance_state" => handle_advance(&client, &server_addr[..], req, &mut storage).await?, + "inspect_state" => handle_inspect(&client, &server_addr[..], req, &mut storage).await?, + &_ => { + eprintln!("Unknown request type"); + "reject" + } + }; + } + } +} + +fn hex_to_string(hex: &str) -> Result> { + let hexstr = hex.strip_prefix("0x").unwrap_or(hex); + let bytes = hex::decode(hexstr).map_err(|e| Box::new(e) as Box)?; + let s = String::from_utf8(bytes).map_err(|e| Box::new(e) as Box)?; + Ok(s) +} + +fn try_fix_json_like(s: &str) -> String { + let mut fixed = s.to_string(); + + // Add quotes around keys (rudimentary repair) + fixed = fixed.replace("{", "{\""); + fixed = fixed.replace(": ", "\":\""); + fixed = fixed.replace(", ", "\", \""); + fixed = fixed.replace("}", "\"}"); + + fixed +} + +async fn emit_report( payload: String) -> Option { + // convert the provided string payload to hex (strip optional "0x" prefix if present) + let hex_string = { + let s = payload.strip_prefix("0x").unwrap_or(payload.as_str()); + hex::encode(s.as_bytes()) + }; + + let server_addr = env::var("ROLLUP_HTTP_SERVER_URL").expect("ROLLUP_HTTP_SERVER_URL not set"); + let client = hyper::Client::new(); + + let response = object! { + "payload" => format!("0x{}", hex_string), + }; + let request = hyper::Request::builder() + .method(hyper::Method::POST) + .header(hyper::header::CONTENT_TYPE, "application/json") + .uri(format!("{}/report", server_addr)) + .body(hyper::Body::from(response.dump())) + .ok()?; + + let response = client.request(request).await; + match response { + Ok(_) => { + println!("Report generation successful"); + return Some(true); + } + Err(e) => { + println!("Report request failed {}", e); + None + } + } +} + +async fn emit_voucher( voucher: JsonValue) -> Option { + let server_addr = env::var("ROLLUP_HTTP_SERVER_URL").expect("ROLLUP_HTTP_SERVER_URL not set"); + let client = hyper::Client::new(); + + let request = hyper::Request::builder() + .method(hyper::Method::POST) + .header(hyper::header::CONTENT_TYPE, "application/json") + .uri(format!("{}/voucher", server_addr)) + .body(hyper::Body::from(voucher.dump())) + .ok()?; + + let response = client.request(request).await; + + match response { + Ok(_) => { + println!("Voucher generation successful"); + return Some(true); + } + Err(e) => { + println!("Voucher request failed {}", e); + None + } + } +} + +async fn emit_notice( payload: String) { + let hex_string = { + let s = payload.strip_prefix("0x").unwrap_or(payload.as_str()); + hex::encode(s.as_bytes()) + }; + + let server_addr = env::var("ROLLUP_HTTP_SERVER_URL").expect("ROLLUP_HTTP_SERVER_URL not set"); + let client = hyper::Client::new(); + + let response = object! { + "payload" => format!("0x{}", hex_string), + }; + let request = hyper::Request::builder() + .method(hyper::Method::POST) + .header(hyper::header::CONTENT_TYPE, "application/json") + .uri(format!("{}/notice", server_addr)) + .body(hyper::Body::from(response.dump())) + .ok(); + let _response = client.request(request.unwrap()).await; +} +``` diff --git a/cartesi-rollups_versioned_docs/version-2.0/tutorials/counter.md b/cartesi-rollups_versioned_docs/version-2.0/tutorials/counter.md new file mode 100644 index 00000000..803616a0 --- /dev/null +++ b/cartesi-rollups_versioned_docs/version-2.0/tutorials/counter.md @@ -0,0 +1,245 @@ +--- +id: counter +title: Build a counter Application +resources: + - url: https://github.com/Mugen-Builders/Counter-X-Marketplace-apps + title: Source code for the counter Application +--- + +This tutorial aims to guide you through creating and interacting with a basic Cartesi application, it'll take you through setting up your dev environment, creating a project then finally running and interacting with your application locally. + +We would also be providing the Rust, JavaScript, Python and Go implementation of the application, so you could choose whichever language you're more conversant with. + +## Set up your environment + +To build an application using Cartesi, it's necessary that you have the following tools installed: + +- Cartesi CLI: A simple tool for building applications on Cartesi. [Install Cartesi CLI for your OS of choice](../development/installation.md). + +- Docker Desktop 4.x: The tool you need to run the Cartesi Machine and its dependencies. [Install Docker for your OS of choice](https://www.docker.com/products/docker-desktop/). + +## Create an application template using the Cartesi CLI + +Creating an application template for your application is a generally simple process, to do this, we utilize the Cartesi CLI by running the below command: + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + + +

+
+```shell
+cartesi create counter --template javascript
+```
+
+
+
+ + +

+
+```shell
+cartesi create counter --template python
+```
+
+
+
+ + +

+
+```shell
+cartesi create counter --template rust
+```
+
+
+
+
+ +This command creates a directory called `counter` and depending on your selected language this directory would contain the necessary entry point file to start your application, for Python developers this would be `dapp.py` while for Rust users it would be `src/main.rs`, then finally for JavaScript users, the entry point file would be `src/index.js`. + +This entry point file contains the default template for interacting with the Cartesi Rollups HTTP Server, it also makes available, two function namely `handle_advance()` and `handle_inspect()` which process "advance / write" and "inspect / read" requests to the application. In the next section we would be updating these functions with the implementation for your application. + +## Implement the Application Logic + +We’ll build the counter with a simple object‑oriented design. It defines a Counter object with three methods: a constructor, `increment()` to increase the count, and `get()` to return the current count. + +We also update `handle_advance()` to increment the counter whenever an advance (write) request arrives, ignoring the request payload. And we update `handle_inspect()` to log the current counter value when an inspect (read) request arrives. + +Together, these handlers let you increment the counter and check its value. + +To try it locally, copy the snippet for your language and replace the contents of the entry point file in your `counter/` directory. + +import CounterJS from './snippets/counter-js.md'; +import CounterPY from './snippets/counter-py.md'; +import CounterRS from './snippets/counter-rs.md'; + + + +

+
+
+
+
+
+ + +

+
+
+
+
+
+ + +

+
+
+
+
+
+
+ +## Build and Run your Application + +Once you have your application logic written out, the next step is to build the application, this is done by running the below commands using the Cartesi CLI: + +```shell +cartesi build +``` + +- Expected Logs: + +```shell +user@user-MacBook-Pro counter % cartesi build +(node:4460) ExperimentalWarning: Importing JSON modules is an experimental feature and might change at any time +(Use `node --trace-warnings ...` to show where the warning was created) +✔ Build drives + ✔ Build drive root + + . + / \ + / \ +\---/---\ /----\ + \ X \ + \----/ \---/---\ + \ / CARTESI + \ / MACHINE + ' + +[INFO rollup_http_server] starting http dispatcher service... +[INFO rollup_http_server::http_service] starting http dispatcher http service! +[INFO actix_server::builder] starting 1 workers +[INFO actix_server::server] Actix runtime found; starting in Actix runtime +[INFO actix_server::server] starting service: "actix-web-service-127.0.0.1:5004", workers: 1, listening on: 127.0.0.1:5004 +[INFO rollup_http_server::dapp_process] starting dapp: python3 dapp.py +INFO:__main__:HTTP rollup_server url is http://127.0.0.1:5004 +INFO:__main__:Sending finish + +Manual yield rx-accepted (1) (0x000020 data) +Cycles: 8108719633 +8108719633: 107174e04a294787e22b6864c61fedd845833e5c8bc9a244480f2996ddabb3c7 +Storing machine: please wait +``` + +The build command compiles your application then builds a Cartesi machine that contains your application. + +This recently built machine alongside other necessary service, like an Anvil network, inspect service, etc. wound next be started by running the command: + +```bash +cartesi run +``` + +If the `run` command is successful, you should see logs similar to this: + +```bash +user@user-MacBook-Pro counter % cartesi run +(node:5404) ExperimentalWarning: Importing JSON modules is an experimental feature and might change at any time +(Use `node --trace-warnings ...` to show where the warning was created) +WARNING: default block is set to 'latest', production configuration will likely use 'finalized' +[+] Pulling 4/0 + ✔ database Skipped - Image is already present locally + ✔ rollups-node Skipped - Image is already present locally + ✔ anvil Skipped - Image is already present locally + ✔ proxy Skipped - Image is already present locally +✔ counter starting at http://127.0.0.1:6751 +✔ anvil service ready at http://127.0.0.1:6751/anvil +✔ rpc service ready at http://127.0.0.1:6751/rpc +✔ inspect service ready at http://127.0.0.1:6751/inspect/counter +✔ counter machine hash is 0x107174e04a294787e22b6864c61fedd845833e5c8bc9a244480f2996ddabb3c7 +✔ counter contract deployed at 0x94b32605a405d690934eb4ecc91856febfa747cc +(l) View logs (b) Build and redeploy (q) Quit +``` + +## Interacting with your Counter Application + +Interacting with your Counter application could be achieved either through initiating transactions on the local anvil network which was activated when you ran the `cartesi run` command or more easily through the Cartesi CLI, for this tutorial, we'll be using the Cartesi CLI to send an input to our application which would increase our counter variable. + +### 1. Query current count value + +We start by querying the current count value, this is done by making an inspect request to the counter application running locally, to achieve this we run the below command in a new terminal: + +```bash +curl -X POST http://127.0.0.1:6751/inspect/counter \ + -H "Content-Type: application/json" \ + -d '{""}' +``` + +:::note Inspect endpoint +Please note that if your application is running on a different port or your application is not named `counter` as in the guide, then you'll need to replace the inspect endpoint `http://127.0.0.1:6751/inspect/counter` with the endpoint provided after running the `cartesi run` command. +::: + +On success, we receive a confirmation response from the HTTP server, something similar to `{"status":"Accepted","reports":null,"processed_input_count":0}`, then on the terminal running our application when we press the `l` key to access our application logs we should get a log confirming that our application received the inspect call and should also contain a log of the current count value. + +```bash +[INFO rollup_http_server::http_service] received new request of type INSPECT +inspect_request.payload_length: 4 +[INFO actix_web::middleware::logger] 127.0.0.1 "POST /finish HTTP/1.1" 200 64 "-" "python-requests/2.31.0" 0.020096 +INFO:__main__:Received finish status 200 +INFO:__main__:Received inspect request data {'payload': '0x7b22227d'} +INFO:__main__:Current counter value: 0 +INFO:__main__:Sending finish +2025-11-09T17:47:44.661 INF Request executed service=inspect status=Accepted application=counter +``` + +As seen in the third to last line of our received log, we can see the `count value` returned to be `0` + +### 2. Increase count value + +Now that we've confirmed our count value to be zero (0), we would be sending an advance request using the CLI to increase the value of our counter by running the below command: + +```bash +cartesi send random_text +``` + +The above command sends an advance request with the payload "random_text" to our application, which ignores this payload then proceeds to increase out count value by `one (1)`, if this command is successful and our application process this request graciously, we should get a log similar to what's presented below on the terminal running our application: + +```bash +INFO:__main__:Received advance request data {'metadata': {'chain_id': 13370, 'app_contract': '0x9d40cfc42bb386b531c5d4eb3ade360f4105c4a3', 'msg_sender': '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', 'block_number': 630, 'block_timestamp': 1762711409, 'prev_randao': '0x63a2bb3993d9f9c371624f995a10a6f493e33c2535e62b32fee565f812b4c4ab', 'input_index': 0}, 'payload': '0x72616e646f6d5f74657874'} +INFO:__main__:Counter increment requested, new count value: 1 +INFO:__main__:Sending finish +``` + +The above logs prove that out application received the advance request, increased our count value, logs the updated count value then finishes that request successfully. + +As seen in the second to last line of the log, our count value has been increased from 0 to 1. To confirm this increase, we can run an inspect request once more to verify the current count value, and on running the same inspect command as last time, we obtain the updated logs below. + +```shell +[INFO rollup_http_server::http_service] received new request of type INSPECT +inspect_request.payload_length: 4 +[INFO actix_web::middleware::logger] 127.0.0.1 "POST /finish HTTP/1.1" 200 64 "-" "python-requests/2.31.0" 0.002048 +INFO:__main__:Received finish status 200 +INFO:__main__:Received inspect request data {'payload': '0x7b22227d'} +INFO:__main__:Current counter value: 1 +INFO:__main__:Sending finish +2025-11-09T18:22:45.142 INF Request executed service=inspect status=Accepted application=counter +``` + +From the latest application logs, it's now clear that the application's count value has been increased from 0 to one, and subsequent advance calls would further increase the count value. + +## Conclusion + +Congratulations, you've successfully bootstrapped, implemented, ran and interacted with your first Cartesi Application. + +For a more detailed version of this code, you can check the `counter` folder for your selected language in [this repository](https://github.com/Mugen-Builders/Counter-X-Marketplace-apps) diff --git a/cartesi-rollups_versioned_docs/version-2.0/tutorials/marketplace.md b/cartesi-rollups_versioned_docs/version-2.0/tutorials/marketplace.md new file mode 100644 index 00000000..f6b01be0 --- /dev/null +++ b/cartesi-rollups_versioned_docs/version-2.0/tutorials/marketplace.md @@ -0,0 +1,424 @@ +--- +id: marketplace +title: Build a marketplace Application +resources: + - url: https://github.com/Mugen-Builders/Counter-X-Marketplace-apps + title: Source code for the marketplace Application +--- + +In this tutorial we'll be building a simple NFT Marketplace application, where users are able to deposit a unique token to be sold at a fixed price, then other users are able to purchase and withdraw these purchased tokens to their wallet. + +This Tutorial is built using an Object oriented approach and aims to cover, application creation, Notice, Voucher and Report generation, we'll also be decoding and consuming the payload passed alongside advance and inspect requests. + +## How the Marketplace Works + +Now that the basic setup is done, we can focus on how the marketplace application actually works. +This tutorial uses an Object-Oriented approach. This means we will create a main Marketplace object that stores the application state and provides methods to update or retrieve that state. Then we'll build other handler functions that utilizes other helper functions to decode user requests then call the appropriate method to update the marketplace object. + +For simplicity, our marketplace will support only: + +- One specific ERC-721 (NFT) contract: This is the only collection users can list. + +- One specific ERC-20 token: This is the token users will use to purchase NFTs. + +### Advance Requests + +**1. Depositing / Listing NFTs** + +- A user sends an NFT to the Cartesi application through the ERC-721 Portal. +- When the application receives the deposit payload, it automatically lists the NFT for sale at a fixed price. +- The price is the same for every NFT in this tutorial to keep things simple. + +**2. Depositing Payment Tokens** + +- Users can then send the marketplace’s payment token to the application through the ERC-20 Portal. +- The application keeps track of how many tokens each user has deposited. + +**3. Buying an NFT** + +- When a user decides to buy an NFT listed on the marketplace, the application checks: + - Does the user have enough deposited tokens? + - Is the NFT still listed? + +- If the transaction is valid, the marketplace transfers the payment token to the seller then creates a voucher that sends the purchased NFT to the buyer. + +### Inspect Requests + +The Inspect route will support three simple read-only queries: + +**1. `get_user_erc20_balance`** + +**Description:** Shows how many tokens a user has stored in the marketplace. + +**Input:** User's address + +**Output:** Amount of ERC-20 tokens deposited. + +**2. `get_token_owner`** + +**Description:** Returns the current owner of a specific NFT. + +**Input:** Token ID + +**Output:** Address of current token owner. + +**3. `get_all_listed_tokens`** + +**Description:** Shows all NFTs currently listed for sale. + +**Input:** (none) + +**Output:** Array of Token IDs currently listed for sale. + +## Set up your environment + +To create a template for your project, we run the below command, based on your language of choice: + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + + +

+
+```shell
+cartesi create marketplace --template javascript
+```
+
+
+
+ + +

+
+```shell
+cartesi create marketplace --template python
+```
+
+
+
+ + +

+
+```shell
+cartesi create marketplace --template rust
+```
+
+
+
+
+ +## Install Project Dependencies + +Since this project would be covering hex payload encoding and decoding, as well as Output (Voucher, Notice, Report) generation, it's important that we install the necessary dependencies to aid these processes. + + + +

+
+```shell
+ npm add viem
+```
+
+
+
+ + +

+
+```shell
+cat > requirements.txt << 'EOF'
+--find-links https://prototyp3-dev.github.io/pip-wheels-riscv/wheels/
+requests==2.32.5
+eth-abi==5.2.0
+eth-utils==5.3.1
+regex==2025.11.3
+pycryptodome==3.23.0
+eth-hash==0.7.1
+EOF
+```
+
+
+
+ + +

+
+```shell
+cargo add hex serde ethers-core 
+```
+
+
+
+
+ +## Implement the Application Logic + +Based on the programming language you selected earlier, copy the appropriate code snippet, then paste in your local entry point file (`dapp.py` or `src/main.rs` or `src/index.js`), created in the setup step: + +import MarketplaceJS from './snippets/marketplace-js.md'; +import MarketplacePY from './snippets/marketplace-py.md'; +import MarketplaceRS from './snippets/marketplace-rs.md'; + + + +

+
+
+
+
+
+ + +

+
+
+
+
+
+ + +

+
+
+
+
+
+
+ +## Build and Run your Application + +Once you have your application logic written out, the next step is to build the application, this is done by running the below commands using the Cartesi CLI: + +```shell +cartesi build +``` + +- Expected Logs: + +```shell +user@user-MacBook-Pro marketplace % cartesi build +(node:4460) ExperimentalWarning: Importing JSON modules is an experimental feature and might change at any time +(Use `node --trace-warnings ...` to show where the warning was created) +✔ Build drives + ✔ Build drive root + + . + / \ + / \ +\---/---\ /----\ + \ X \ + \----/ \---/---\ + \ / CARTESI + \ / MACHINE + ' + +[INFO rollup_http_server] starting http dispatcher service... +[INFO rollup_http_server::http_service] starting http dispatcher http service! +[INFO actix_server::builder] starting 1 workers +[INFO actix_server::server] Actix runtime found; starting in Actix runtime +[INFO actix_server::server] starting service: "actix-web-service-127.0.0.1:5004", workers: 1, listening on: 127.0.0.1:5004 +[INFO rollup_http_server::dapp_process] starting dapp: python3 dapp.py +INFO:__main__:HTTP rollup_server url is http://127.0.0.1:5004 +INFO:__main__:Sending finish + +Manual yield rx-accepted (1) (0x000020 data) +Cycles: 8108719633 +8108719633: 107174e04a294787e22b6864c61fedd845833e5c8bc9a244480f2996ddabb3c7 +Storing machine: please wait +``` + +The build command compiles your application then builds a Cartesi machine that contains your application. + +This recently built machine alongside other necessary service, like an Anvil network, inspect service, etc. wound next be started by running the command: + +```bash +cartesi run +``` + +If the `run` command is successful, you should see logs similar to this: + +```bash +user@user-MacBook-Pro marketplace % cartesi run +(node:5404) ExperimentalWarning: Importing JSON modules is an experimental feature and might change at any time +(Use `node --trace-warnings ...` to show where the warning was created) +WARNING: default block is set to 'latest', production configuration will likely use 'finalized' +[+] Pulling 4/0 + ✔ database Skipped - Image is already present locally + ✔ rollups-node Skipped - Image is already present locally + ✔ anvil Skipped - Image is already present locally + ✔ proxy Skipped - Image is already present locally +✔ marketplace starting at http://127.0.0.1:6751 +✔ anvil service ready at http://127.0.0.1:6751/anvil +✔ rpc service ready at http://127.0.0.1:6751/rpc +✔ inspect service ready at http://127.0.0.1:6751/inspect/marketplace +✔ marketplace machine hash is 0x107174e04a294787e22b6864c61fedd845833e5c8bc9a244480f2996ddabb3c7 +✔ marketplace contract deployed at 0x94b32605a405d690934eb4ecc91856febfa747cc +(l) View logs (b) Build and redeploy (q) Quit +``` + +## Interacting with your Marketplace Application + +Once your Marketplace application is up and running (via cartesi run), you can interact with it in two main ways — either by sending on-chain transactions through the local Anvil network (Advance requests) or by making HTTP requests directly to the Rollups HTTP server’s Inspect endpoint (Inspect requests). + +In this section, we’ll focus on using the Cartesi CLI to send Advance requests, since it provides a much simpler and faster way to test your application locally. + +:::note Inspect endpoint +If your application is running on a different port or has a different name from marketplace, remember to replace the inspect endpoint http://127.0.0.1:6751/inspect/marketplace with the one displayed after running the cartesi run command. +Also, ensure that all CLI commands are executed from the root directory of your application. +::: + +### 1. Mint an ERC-721 Token and Grant Approval + +With your Marketplace application now deployed, the first step is to mint the NFT you plan to list and grant approval for it to be transferred via the ERC-721 portal. +Since our app uses the test ERC-721 and ERC-20 contracts automatically deployed by the CLI, you can use the commands below to mint your token and set the necessary approvals. + +- Mint token ID 1: + +```bash +cast send 0xBa46623aD94AB45850c4ecbA9555D26328917c3B \ + "safeMint(address, uint256, string)" \ + 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 1 "" \ + --rpc-url http://127.0.0.1:6751/anvil \ + --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 +``` + +This command calls the `safeMint` function in the `testNFT` contract deployed by the CLI, minting token `ID 1` to the address `0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266`. + +- Grant approval to the ERC721-Portal: + +Before an NFT can be deposited into the application, the portal contract must have permission to transfer it on behalf of the owner. Use the following command to grant that approval: + +```bash +cast send 0xBa46623aD94AB45850c4ecbA9555D26328917c3B \ + "setApprovalForAll(address,bool)" \ + 0xc700d52F5290e978e9CAe7D1E092935263b60051 true \ + --rpc-url http://127.0.0.1:6751/anvil \ + --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 +``` + +### 2. Deposit NFT with ID 1 to the Marketplace [advance request] + +Now that the NFT is minted and approved, it’s time to list it on the marketplace. +We’ll do this by depositing it using the Cartesi CLI: + +```bash +cartesi deposit erc721 +``` + +The CLI will prompt you for the token ID (enter 1) and the token address (press Enter to use the default test token). +Under the hood, the CLI transfers the NFT from your address to the ERC-721 portal, which then sends the deposit payload to your application. + +Once the deposit succeeds, the terminal running your application should show logs similar to: + +```bash +INFO rollup_http_server::http_service] received new request of type ADVANCE +[INFO actix_web::middleware::logger] 127.0.0.1 "POST /finish HTTP/1.1" 200 751 "-" "-" 0.018688 +Received finish status 200 OK +Received advance request data {"request_type":"advance_state","data":{"metadata":{"chain_id":13370,"app_contract":"0xb86306660e0be8e228c0cd14b8a1c5d5eddb8d20","msg_sender":"0xc700d52f5290e978e9cae7d1e092935263b60051","block_number":675,"block_timestamp":1762948794,"prev_randao":"0x3d144a3d5bb3125c92a230e7597e04e82eb5d5acbea185db2b1eadda3530d5c7","input_index":0},"payload":"0xba46623ad94ab45850c4ecba9555d26328917c3bf39fd6e51aad88f6f4ce6ab8827279cfffb9226600000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"}} +Token deposit and listing processed successfully +[INFO actix_web::middleware::logger] 127.0.0.1 "POST /notice HTTP/1.1" 201 11 "-" "-" 0.001088 +Sending finish +``` + +### 3. View All NFTs Listed on the Marketplace [inspect request] + +After depositing, your NFT is automatically listed. +To verify this, you can query the marketplace for all listed tokens using an Inspect request: + +```bash +curl -X POST http://127.0.0.1:6751/inspect/marketplace \ + -H "Content-Type: application/json" \ + -d '{"method":"get_all_listed_tokens"}' +``` + +This call returns a hex payload containing a list of all listed tokens on the marketplace. + +```bash +{"status":"Accepted","reports":[{"payload":"0x416c6c206c697374656420746f6b656e73206172653a205b315d"}],"processed_input_count":6} +``` + +The payload hex `0x416c6...205b315d` when decoded, returns `All listed tokens are: [1]`. Thereby confirming that the token with `Id 1` has successfully been listed. + +### 4. Deposit ERC20 token for making purchases [advance request] + +With the NFT successfully listed for sale, it's time to attempt to purchase this token, but before we do that, we'll need first deposit the required amount of tokens to purchase the listed NFT. Since our marketplace lists all NFT's at the price of `100 testTokens` we'll be transferring 100 tokens to the new address we'll be using to purchase, before proceeding with the purchase. + +- Transfer required tokens to purchase address. + +```bash +cast send 0xFBdB734EF6a23aD76863CbA6f10d0C5CBBD8342C \ +"transfer(address,uint256)" 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 100000000000000000000 \ +--private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \ +--rpc-url http://127.0.0.1:6751/anvil +``` + +- Deposit 100 tokens to the marketplace application. + +```bash +cartesi deposit --from 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 +``` + +The CLI will prompt you for the token address (press Enter to use the default test token), and then the amount of tokens we intend to deposit `(100)`. The CLI would finally proceed to grant the necessary approvals after which it would deposit the tokens to our application. + +On a successful deposit our application should return logs that look like this: + +```bash +[INFO rollup_http_server::http_service] received new request of type ADVANCE +[INFO actix_web::middleware::logger] 127.0.0.1 "POST /finish HTTP/1.1" 200 496 "-" "-" 0.018176 +Received finish status 200 OK +Received advance request data {"request_type":"advance_state","data":{"metadata":{"chain_id":13370,"app_contract":"0xb86306660e0be8e228c0cd14b8a1c5d5eddb8d20","msg_sender":"0xc700d6add016eecd59d989c028214eaa0fcc0051","block_number":2272,"block_timestamp":1762951994,"prev_randao":"0x0992ab8380b23c1c98928a76ae9a79c501ae27625943a53b0fd57455f10e5164","input_index":1},"payload":"0xfbdb734ef6a23ad76863cba6f10d0c5cbbd8342c70997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000000000000000000000000000056bc75e2d63100000"}} +Deposited token: 0xfbdb734ef6a23ad76863cba6f10d0c5cbbd8342c, Receiver: 0x70997970c51812dc3a010c7d01b50e0d17dc79c8, Amount: 100000000000000000000 +Token deposit processed successfully +Sending finish +``` + +### 5. Purchase Token with ID 1 [advance request] + +Now that the buyer has deposited funds, we can proceed to purchase the NFT. To do this we make an advance request to the application using the Cartesi CLI by running the command: + +```bash + cartesi send "{"method": "purchase_token", "token_id": 1}" --from 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 +``` + +This command notifies the marketplace that the address `0x7099797....0d17dc79C8` which initially deposited 100 tokens, would want to purchase token ID 1, The marketplace proceeds to run necessary checks like verifying that the token is for sale, and that the buyer has sufficient tokens to make the purchase, after which it executes the purchase and finally emits a voucher that transfers the tokens to the buyer's address. On a successful purchase, you should get logs similar to the below. + +```bash +[INFO rollup_http_server::http_service] received new request of type ADVANCE +[INFO actix_web::middleware::logger] 127.0.0.1 "POST /finish HTTP/1.1" 200 426 "-" "-" 0.001792 +Received finish status 200 OK +Received advance request data {"request_type":"advance_state","data":{"metadata":{"chain_id":13370,"app_contract":"0xb86306660e0be8e228c0cd14b8a1c5d5eddb8d20","msg_sender":"0x70997970c51812dc3a010c7d01b50e0d17dc79c8","block_number":3576,"block_timestamp":1762954606,"prev_randao":"0xd29cd42cfd3c9ff4f31b39f497fc2f4d0a7add5a67da98be0d7841c37c7ad25f","input_index":2},"payload":"0x7b6d6574686f643a2070757263686173655f746f6b656e2c20746f6b656e5f69643a20317d"}} +PURCHASE SUCCESSFUL, STRUCTURING VOUCHERS!! +[INFO actix_web::middleware::logger] 127.0.0.1 "POST /voucher HTTP/1.1" 201 11 "-" "-" 0.015424 +Voucher generation successful +Token purchased and Withdrawn successfully +``` + +### 6. Recheck NFTs Listed on the Marketplace [inspect request] + +Finally, we can confirm that the purchased NFT has been removed from the listings by running the inspect query again: + +```bash +curl -X POST http://127.0.0.1:6751/inspect/marketplace \ + -H "Content-Type: application/json" \ + -d '{"method":"get_all_listed_tokens"}' +``` + +This call returns a hex payload like below: + +```bash +{"status":"Accepted","reports":[{"payload":"0x416c6c206c697374656420746f6b656e73206172653a205b5d"}],"processed_input_count":7} +``` + +The payload hex `0x416c6...653a205b5d` when decoded, returns `All listed tokens are: []`. Thereby verifying that the token with `Id 1` has successfully been sold and no longer listed for sale in the marketplace. + +## Conclusion + +Congratulations!!! + +You’ve successfully built and interacted with your own Marketplace application on Cartesi. + +This example covered essential Cartesi concepts such as routing, asset management, voucher generation, and the use of both Inspect and Advance routes. + +For a more detailed version of this code, you can check the `marketplace` folder for your selected language in [this repository](https://github.com/Mugen-Builders/Counter-X-Marketplace-apps) \ No newline at end of file diff --git a/cartesi-rollups_versioned_docs/version-2.0/tutorials/snippets/counter-js.md b/cartesi-rollups_versioned_docs/version-2.0/tutorials/snippets/counter-js.md new file mode 100644 index 00000000..d70b32a9 --- /dev/null +++ b/cartesi-rollups_versioned_docs/version-2.0/tutorials/snippets/counter-js.md @@ -0,0 +1,64 @@ +```javascript +const rollup_server = process.env.ROLLUP_HTTP_SERVER_URL; +console.log("HTTP rollup_server url is " + rollup_server); + +class Counter { + constructor() { + this.count = 0; + } + + increment() { + this.count += 1; + return this.count; + } + + get() { + return this.count; + } +} + +var counter = new Counter(); + +async function handle_advance(data) { + console.log("Received advance request data " + JSON.stringify(data)); + var new_val = counter.increment(); + console.log(`Counter increment requested, new count value: ${new_val}`); + return "accept"; +} + +async function handle_inspect(data) { + console.log("Received inspect request data " + JSON.stringify(data)); + console.log(`Current counter value: ${counter.get()}`); + return "accept"; +} + +var handlers = { + advance_state: handle_advance, + inspect_state: handle_inspect, +}; + +var finish = { status: "accept" }; + +(async () => { + while (true) { + const finish_req = await fetch(rollup_server + "/finish", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ status: "accept" }), + }); + + console.log("Received finish status " + finish_req.status); + + if (finish_req.status == 202) { + console.log("No pending rollup request, trying again"); + } else { + const rollup_req = await finish_req.json(); + var handler = handlers[rollup_req["request_type"]]; + finish["status"] = await handler(rollup_req["data"]); + } + } +})(); + +``` diff --git a/cartesi-rollups_versioned_docs/version-2.0/tutorials/snippets/counter-py.md b/cartesi-rollups_versioned_docs/version-2.0/tutorials/snippets/counter-py.md new file mode 100644 index 00000000..9ea9c973 --- /dev/null +++ b/cartesi-rollups_versioned_docs/version-2.0/tutorials/snippets/counter-py.md @@ -0,0 +1,57 @@ +```python +from os import environ +import logging +import requests + +logging.basicConfig(level="INFO") +logger = logging.getLogger(__name__) + +rollup_server = environ["ROLLUP_HTTP_SERVER_URL"] +logger.info(f"HTTP rollup_server url is {rollup_server}") + +class Counter: + def __init__(self): + self.value = 0 + + def increment(self): + self.value += 1 + return self.value + + def get(self): + return self.value + +counter = Counter() + +def handle_advance(data): + logger.info(f"Received advance request data {data}") + new_val = counter.increment() + logger.info(f"Counter increment requested, new count value: {new_val}") + return "accept" + + +def handle_inspect(data): + logger.info(f"Received inspect request data {data}") + logger.info(f"Current counter value: {counter.get()}") + return "accept" + + +handlers = { + "advance_state": handle_advance, + "inspect_state": handle_inspect, +} + +finish = {"status": "accept"} + +while True: + logger.info("Sending finish") + response = requests.post(rollup_server + "/finish", json=finish) + logger.info(f"Received finish status {response.status_code}") + if response.status_code == 202: + logger.info("No pending rollup request, trying again") + else: + rollup_request = response.json() + data = rollup_request["data"] + handler = handlers[rollup_request["request_type"]] + finish["status"] = handler(rollup_request["data"]) + +``` diff --git a/cartesi-rollups_versioned_docs/version-2.0/tutorials/snippets/counter-rs.md b/cartesi-rollups_versioned_docs/version-2.0/tutorials/snippets/counter-rs.md new file mode 100644 index 00000000..3dfd8aa5 --- /dev/null +++ b/cartesi-rollups_versioned_docs/version-2.0/tutorials/snippets/counter-rs.md @@ -0,0 +1,99 @@ +```rust +use json::{object, JsonValue}; +use std::env; + +#[derive(Clone, Debug, Copy)] +pub struct Counter { + count: u64, +} + +impl Counter { + fn new() -> Self { + Counter { count: 0 } + } + + fn increment(&mut self) { + self.count += 1; + } + + fn get_count(&self) -> u64 { + self.count + } +} + +pub async fn handle_advance( + _client: &hyper::Client, + _server_addr: &str, + request: JsonValue, + counter: &mut Counter +) -> Result<&'static str, Box> { + println!("Received advance request data {}", &request); + let _payload = request["data"]["payload"] + .as_str() + .ok_or("Missing payload")?; + + counter.increment(); + println!("Counter increment requested, new count value: {}", counter.get_count()); + Ok("accept") +} + +pub async fn handle_inspect( + _client: &hyper::Client, + _server_addr: &str, + request: JsonValue, + counter: &mut Counter +) -> Result<&'static str, Box> { + println!("Received inspect request data {}", &request); + let _payload = request["data"]["payload"] + .as_str() + .ok_or("Missing payload")?; + + println!("fetching current count value"); + let current_count = counter.get_count(); + println!("Current counter value: {}", current_count); + Ok("accept") +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = hyper::Client::new(); + let server_addr = env::var("ROLLUP_HTTP_SERVER_URL")?; + + let mut counter = Counter::new(); + println!("Initial counter value: {}", counter.get_count()); + + let mut status = "accept"; + loop { + println!("Sending finish"); + let response = object! {"status" => status}; + let request = hyper::Request::builder() + .method(hyper::Method::POST) + .header(hyper::header::CONTENT_TYPE, "application/json") + .uri(format!("{}/finish", &server_addr)) + .body(hyper::Body::from(response.dump()))?; + let response = client.request(request).await?; + println!("Received finish status {}", response.status()); + + if response.status() == hyper::StatusCode::ACCEPTED { + println!("No pending rollup request, trying again"); + } else { + let body = hyper::body::to_bytes(response).await?; + let utf = std::str::from_utf8(&body)?; + let req = json::parse(utf)?; + + let request_type = req["request_type"] + .as_str() + .ok_or("request_type is not a string")?; + status = match request_type { + "advance_state" => handle_advance(&client, &server_addr[..], req, &mut counter).await?, + "inspect_state" => handle_inspect(&client, &server_addr[..], req, &mut counter).await?, + &_ => { + eprintln!("Unknown request type"); + "reject" + } + }; + } + } +} + +``` diff --git a/cartesi-rollups_versioned_docs/version-2.0/tutorials/snippets/marketplace-js.md b/cartesi-rollups_versioned_docs/version-2.0/tutorials/snippets/marketplace-js.md new file mode 100644 index 00000000..d7434c14 --- /dev/null +++ b/cartesi-rollups_versioned_docs/version-2.0/tutorials/snippets/marketplace-js.md @@ -0,0 +1,424 @@ +```javascript +import { encodeFunctionData, toHex, zeroHash } from "viem"; + +const rollup_server = process.env.ROLLUP_HTTP_SERVER_URL; +console.log("HTTP rollup_server url is " + rollup_server); + +const asBigInt = (v) => (typeof v === "bigint" ? v : BigInt(v)); +const normAddr = (a) => a.toLowerCase(); +const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; +const erc721Abi = [ + { + name: "transferFrom", + type: "function", + stateMutability: "nonpayable", + inputs: [ + { name: "from", type: "address" }, + { name: "to", type: "address" }, + { name: "tokenId", type: "uint256" }, + ], + outputs: [], + }, +]; + +class Storage { + constructor(erc721_portal_address, erc20_portal_address, erc721_token, erc20_token, list_price) { + this.erc721_portal_address = erc721_portal_address; + this.erc20_portal_address = erc20_portal_address; + this.erc721_token = erc721_token; + this.erc20_token = erc20_token; + this.application_address = normAddr(ZERO_ADDRESS); + this.list_price = list_price + + this.listed_tokens = []; + this.users_erc20_token_balance = new Map(); + this.user_erc721_token_balance = new Map(); + this.erc721_id_to_owner_address = new Map(); + } + + getListedTokens() { + return this.listed_tokens; + } + + getAppAddress() { + return this.application_address; + } + + setAppAddress(app_addr) { + this.application_address = normAddr(app_addr); + } + + getUserERC721TokenBalance(userAddress) { + return this.user_erc721_token_balance.get(normAddr(userAddress)); + } + + getERC721TokenOwner(tokenId) { + return this.erc721_id_to_owner_address.get(asBigInt(tokenId)); + } + + getUserERC20TokenBalance(userAddress) { + return this.users_erc20_token_balance.get(normAddr(userAddress)) || 0n; + } + + increaseUserBalance(userAddress, amount) { + const addr = normAddr(userAddress); + const current = this.users_erc20_token_balance.get(addr) || 0n; + this.users_erc20_token_balance.set(addr, current + BigInt(amount)); + } + + async reduceUserBalance(userAddress, amount) { + const addr = normAddr(userAddress); + const current = this.users_erc20_token_balance.get(addr); + + if (current === undefined || current < BigInt(amount)) { + await emitReport(`User ${addr} record not found`); + console.log("User balance record not found"); + return; + } + this.users_erc20_token_balance.set(addr, current - BigInt(amount)); + } + + depositERC721Token(userAddress, tokenId) { + const addr = normAddr(userAddress); + const tid = asBigInt(tokenId); + this.erc721_id_to_owner_address.set(tid, addr); + + let previous_owner = this.getERC721TokenOwner(tid); + + if (normAddr(previous_owner) === normAddr(ZERO_ADDRESS)) { + this.changeERC721TokenOwner(tid, addr, normAddr(ZERO_ADDRESS)); + } else { + const tokens = this.user_erc721_token_balance.get(addr) || []; + if (!tokens.some((t) => t === tid)) tokens.push(tid); + this.user_erc721_token_balance.set(addr, tokens); + } + } + + listTokenForSale(tokenId) { + const tid = asBigInt(tokenId); + if (!this.listed_tokens.some((id) => id === tid)) this.listed_tokens.push(tid); + } + + changeERC721TokenOwner(tokenId, newOwner, oldOwner) { + const tid = asBigInt(tokenId); + const newAddr = normAddr(newOwner); + const oldAddr = normAddr(oldOwner); + + this.erc721_id_to_owner_address.set(tid, newAddr); + + const newOwnerTokens = this.user_erc721_token_balance.get(newAddr) || []; + if (!newOwnerTokens.some((id) => id === tid)) newOwnerTokens.push(tid); + this.user_erc721_token_balance.set(newAddr, newOwnerTokens); + + const oldOwnerTokens = this.user_erc721_token_balance.get(oldAddr) || []; + this.user_erc721_token_balance.set(oldAddr, oldOwnerTokens.filter((id) => id !== tid)); + } + + + async purchaseERC721Token(buyerAddress, erc721TokenAddress, tokenId) { + const tid = asBigInt(tokenId); + + if (!storage.listed_tokens.includes(tokenId)) { + await emitReport(`Token ${erc721TokenAddress} with id ${tid} is not for sale`); + console.log("Token is not for sale"); + return; + } + const owner = this.getERC721TokenOwner(tid); + if (!owner) { + await emitReport(`Token owner for token ${erc721TokenAddress} with id ${tid} not found`); + console.log("Token owner not found"); + return; + } + + await this.reduceUserBalance(buyerAddress, storage.list_price); + this.increaseUserBalance(owner, storage.list_price); + this.changeERC721TokenOwner(tid, ZERO_ADDRESS, owner); + this.listed_tokens = this.listed_tokens.filter((id) => id !== tid); + } +} + +async function handleERC20Deposit(depositorAddress, amountDeposited, tokenAddress) { + if (normAddr(tokenAddress) === normAddr(storage.erc20_token)) { + try { + storage.increaseUserBalance(depositorAddress, amountDeposited); + console.log("Token deposit processed successfully"); + } catch (error) { + console.log("error, handing ERC20 deposit ", error) + await emitReport(error.toString()); + } + } else { + console.log("Unsupported token deposited"); + await emitReport("Unsupported token deposited"); + } +} + +async function handleERC721Deposit(depositorAddress, tokenId, tokenAddress) { + if (normAddr(tokenAddress) === normAddr(storage.erc721_token)) { + try { + storage.depositERC721Token(depositorAddress, tokenId); + storage.listTokenForSale(tokenId); + console.log("Token deposit and Listing processed successfully"); + emitNotice("Token ID: " + tokenId + " Deposited by User: " + depositorAddress) + } catch (error) { + console.log("error, handing ERC721 deposit ", error) + await emitReport(error.toString()); + } + } else { + console.log("Unsupported token deposited"); + await emitReport("Unsupported token deposited"); + } +} + + +function tokenDepositParse(payload) { + const hexstr = payload.startsWith("0x") ? payload.slice(2) : payload; + const bytes = Buffer.from(hexstr, "hex"); + + if (bytes.length < 20 + 20 + 32) { + console.log(`payload too short: ${bytes.length} bytes`); + } + + const token = bytes.slice(0, 20); + const receiver = bytes.slice(20, 40); + const amount_be = bytes.slice(40, 72); + + for (let i = 0; i < 16; i++) { + if (amount_be[i] !== 0) { + console.log("amount too large for u128"); + } + } + + const lo = amount_be.slice(16); + let amount = 0n; + for (const b of lo) { + amount = (amount << 8n) + BigInt(b); + } + + return { + token: "0x" + token.toString("hex"), + receiver: "0x" + receiver.toString("hex"), + amount: amount.toString(), + }; +} + +async function extractField(json, field) { + const value = json[field]; + + if (typeof value === "string" && value.trim() !== "") { + return value; + } else { + await emitReport(`Missing or invalid ${field} field in payload`); + console.log(`Missing or invalid ${field} field in payload`); + } +} + + +async function handlePurchaseToken(callerAddress, userInput) { + try { + const erc721TokenAddress = normAddr(storage.erc721_token); + const tokenId = BigInt(await extractField(userInput, "token_id")); + + try { + await storage.purchaseERC721Token(callerAddress, erc721TokenAddress, tokenId); + console.log("Token purchased successfully"); + let voucher = structureVoucher({ + abi: erc721Abi, + functionName: "transferFrom", + args: [storage.application_address, callerAddress, tokenId], + destination: storage.erc721_token, + }) + emitVoucher(voucher); + } catch (e) { + await emitReport(`Failed to purchase token: ${e.message}`); + console.log(`Failed to purchase token: ${e.message}`); + } + } catch (error) { + console.log("error purchasing token: ", error); + await emitReport(error.toString()); + } +} + +function hexToString(hex) { + if (typeof hex !== "string") return ""; + if (hex.startsWith("0x")) hex = hex.slice(2); + return Buffer.from(hex, "hex").toString("utf8"); +} + +async function handle_advance(data) { + console.log("Received advance request data " + JSON.stringify(data)); + + const sender = data["metadata"]["msg_sender"]; + app_contract = normAddr(data["metadata"]["app_contract"]) + + if (normAddr(storage.application_address) == normAddr(ZERO_ADDRESS)) { + storage.setAppAddress(app_contract); + } + + const payload = hexToString(data.payload); + + if (normAddr(sender) == normAddr(storage.erc20_portal_address)) { + let { token, receiver, amount } = tokenDepositParse(data.payload); + await handleERC20Deposit(receiver, amount, token); + } else if (normAddr(sender) == normAddr(storage.erc721_portal_address)) { + let { token, receiver, amount } = tokenDepositParse(data.payload) + await handleERC721Deposit(receiver, asBigInt(amount), token); + } else { + const payload_obj = JSON.parse(payload); + let method = payload_obj["method"]; + switch (method) { + case "purchase_token": { + await handlePurchaseToken(sender, payload_obj); + break; + } + default: {console.log("Unwupported method called!!"); emitReport("Unwupported method called!!")} + } + } + return "accept"; +} + +async function handle_inspect(data) { + console.log("Received inspect request data " + JSON.stringify(data)); + + const payload = hexToString(data.payload); + let payload_obj; + + try { + payload_obj = JSON.parse(payload); + } catch (e) { + await emitReport("Invalid payload JSON"); + return "accept"; + } + + switch (payload_obj.method) { + case "get_user_erc20_balance": { + const user_address = payload_obj["user_address"]; + const bal = storage.getUserERC20TokenBalance(normAddr(user_address)); + await emitReport(`User: ${user_address} Balance: ${bal.toString()}`); + break; + } + case "get_token_owner": { + const token_id = BigInt(payload_obj["token_id"]); + const token_owner = storage.getERC721TokenOwner(token_id); + await emitReport(`Token_id: ${token_id.toString()} owner: ${token_owner ?? "None"}`); + break; + } + case "get_all_listed_tokens": { + const listed_tokens = storage.getListedTokens(); + await emitReport(`All listed tokens are: ${listed_tokens.map(String).join(",")}`); + break; + } + default: { + console.log("Unsupported method called!!"); + await emitReport("Unsupported inspect method"); + } + } + return "accept"; +} + +function stringToHex(str) { + if (typeof str !== "string") { + console.log("stringToHex: input must be a string"); + } + const utf8 = Buffer.from(str, "utf8"); + return "0x" + utf8.toString("hex"); +} + +function structureVoucher({ abi, functionName, args, destination, value = 0n }) { + const payload = encodeFunctionData({ + abi, + functionName, + args, + }); + + const valueHex = value === 0n ? zeroHash : toHex(BigInt(value)); + + return { + destination, + payload, + value: valueHex, + } +} + +const emitVoucher = async (voucher) => { + try { + await fetch(rollup_server + "/voucher", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(voucher), + }); + } catch (error) { + emitReport("error emitting Voucher"); + console.log("error emitting voucher: ", error) + } +}; + +const emitReport = async (payload) => { + let hexPayload = stringToHex(payload); + try { + await fetch(rollup_server + "/report", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ payload: hexPayload }), + }); + } catch (error) { + console.log("error emitting report: ", error) + } +}; + +const emitNotice = async (payload) => { + let hexPayload = stringToHex(payload); + try { + await fetch(rollup_server + "/notice", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ payload: hexPayload }), + }); + } catch (error) { + emitReport("error emitting Notice"); + console.log("error emitting notice: ", error) + } +}; + +var handlers = { + advance_state: handle_advance, + inspect_state: handle_inspect, +}; + +let erc721_portal_address = "0xc700d52F5290e978e9CAe7D1E092935263b60051"; +let erc20_portal_address = "0xc700D6aDd016eECd59d989C028214Eaa0fCC0051"; +let erc20_token = "0xFBdB734EF6a23aD76863CbA6f10d0C5CBBD8342C"; +let erc721_token = "0xBa46623aD94AB45850c4ecbA9555D26328917c3B"; +let list_price = BigInt("100000000000000000000"); + +var storage = new Storage(erc721_portal_address, erc20_portal_address, erc721_token, erc20_token, list_price); + +var finish = { status: "accept" }; + +(async () => { + while (true) { + const finish_req = await fetch(rollup_server + "/finish", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ status: "accept" }), + }); + + console.log("Received finish status " + finish_req.status); + + if (finish_req.status == 202) { + console.log("No pending rollup request, trying again"); + } else { + const rollup_req = await finish_req.json(); + var handler = handlers[rollup_req["request_type"]]; + finish["status"] = await handler(rollup_req["data"]); + } + } +})(); +``` diff --git a/cartesi-rollups_versioned_docs/version-2.0/tutorials/snippets/marketplace-py.md b/cartesi-rollups_versioned_docs/version-2.0/tutorials/snippets/marketplace-py.md new file mode 100644 index 00000000..cbf84ad9 --- /dev/null +++ b/cartesi-rollups_versioned_docs/version-2.0/tutorials/snippets/marketplace-py.md @@ -0,0 +1,379 @@ +```python +from os import environ +import logging +import requests +import binascii +import json +from eth_utils import function_signature_to_4byte_selector +from eth_abi import encode + +logging.basicConfig(level="INFO") +logger = logging.getLogger(__name__) + +rollup_server = environ["ROLLUP_HTTP_SERVER_URL"] +logger.info(f"HTTP rollup_server url is {rollup_server}") + + +def norm_addr(addr: str) -> str: + return addr.lower() + +def as_int(v) -> int: + return v if isinstance(v, int) else int(v) + +def string_to_hex(s: str) -> str: + return "0x" + binascii.hexlify(s.encode("utf-8")).decode() + +def hex_to_string(hexstr: str) -> str: + if not isinstance(hexstr, str): + return "" + if hexstr.startswith("0x"): + hexstr = hexstr[2:] + if hexstr == "": + return "" + try: + return binascii.unhexlify(hexstr).decode("utf-8") + except UnicodeDecodeError: + return "0x" + hexstr + +def token_deposit_parse(payload_hex: str): + hexstr = payload_hex[2:] if payload_hex.startswith("0x") else payload_hex + b = binascii.unhexlify(hexstr) + + if len(b) < 20 + 20 + 32: + logger.error(f"payload too short: {len(b)} bytes") + return + + token = b[0:20] + receiver = b[20:40] + amount_be = b[40:72] + + val = int.from_bytes(amount_be, byteorder="big", signed=False) + + token_hex = "0x" + token.hex() + receiver_hex = "0x" + receiver.hex() + return { + "token": token_hex, + "receiver": receiver_hex, + "amount": str(val), + } + +def extract_field(obj: dict, field: str) -> str: + v = obj.get(field, None) + if isinstance(v, str) and v.strip() != "": + return v + else: + logger.error(f"Missing or invalid {field} field in payload") + return + + +def emitReport(payload: str): + hex_payload = string_to_hex(payload) + try: + response = requests.post( + f"{rollup_server}/report", + json={"payload": hex_payload}, + headers={"Content-Type": "application/json"}, + timeout=5, + ) + logger.info(f"emit_report → status {response.status_code}") + except requests.RequestException as error: + logger.error("Error emitting report: %s", error) + + +def emitNotice(payload: str): + hex_payload = string_to_hex(payload) + try: + response = requests.post( + f"{rollup_server}/notice", + json={"payload": hex_payload}, + headers={"Content-Type": "application/json"}, + timeout=5, + ) + logger.info(f"notice → status {response.status_code}") + except requests.RequestException as error: + logger.error("Error emitting notice: %s", error) + +def structure_voucher(function_signature, destination, types, values, value=0) -> dict: + selector = function_signature_to_4byte_selector(function_signature) + encoded_args = encode(types, values) + payload = "0x" + (selector + encoded_args).hex() + + return { + "destination": destination, + "payload": payload, + "value": f"0x{value:064x}" + } + +def emitVoucher(voucher: dict): + try: + response = requests.post( + f"{rollup_server}/voucher", + json= voucher, + headers={"Content-Type": "application/json"}, + timeout=5, + ) + logger.info(f"emit_voucher → status {response.status_code}") + except requests.RequestException as error: + logger.error("Error emitting voucher: %s", error) + +class Storage: + def __init__(self, erc721_portal_address: str, erc20_portal_address: str, + erc721_token: str, erc20_token: str, list_price: int): + + self.erc721_portal_address = norm_addr(erc721_portal_address) + self.erc20_portal_address = norm_addr(erc20_portal_address) + self.erc721_token = norm_addr(erc721_token) + self.erc20_token = norm_addr(erc20_token) + self.application_address = norm_addr("0x" + "0" * 40) + self.list_price = list_price + + self.listed_tokens: list[int] = [] + self.users_erc20_token_balance: dict[str, int] = {} + self.user_erc721_token_balance: dict[str, list[int]] = {} + self.erc721_id_to_owner_address: dict[int, str] = {} + + def getListedTokens(self): + return self.listed_tokens + + def getAppAddress(self): + return self.application_address + + def setAppAddress(self, app_addr): + self.application_address = norm_addr(app_addr) + + def getERC721TokenOwner(self, tokenId: int): + return self.erc721_id_to_owner_address.get(as_int(tokenId)) + + def getUserERC20TokenBalance(self, userAddress: str) -> int: + addr = norm_addr(userAddress) + return self.users_erc20_token_balance.get(addr, 0) + + def increaseUserBalance(self, userAddress: str, amount): + addr = norm_addr(userAddress) + amt = as_int(amount) + current = self.users_erc20_token_balance.get(addr, 0) + self.users_erc20_token_balance[addr] = current + amt + + def reduceUserBalance(self, userAddress: str, amount): + addr = norm_addr(userAddress) + amt = as_int(amount) + current = self.users_erc20_token_balance.get(addr, None) + + if current is None: + emitReport(f"User {addr} record not found") + logger.error("User balance record not found") + return + + if current < amt: + emitReport(f"User {addr} has insufficient balance") + logger.error("User has insufficient balance") + return + + self.users_erc20_token_balance[addr] = current - amt + + def depositERC721Token(self, userAddress: str, tokenId): + addr = norm_addr(userAddress) + tid = as_int(tokenId) + zero_addr = "0x" + "0" * 40; + + previous_owner: str = self.getERC721TokenOwner(tid); + + if previous_owner and norm_addr(previous_owner) == norm_addr(zero_addr): + self.changeERC721TokenOwner(tid, addr, zero_addr); + else: + self.erc721_id_to_owner_address[tid] = addr + tokens = self.user_erc721_token_balance.get(addr, []) + if tid not in tokens: + tokens.append(tid) + self.user_erc721_token_balance[addr] = tokens + + def listTokenForSale(self, tokenId): + tid = as_int(tokenId) + if tid not in self.listed_tokens: + self.listed_tokens.append(tid) + + def changeERC721TokenOwner(self, tokenId, newOwner: str, oldOwner: str): + tid = as_int(tokenId) + new_addr = norm_addr(newOwner) + old_addr = norm_addr(oldOwner) + + self.erc721_id_to_owner_address[tid] = new_addr + + new_tokens = self.user_erc721_token_balance.get(new_addr, []) + if tid not in new_tokens: + new_tokens.append(tid) + self.user_erc721_token_balance[new_addr] = new_tokens + + old_tokens = self.user_erc721_token_balance.get(old_addr, []) + self.user_erc721_token_balance[old_addr] = [i for i in old_tokens if i != tid] + + def purchaseERC721Token(self, buyerAddress: str, erc721TokenAddress: str, tokenId) -> bool: + tid = as_int(tokenId) + + if not tokenId in self.listed_tokens: + emitReport(f"Token {erc721TokenAddress} with id {tid} is not for sale") + logger.error("Token is not for sale") + return False + + self.reduceUserBalance(buyerAddress, self.list_price) + + owner = self.getERC721TokenOwner(tid) + if not owner: + emitReport(f"Token owner for token {erc721TokenAddress} with id {tid} not found") + logger.error("Token owner not found") + return False + + zero_address = norm_addr("0x" + "0" * 40) + self.increaseUserBalance(owner, self.list_price) + self.listed_tokens = [i for i in self.listed_tokens if i != tid] + self.changeERC721TokenOwner(tid, zero_address, owner) + return True + + +erc_721_portal_address = "0xc700d52F5290e978e9CAe7D1E092935263b60051" +erc20_portal_address = "0xc700D6aDd016eECd59d989C028214Eaa0fCC0051" +erc20_token = "0xFBdB734EF6a23aD76863CbA6f10d0C5CBBD8342C" +erc721_token = "0xBa46623aD94AB45850c4ecbA9555D26328917c3B" +list_price = 100_000_000_000_000_000_000 + +storage = Storage(erc_721_portal_address, erc20_portal_address, erc721_token, erc20_token, list_price) + + +def handle_erc20_deposit(depositor_address: str, amount_deposited, token_address: str): + if norm_addr(token_address) == storage.erc20_token: + try: + storage.increaseUserBalance(depositor_address, as_int(amount_deposited)) + except Exception as e: + logger.error("Error handling ERC20 deposit: %s", e) + emitReport(str(e)) + else: + emitReport("Unsupported token deposited") + + +def handle_erc721_deposit(depositor_address: str, token_id, token_address: str): + if norm_addr(token_address) == storage.erc721_token: + try: + storage.depositERC721Token(depositor_address, as_int(token_id)) + storage.listTokenForSale(token_id) + logger.info("Token Listed Successfully") + emitNotice(f"Token ID: {token_id} Deposited by User: {depositor_address}") + except Exception as e: + logger.error("Error handling ERC721 deposit: %s", e) + emitReport(str(e)) + else: + logger.info("Unsupported token deposited (ERC721)") + emitReport("Unsupported token deposited") + +def handle_purchase_token(caller_address: str, payload_obj: dict): + try: + token_id_str = extract_field(payload_obj, "token_id") + token_id = as_int(token_id_str) + erc721_addr = storage.erc721_token + try: + if storage.purchaseERC721Token(caller_address, erc721_addr, token_id): + logger.info("Token purchased successfully, Processing Voucher....") + voucher = structure_voucher( + "transferFrom(address,address,uint256)", + storage.erc721_token, + ["address", "address", "uint256"], + [ storage.application_address, norm_addr(caller_address), token_id], + ) + emitVoucher(voucher) + else: + emitReport(f"Error purchasing token {token_id}") + return + except Exception as e: + emitReport(f"Failed to purchase token: {e}") + logger.error("Failed to purchase token: %s", e) + except Exception as e: + logger.error("Error purchasing token: %s", e) + emitReport(str(e)) + + +def handle_advance(data): + logger.info(f"Received advance request data {data}") + + sender = norm_addr(data["metadata"]["msg_sender"]) + app_contract = norm_addr(data["metadata"]["app_contract"]) + zero_addr = norm_addr("0x" + "0" * 40) + + if norm_addr(storage.application_address) == zero_addr: + storage.setAppAddress(app_contract) + + payload_hex = data.get("payload", "") + payload_str = hex_to_string(payload_hex) + + if sender == storage.erc20_portal_address: + parsed = token_deposit_parse(payload_hex) + token, receiver, amount = parsed["token"], parsed["receiver"], parsed["amount"] + handle_erc20_deposit(receiver, int(amount), token) + + elif sender == storage.erc721_portal_address: + parsed = token_deposit_parse(payload_hex) + token, receiver, amount = parsed["token"], parsed["receiver"], parsed["amount"] + handle_erc721_deposit(receiver, int(amount), token) + + else: + try: + payload_obj = json.loads(payload_str) + except Exception: + emitReport("Invalid payload JSON") + return "accept" + + method = payload_obj.get("method", "") + if method == "purchase_token": + handle_purchase_token(sender, payload_obj) + else: + logger.info("Unsupported method called") + emitReport("Unsupported method") + return "accept" + + +def handle_inspect(data: dict): + logger.info(f"Received inspect request data {data}") + + payload_str = hex_to_string(data.get("payload", "0x")) + try: + payload_obj = json.loads(payload_str) + except Exception: + emitReport("Invalid payload string") + return "accept" + + method = payload_obj.get("method", "") + if method == "get_user_erc20_balance": + user_address = norm_addr(payload_obj.get("user_address", "")) + bal = storage.getUserERC20TokenBalance(user_address) + emitReport(f"User: {user_address} Balance: {bal}") + elif method == "get_token_owner": + token_id = as_int(payload_obj.get("token_id", 0)) + owner = storage.getERC721TokenOwner(token_id) + emitReport(f"Token_id: {token_id} owner: {owner if owner else 'None'}") + elif method == "get_all_listed_tokens": + listed = storage.getListedTokens() + emitReport("All listed tokens are: " + ",".join(map(str, listed))) + else: + logger.info("Unsupported inspect method") + emitReport("Unsupported inspect method") + return "accept" + + +handlers = { + "advance_state": handle_advance, + "inspect_state": handle_inspect, +} + +finish = {"status": "accept"} + +while True: + logger.info("Sending finish") + response = requests.post(rollup_server + "/finish", json=finish) + logger.info(f"Received finish status {response.status_code}") + if response.status_code == 202: + logger.info("No pending rollup request, trying again") + else: + rollup_request = response.json() + data = rollup_request["data"] + handler = handlers[rollup_request["request_type"]] + finish["status"] = handler(rollup_request["data"]) + +``` \ No newline at end of file diff --git a/cartesi-rollups_versioned_docs/version-2.0/tutorials/snippets/marketplace-rs.md b/cartesi-rollups_versioned_docs/version-2.0/tutorials/snippets/marketplace-rs.md new file mode 100644 index 00000000..14911d7d --- /dev/null +++ b/cartesi-rollups_versioned_docs/version-2.0/tutorials/snippets/marketplace-rs.md @@ -0,0 +1,469 @@ +```rust +use json::{object, JsonValue}; +use std::env; +use ethers_core::abi::{encode, Token}; +use ethers_core::utils::id; +use hex; + +#[derive(Debug, Clone, Default)] +pub struct Storage { + pub erc721_portal_address: String, + pub erc20_portal_address: String, + pub erc721_token: String, + pub erc20_token: String, + pub app_address: String, + pub list_price: u128, + pub listed_tokens: Vec, + + pub users_erc20_token_balance: std::collections::HashMap, + pub user_erc721_token_balance: std::collections::HashMap>, + pub erc721_id_to_owner_address: std::collections::HashMap, +} + +impl Storage { + fn new ( + erc721_portal_address: String, + erc20_portal_address: String, + erc721_token: String, + erc20_token: String, + list_price: u128 + ) -> Self { + Storage{ + erc20_portal_address, + erc721_portal_address, + erc20_token, + erc721_token, + list_price, + app_address: "0x0000000000000000000000000000000000000000".to_string(), + listed_tokens: Vec::new(), + user_erc721_token_balance: std::collections::HashMap::new(), + users_erc20_token_balance: std::collections::HashMap::new(), + erc721_id_to_owner_address: std::collections::HashMap::new(), + } + } + + fn get_listed_tokens(&self) -> Vec<&u128> { + self.listed_tokens.iter().collect() + } + + fn get_erc721_token_owner(&self, token_id: u128) -> Option<&String> { + self.erc721_id_to_owner_address.get(&token_id) + } + + fn get_user_erc20_token_balance(&self, user_address: &str) -> Option<&u128> { + self.users_erc20_token_balance.get(user_address) + } + + fn increase_user_balance(&mut self, user_address: String, amount: u128) { + let balance = self.users_erc20_token_balance.entry(user_address).or_insert(0); + *balance += amount; + } + + fn deposit_erc721_token(&mut self, user_address: String, token_id: u128) { + let zero_address = "0x0000000000000000000000000000000000000000".to_string(); + if self.get_erc721_token_owner(token_id) == Some(&zero_address) { + self.change_erc721_token_owner(token_id, user_address, zero_address); + } else { + self.erc721_id_to_owner_address.insert(token_id, user_address.to_lowercase()); + self.user_erc721_token_balance + .entry(user_address) + .or_insert_with(Vec::new) + .push(token_id); + } + } + + fn list_token_for_sale(&mut self, token_id: u128) { + if !self.listed_tokens.contains(&token_id) { + self.listed_tokens.push(token_id); + } + } + + async fn reduce_user_balance(&mut self, user_address: &str, amount: u128) -> Result<(), String> { + if let Some(balance) = self.users_erc20_token_balance.get_mut(user_address){ + if *balance >= amount { + *balance -= amount; + Ok(()) + } else { + emit_report(format!("User {} has insufficient balance to purchase token", user_address)).await; + Err(String::from("User has insufficient balance")) + } + } else { + emit_report(format!("User {} Record, not found", user_address)).await; + Err(String::from("User balance record not found")) + } + } + + fn change_erc721_token_owner(&mut self, token_id: u128, new_owner: String, old_owner: String) { + if let Some(owner) = self.erc721_id_to_owner_address.get_mut(&token_id) { + *owner = new_owner.clone(); + } + + if let Some(tokens) = self.user_erc721_token_balance.get_mut(&new_owner) { + tokens.push(token_id); + } + + if let Some(tokens) = self.user_erc721_token_balance.get_mut(&old_owner) { + tokens.retain(|token| !(*token == token_id)); + } + } + + async fn purchase_erc721_token(&mut self, buyer_address: &str, token_id: u128) -> Result<(), String> { + self.reduce_user_balance( buyer_address, self.list_price).await?; + if let Some(owner) = self.get_erc721_token_owner( token_id) { + self.increase_user_balance(owner.clone(), self.list_price); + } else { + emit_report(format!("Token owner for token with id {} not found", token_id)).await; + return Err("Token owner not found".to_string()); + } + self.listed_tokens.retain(|token| (*token != token_id)); + let zero_address = "0x0000000000000000000000000000000000000000"; + self.change_erc721_token_owner( token_id, zero_address.to_string(), self.get_erc721_token_owner( token_id).unwrap().to_string()); + Ok(()) + } +} + +async fn handle_erc20_deposit(depositor_address: String, amount_deposited: u128, token_address: String, storage: &mut Storage) { + if token_address.to_lowercase() == storage.erc20_token.to_lowercase() { + storage.increase_user_balance(depositor_address, amount_deposited); + println!("Token deposit processed successfully"); + } else { + println!("Unsupported token deposited"); + emit_report("Unsupported token deposited".into()).await; + } +} + +async fn handle_erc721_deposit(depositor_address: String, token_id: u128, token_address: String, storage: &mut Storage) { + if token_address.to_lowercase() == storage.erc721_token.to_lowercase() { + storage.deposit_erc721_token(depositor_address.clone(), token_id); + storage.list_token_for_sale(token_id); + println!("Token deposit and listing processed successfully"); + emit_notice(format!("Token Id: {}, Deposited by User: {}", token_id, depositor_address)).await; + } else { + println!("Unsupported token deposited"); + emit_report("Unsupported token deposited".into()).await; + } +} + +async fn handle_purchase_token(sender: String, user_input: JsonValue, storage: &mut Storage) { + let token_id: u128 = user_input["token_id"].as_str().unwrap_or("0").parse().unwrap_or(0); + if storage.listed_tokens.contains(&token_id) != true { + emit_report("TOKEN NOT LISTED FOR SALE!!".to_string()).await; + return; + } + match storage.purchase_erc721_token(&sender, token_id).await { + Ok(_) => { + let args = vec![ + Token::Address(storage.app_address.parse().unwrap()), + Token::Address(sender.parse().unwrap()), + Token::Uint(token_id.into()), + ]; + let function_sig = "transferFrom(address,address,uint256)"; + let value: u128 = 0; + let selector = &id(function_sig)[..4]; + let encoded_args = encode(&args); + + let mut payload_bytes = Vec::new(); + payload_bytes.extend_from_slice(selector); + payload_bytes.extend_from_slice(&encoded_args); + + let payload = format!("0x{}", hex::encode(payload_bytes)); + + let voucher = object! { + "destination" => format!("{}", storage.erc721_token), + "payload" => format!("{}", payload), + "value" => format!("0x{}", hex::encode(value.to_be_bytes())), + }; + + emit_voucher(voucher).await; + println!("Token purchased and Withdrawn successfully"); + + }, + Err(e) => {emit_report("Failed to purchase token".into()).await; println!("Failed to purchase token: {}", e);} + } +} + +pub async fn handle_advance( + _client: &hyper::Client, + _server_addr: &str, + request: JsonValue, + storage: &mut Storage +) -> Result<&'static str, Box> { + println!("Received advance request data {}", &request); + let _payload = request["data"]["payload"] + .as_str() + .ok_or("Missing payload")?; + let zero_address = "0x0000000000000000000000000000000000000000".to_string(); + let app_addr = request["data"]["metadata"]["app_contract"] + .as_str() + .ok_or("Missing payload")?; + + if storage.app_address == zero_address { + storage.app_address = app_addr.to_string(); + } + let sender = request["data"]["metadata"]["msg_sender"] + .as_str() + .ok_or("Missing msg_sender in metadata")? + .to_string(); + + match sender.as_str() { + s if s.to_lowercase() == storage.erc20_portal_address.as_str().to_lowercase() => { + let (deposited_token, receiver_address, amount) = token_deposit_parse(&_payload)?; + println!("Deposited token: {}, Receiver: {}, Amount: {}", deposited_token, receiver_address, amount); + handle_erc20_deposit(receiver_address.to_lowercase(), amount, deposited_token.to_lowercase(), storage).await; + }, + s if s.to_lowercase() == storage.erc721_portal_address.as_str().to_lowercase() => { + let (deposited_token, receiver_address, token_id) = token_deposit_parse(&_payload)?; + println!("Deposited and listed token: {}, Receiver: {}, Token ID: {}", deposited_token, receiver_address, token_id); + handle_erc721_deposit(receiver_address.to_lowercase(), token_id, deposited_token.to_lowercase(), storage).await; + }, + _ => { + let payload_str = hex_to_string(_payload)?; + let payload_json ; + match json::parse(&payload_str) { + Err(_) => { + let fixed_payload = try_fix_json_like(&payload_str); + match json::parse(&fixed_payload) { + Err(_) => panic!("Failed to parse decoded payload as JSON even after attempting to fix it"), + Ok(val) => payload_json = val, + }; + } + Ok(val) => payload_json = val + }; + if !payload_json.is_object() { + emit_report("Decoded payload is not a JSON object".into()).await; + println!("Decoded payload is not a JSON object"); + } + if let Some(method_str) = payload_json["method"].as_str() { + match method_str { + "purchase_token" => {handle_purchase_token(sender.to_lowercase(), payload_json.clone(), storage).await; }, + _ => {emit_report("Unknown method in payload".into()).await; println!("Unknown method in payload")}, + } + } else { + emit_report("Missing or invalid method field in payload".into()).await; + println!("Missing or invalid method field in payload"); + } + } + } + Ok("accept") +} + +pub async fn handle_inspect( + _client: &hyper::Client, + _server_addr: &str, + request: JsonValue, + storage: &mut Storage +) -> Result<&'static str, Box> { + println!("Received inspect request data {}", &request); + let zero_address = "0x0000000000000000000000000000000000000000".to_string(); + let _payload = request["data"]["payload"] + .as_str() + .ok_or("Missing payload")?; + let payload_str = hex_to_string(_payload)?; + println!("Decoded payload: {}", payload_str); + + let payload_json ; + + match json::parse(&payload_str) { + Err(_) => { + let fixed_payload = try_fix_json_like(&payload_str); + match json::parse(&fixed_payload) { + Err(_) => panic!("Failed to parse decoded payload as JSON even after attempting to fix it"), + Ok(val) => payload_json = val, + }; + } + Ok(val) => payload_json = val + }; + + if let Some(method_str) = payload_json["method"].as_str() { + match method_str { + "get_user_erc20_balance" => { + let user_address = payload_json["user_address"].as_str().unwrap_or(&zero_address).to_lowercase(); + let user_balance = storage.get_user_erc20_token_balance(user_address.as_str()).unwrap_or(&0); + emit_report(format!("User: {}, balance: {}", user_address, user_balance)).await; + }, + "get_token_owner" => { + let token_id: u128 = payload_json["token_id"].as_u64().unwrap_or(0).into(); + let token_owner = storage.get_erc721_token_owner(token_id).unwrap_or(&zero_address); + emit_report(format!("Token id: {}, owner: {}", token_id, token_owner)).await; + }, + "get_all_listed_tokens" => { + let listed_tokens = storage.get_listed_tokens(); + emit_report(format!("All listed tokens are: {:?}", listed_tokens)).await; + }, + _ => {emit_report("Unknown method in payload".into()).await; println!("Unknown method in payload")}, + } + } else { + emit_report("Missing or invalid method field in payload".into()).await; + println!("Missing or invalid method field in payload"); + } + Ok("accept") +} + +pub fn token_deposit_parse(payload: &str) -> Result<(String, String, u128), String> { + let hexstr = payload.strip_prefix("0x").unwrap_or(payload); + let bytes = hex::decode(hexstr).map_err(|e| format!("hex decode error: {}", e))?; + if bytes.len() < 20 + 20 + 32 { + return Err(format!("payload too short: {} bytes", bytes.len())); + } + let token = &bytes[0..20]; + let receiver = &bytes[20..40]; + let amount_be = &bytes[40..72]; + + if amount_be[..16].iter().any(|&b| b != 0) { + return Err("amount too large for u128".into()); + } + let mut lo = [0u8; 16]; + lo.copy_from_slice(&amount_be[16..]); + let amount = u128::from_be_bytes(lo); + + Ok(( + format!("0x{}", hex::encode(token)), + format!("0x{}", hex::encode(receiver)), + amount, + )) +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = hyper::Client::new(); + let server_addr = env::var("ROLLUP_HTTP_SERVER_URL")?; + + let erc721_portal_address = String::from("0xc700d52F5290e978e9CAe7D1E092935263b60051"); + let erc20_portal_address = String::from("0xc700D6aDd016eECd59d989C028214Eaa0fCC0051"); + let erc20_token = String::from("0xFBdB734EF6a23aD76863CbA6f10d0C5CBBD8342C"); + let erc721_token = String::from("0xBa46623aD94AB45850c4ecbA9555D26328917c3B"); + let list_price: u128 = 100_000_000_000_000_000_000; + let mut storage = Storage::new(erc721_portal_address, erc20_portal_address, erc721_token, erc20_token, list_price); + + let mut status = "accept"; + loop { + println!("Sending finish"); + let response = object! {"status" => status}; + let request = hyper::Request::builder() + .method(hyper::Method::POST) + .header(hyper::header::CONTENT_TYPE, "application/json") + .uri(format!("{}/finish", &server_addr)) + .body(hyper::Body::from(response.dump()))?; + let response = client.request(request).await?; + println!("Received finish status {}", response.status()); + + if response.status() == hyper::StatusCode::ACCEPTED { + println!("No pending rollup request, trying again"); + } else { + let body = hyper::body::to_bytes(response).await?; + let utf = std::str::from_utf8(&body)?; + let req = json::parse(utf)?; + + let request_type = req["request_type"] + .as_str() + .ok_or("request_type is not a string")?; + status = match request_type { + "advance_state" => handle_advance(&client, &server_addr[..], req, &mut storage).await?, + "inspect_state" => handle_inspect(&client, &server_addr[..], req, &mut storage).await?, + &_ => { + eprintln!("Unknown request type"); + "reject" + } + }; + } + } +} + +fn hex_to_string(hex: &str) -> Result> { + let hexstr = hex.strip_prefix("0x").unwrap_or(hex); + let bytes = hex::decode(hexstr).map_err(|e| Box::new(e) as Box)?; + let s = String::from_utf8(bytes).map_err(|e| Box::new(e) as Box)?; + Ok(s) +} + +fn try_fix_json_like(s: &str) -> String { + let mut fixed = s.to_string(); + + // Add quotes around keys (rudimentary repair) + fixed = fixed.replace("{", "{\""); + fixed = fixed.replace(": ", "\":\""); + fixed = fixed.replace(", ", "\", \""); + fixed = fixed.replace("}", "\"}"); + + fixed +} + +async fn emit_report( payload: String) -> Option { + // convert the provided string payload to hex (strip optional "0x" prefix if present) + let hex_string = { + let s = payload.strip_prefix("0x").unwrap_or(payload.as_str()); + hex::encode(s.as_bytes()) + }; + + let server_addr = env::var("ROLLUP_HTTP_SERVER_URL").expect("ROLLUP_HTTP_SERVER_URL not set"); + let client = hyper::Client::new(); + + let response = object! { + "payload" => format!("0x{}", hex_string), + }; + let request = hyper::Request::builder() + .method(hyper::Method::POST) + .header(hyper::header::CONTENT_TYPE, "application/json") + .uri(format!("{}/report", server_addr)) + .body(hyper::Body::from(response.dump())) + .ok()?; + + let response = client.request(request).await; + match response { + Ok(_) => { + println!("Report generation successful"); + return Some(true); + } + Err(e) => { + println!("Report request failed {}", e); + None + } + } +} + +async fn emit_voucher( voucher: JsonValue) -> Option { + let server_addr = env::var("ROLLUP_HTTP_SERVER_URL").expect("ROLLUP_HTTP_SERVER_URL not set"); + let client = hyper::Client::new(); + + let request = hyper::Request::builder() + .method(hyper::Method::POST) + .header(hyper::header::CONTENT_TYPE, "application/json") + .uri(format!("{}/voucher", server_addr)) + .body(hyper::Body::from(voucher.dump())) + .ok()?; + + let response = client.request(request).await; + + match response { + Ok(_) => { + println!("Voucher generation successful"); + return Some(true); + } + Err(e) => { + println!("Voucher request failed {}", e); + None + } + } +} + +async fn emit_notice( payload: String) { + let hex_string = { + let s = payload.strip_prefix("0x").unwrap_or(payload.as_str()); + hex::encode(s.as_bytes()) + }; + + let server_addr = env::var("ROLLUP_HTTP_SERVER_URL").expect("ROLLUP_HTTP_SERVER_URL not set"); + let client = hyper::Client::new(); + + let response = object! { + "payload" => format!("0x{}", hex_string), + }; + let request = hyper::Request::builder() + .method(hyper::Method::POST) + .header(hyper::header::CONTENT_TYPE, "application/json") + .uri(format!("{}/notice", server_addr)) + .body(hyper::Body::from(response.dump())) + .ok(); + let _response = client.request(request.unwrap()).await; +} +``` diff --git a/cartesi-rollups_versioned_sidebars/version-1.5-sidebars.json b/cartesi-rollups_versioned_sidebars/version-1.5-sidebars.json index 48c55283..4ecb9d49 100644 --- a/cartesi-rollups_versioned_sidebars/version-1.5-sidebars.json +++ b/cartesi-rollups_versioned_sidebars/version-1.5-sidebars.json @@ -159,7 +159,9 @@ "label": "Tutorials", "collapsed": true, "items": [ + "tutorials/counter", "tutorials/calculator", + "tutorials/marketplace", "tutorials/ether-wallet", "tutorials/erc-20-token-wallet", "tutorials/erc-721-token-wallet", diff --git a/cartesi-rollups_versioned_sidebars/version-2.0-sidebars.json b/cartesi-rollups_versioned_sidebars/version-2.0-sidebars.json index f55cdb8a..e059597c 100644 --- a/cartesi-rollups_versioned_sidebars/version-2.0-sidebars.json +++ b/cartesi-rollups_versioned_sidebars/version-2.0-sidebars.json @@ -237,7 +237,9 @@ "label": "Tutorials", "collapsed": true, "items": [ + "tutorials/counter", "tutorials/calculator", + "tutorials/marketplace", "tutorials/ether-wallet", "tutorials/erc-20-token-wallet", "tutorials/erc-721-token-wallet",