# Local Devnet & Starknet.py's Account Capabilities Full-Stack Starknet: * **[Part 1]** 🚧 [Getting Started in Cairo & Deploying with Nile](https://hackmd.io/@sambarnes/BJvGs0JpK) * **[Part 2]** 🐍 [Contract Interaction with starknet.py](https://hackmd.io/@sambarnes/H1Fx7OMaF) * **[Part 3]** πŸ‘₯ [StarkNet Account Abstraction & Using Standard Contracts](https://hackmd.io/@sambarnes/rkGekNvAY) * **[Part 4]** πŸ’½ [Local Devnet & Starknet.py's Account Capabilities](https://hackmd.io/@sambarnes/By7kitOCt) (***you are here***) * **[Part 5]** 🎨 [StarkNet Frontends w/ Cairopal & Argent X](https://hackmd.io/@sambarnes/HydPlH9CY) * **[Notes]** πŸ’° [Contract Costs & Why Our Design Needs Work](https://hackmd.io/@sambarnes/SkxMZHhRK) *Completed code on GitHub [here](https://github.com/sambarnes/fullstack-starknet)*. If you remember back in part 2, we waited a while for contracts to be deployed & transactions to be processed during our testing. Although the public testnet is nice for retaining state across manual or end to end tests, it's not the best experience while quickly iterating on a project. ## Using a Local Devnet Instead of working with the testnet and it's longer block times, let's use another tool to speed things up. A local [devnet](https://github.com/Shard-Labs/starknet-devnet) server actually comes installed with nile. ``` (venv) sam@sam:~/fullstack-starknet/part4/starknet$ nile node * Running on http://localhost:5000/ (Press CTRL+C to quit) ``` This will be our own sandbox to build and transact in. Let's also create a `.env` file to keep our private key & other configuration in: ``` #!/bin/bash export NETWORK=localhost export VEHICLE_ID=1 export SIGNER_SECRET=12345 export SIGNER_ADDRESS= export CONTRACT_ADDRESS= ``` > πŸ’‘ NOTE: running `source .env` will bring these variables into scope for your session Turns out, nile can also interact with our devnet. To deploy a new account contract, we can run the following: ``` (venv) sam@sam:~/fullstack-starknet/part4/starknet$ nile setup SIGNER_SECRET πŸš€ Deploying Account ⏳ ️Deployment of Account successfully sent at 0x020f85039a06ae18f071c8f781613e5461cdea07b9a3ff87c33d2e6f525e5b45 🧾 Transaction hash: 0x02bd1b29252d83c83daa87f583f11709174814bb63099379dc966d9fba654d12 πŸ“¦ Registering deployment as account-0 in localhost.deployments.txt ``` The `setup` command reads the private key `SIGNER_SECRET` from the environment variable we exported earlier -- `$SIGNER_SECRET` -- and deploys an account around that private key. To compile & deploy our newest blackbox contract: ``` (venv) sam@sam:~/fullstack-starknet/part4/starknet$ nile compile πŸ€– Compiling all Cairo contracts in the contracts directory πŸ”¨ Compiling contracts/IAccount.cairo πŸ”¨ Compiling contracts/ERC165_base.cairo πŸ”¨ Compiling contracts/Initializable.cairo πŸ”¨ Compiling contracts/Account.cairo πŸ”¨ Compiling contracts/contract.cairo πŸ”¨ Compiling contracts/utils/safemath.cairo πŸ”¨ Compiling contracts/utils/constants.cairo βœ… Done # Targetting localhost and assigning a contract alias to our deployment (venv) sam@sam:~/fullstack-starknet/part4/starknet$ nile deploy --network=localhost contract --alias blackbox πŸš€ Deploying contract ⏳ ️Deployment of contract successfully sent at 0x019ffaa269233a233a238ff00fe74ef377aa2b34688591e5115162485f57a86f 🧾 Transaction hash: 0x008a0170df3e1f8e3a7787c7c797e6be814e8afad0ebd70d3b578c49f8b512c9 πŸ“¦ Registering deployment as blackbox in localhost.deployments.txt ``` We'll take the deployment addresses in the output, and update the `.env` file accordingly: ```shell # ...previous stuff export SIGNER_ADDRESS=0x020f85039a06ae18f071c8f781613e5461cdea07b9a3ff87c33d2e6f525e5b45 export CONTRACT_ADDRESS=0x019ffaa269233a233a238ff00fe74ef377aa2b34688591e5115162485f57a86f ``` We've now: - created a local development network - deployed a single account that will be our vehicle owner & signer (for simplicity) - deployed our blackbox contract Time to refactor our registration and commitment code in the application server ## Application Server Config Before we get to that, a quick housekeeping item. A new module `app/config.py` will help us easily load our environment variables without it being hardcoded into the app: ```python= import pydantic class Config(pydantic.BaseSettings): contract_address: str = pydantic.Field( default=..., description="The hex address of the blackbox contract", ) vehicle_id: int = pydantic.Field( default=1, description="The vehicle ID to signing for", ) signer_secret: int = pydantic.Field( default=..., description="The private key to sign with", ) signer_address: str = pydantic.Field( default=..., description="The hex address of the account contract for the signer", ) ``` [Pydantic](https://pydantic-docs.helpmanual.io/) will automatically read & convert types for the environment variables listed in that class (case insensitive). This will just make it easier to keep our private keys and other configuration out of version control. ## Accounts with Starknet.py Alright, back to the part of the API that interacts with StarkNet. Previously, our application had to construct the calldata, keep track of a nonce, hash, and sign it all. Sizable chunk of code. For what should be a super common task. Luckily, the talented folks at [software-mansion](https://github.com/software-mansion/starknet.py) provide first class support for the OpenZeppelin Account contract. So by using standard account contracts over in our cairo project, we can immediately leverage some powerful helpers on the python side. ```python= import pydantic from fastapi import FastAPI, HTTPException from starknet_py.contract import Contract from starknet_py.net.client import Client from starknet_py.net.account.account_client import AccountClient, KeyPair from starkware.python.utils import from_bytes from app.config import Config API_VERSION = "0.0.1" app = FastAPI() # Load env vars into config class config = Config() # Create a generic starknet client that connects to our local devnet network = "http://localhost:5000" chain_id = from_bytes(b"SN_LOCALHOST") starknet_client = Client(net=network, chain=chain_id) account_client = AccountClient( address=config.signer_address, key_pair=KeyPair.from_private_key(config.signer_secret), net=network, chain=chain_id, ) @app.get(path="/api") def api_info(): return {"version": API_VERSION, **config.dict(exclude={"signer_secret"})} @app.post(path="/api/register") async def register_vehicle(): """ Registers the car on chain, with identical owner and signer. Returns the transaction hash. """ # Contract constructor passes in the account_client. # This allows proxying transactions through the account, to the target contract. contract = await Contract.from_address( address=config.contract_address, client=account_client, ) # Reads can still be processed immediately, without an AccountClient (owner,) = await contract.functions["get_owner"].call(config.vehicle_id) if owner != 0: raise HTTPException(status_code=422, detail="Vehicle already registered") # For writes, the AccountClient allows us to modify the behavior of any `.invoke()` # such that the transaction is: # * signed by the account's private key # * proxied through the account's contract # * and finally executed at the targeted blackbox contract invocation = await contract.functions["register_vehicle"].invoke( vehicle_id=config.vehicle_id, owner_address=int(config.signer_address, 16), signer_address=int(config.signer_address, 16), ) await invocation.wait_for_acceptance() return {"tx_hash": invocation.hash} ``` In a new terminal window, within the pi application directory, we can test this out by running the application server: ``` sam@sam:~/fullstack-starknet/part4/pi$ poetry run task dev INFO: Will watch for changes in these directories: ['/home/sam/fullstack-starknet/part4/pi'] INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) INFO: Started reloader process [9152] using statreload INFO: Started server process [9154] INFO: Waiting for application startup. INFO: Application startup complete. ``` Then back in our old terminal session for the contract, send a request to register our vehicle: ``` (venv) sam@sam:~/fullstack-starknet/part4/starknet$ http POST localhost:8000/api/register HTTP/1.1 200 OK { "tx_hash": "0x00ba970426cbaf455b9ed0b4158b4333d07637c1cff05789fb625a306656491a" } ``` Looks like it went through! Using nile, we can veify the transaction was processed by getting the current owner of the vehicle: ``` (venv) sam@sam:~/fullstack-starknet/part4/starknet$ nile call blackbox get_owner 1 0x20f85039a06ae18f071c8f781613e5461cdea07b9a3ff87c33d2e6f525e5b45 ``` ...it's our account contract's address! > ✨ Exercise: Using the AccountClient pattern observed in the registration logic, are you able to refactor the bits that call `attest_state` in the `POST /api/commit` endpoint? *(answer hidden below)* :::spoiler Something along these lines would work well enough. Now letting us separate smart contract interactions and state_hash calculation (i.e. reading from OBD2, formatting & storing data, etc... the boring parts πŸ˜„). ```python class CommitRequest(pydantic.BaseModel): state_id: int state_hash: int @app.post(path="/api/commit") async def commit(req: CommitRequest): """Commits a single state attestation on chain, using the most recent window of data.""" contract = await Contract.from_address( address=config.contract_address, client=account_client, ) invocation = await contract.functions["attest_state"].invoke( vehicle_id=config.vehicle_id, state_id=req.state_id, state_hash=req.state_hash, ) await invocation.wait_for_acceptance() return {"tx_hash": invocation.hash} ``` ::: --- If you've implemented it correctly, hitting the route should now return something like this after creating the transaction. ``` (venv) sam@sam:~/fullstack-starknet/part4/starknet$ http POST localhost:8000/api/commit state_id=1 state_hash=424242 HTTP/1.1 200 OK { "tx_hash": "0x0780adf2461b522fd2a30abebe784d05e84b9caf1e2709283322c964c1616382" } ``` Subsequently, we can verify by reading back the state in the CLI. For car 1, at state_id 1, we see our state was updated correctly: ``` (venv) sam@sam:~/fullstack-starknet/part4/starknet$ nile call blackbox get_state 1 1 424242 ``` https://www.youtube.com/watch?v=1Bix44C1EzY πŸŽŠπŸŽ‰