# 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 ππ