Full-Stack Starknet:
Completed code on GitHub here.
We've built the cairo contracts and a raspberry pi server that will attest to a vehicle state periodically. Ideally though, a user shouldn't need to issue commands on the pi to register a vehicle or rotate the signing account. They should instead get a more modern experience: a webapp that connects to their Argent X browser extension (current leading wallet provider).
Not a huge fan of frontend work personally. Just never caught the JS bug 🤷 (I'm not bitter about it, I swear)
Fortunately, a few community members have crafted cairopal – a batteries-included frontend template for dapps on StarkNet. Thanks to their heavy lifting (seriously, thank you 🙌🙏) we have a massive head start in making a functional frontend with a smooth and clean UI.
💡 If you're coming here just looking for React library to use, check out starknet-react from Francesco Ceccon. The template we'll use here is based on an early version of that work. Though it may be ported in the near future to use startknet-react now that it's an official package.
We'll use this head start to quickly create a fontend that:
Clone cairopal to be the base of our project:
sam@sam:~/fullstack-starknet/part5$ git clone https://github.com/abigger87/cairopal frontend
sam@sam:~/fullstack-starknet/part5$ cd frontend
Install the dependencies with a yarn install
and start it with a yarn dev
– then we'll be able to see the site at http://localhost:3000
Looks pretty sleek without needing to modify much. Right out of the box, we can connect our account in the Argent X browser extension.
Two foundational components are included as well:
Between these, we should be able to re-use the same underlying mechanisms to build out our frontend.
But before we move on, remember back to Part 4 and try to setup our dev session.
✨ Exercise: Start a local devnet and deploy our contract. Then, use the Argent X wallet to deploy a new account on that same local network.
First, a nile node
to bring up the local network:
(venv) sam@sam:~/fullstack-starknet/part4/starknet$ nile node
* Running on http://localhost:5000/ (Press CTRL+C to quit)
Then, a compile & deploy:
(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
# NOTE: you may need to `rm localhost.deployments.txt` first to clear current aliases
(venv) sam@sam:~/fullstack-starknet/part4/starknet$ nile deploy --network=localhost contract --alias blackbox
🚀 Deploying contract
⏳ ️Deployment of contract successfully sent at 0x04f2d8ea9774229a040924c37b12a9244bae7451000502612340488e659206f2
🧾 Transaction hash: 0x01cdff319cdfaafa3caf419f62e02c9571e314f487401240b1e9d755aa3783c9
📦 Registering deployment as blackbox in localhost.deployments.txt
To verify it was deployed, we can check with the CLI:
(venv) sam@sam:~/fullstack-starknet/part4/starknet$ starknet tx_status --hash 0x01cdff319cdfaafa3caf419f62e02c9571e314f487401240b1e9d755aa3783c9 --feeder_gateway_url http://localhost:5000
{
"block_hash": "0x039ed1060ae667d44c93ea7f3f0c2de409768f13299eca7ccdc9cbbb25ef0c13",
"tx_status": "ACCEPTED_ON_L2"
}
💡 Tip: you can use this same command to display error messages from rejected transactions
Finally, open the extension & select the localhost network. Clicking the + button will deploy to our local network.
NOTE: If you want to skip local devnet and just work against testnet, use this goerli contract I deployed: 0x0220d51c9f7ad564c4cc2fecfeeebb35f18646329c8f5cfcc5b6ec00cf31cbc0
You'll still need your own argent account deployed to goerli.
As we saw earlier, there's a Transactions List view and a Mint Tokens button to build off of. If ya go to the <MintTokens>
component, we can rename this file (and it's references) to RegisterVehicle
. That will be the only responsibility of this component.
The first obvious change is to set the contract address to what we've deployed.
const CONTRACT_ADDRESS =
"0x04f2d8ea9774229a040924c37b12a9244bae7451000502612340488e659206f2";
We're also going to need access to the currently connected account address. We can bring it into scope by modifying this line:
const { connected, library, account } = useStarknet();
Then head down to the mintTokens
function, where the contract interaction happens.
Let's modify it to match the function signature we want to hit (func register_vehicle(vehicle_id : felt, signer_address : felt):
). Overall it should feel pretty similar to how we've called our contracts from python. Just a slightly different layout.
const registerVehicle = async () => {
// The account address is a large hex value, as a string. We need it as a felt.
// A `parseInt(account, 16).toString()` might seem like it would work at first,
// but it actually prints in scientific notation. A BigInt can show all digits.
const account_big = BigInt(account!);
const account_address_param = account_big.toString(10);;
const registerVehicleResponse = await library.addTransaction({
type: "INVOKE_FUNCTION",
contract_address: CONTRACT_ADDRESS,
entry_point_selector: selector,
calldata: [
"42", // vehicle_id
account_address_param, // signer address (same as owner for simplicity)
],
});
// eslint-disable-next-line no-console
console.log(registerVehicleResponse);
};
Heading back to our refreshed site, we should be able to try it out:
Once sent & accepted, we can use nile to check if the owner of vehicle 42 was updated:
(venv) sam@sam:~/fullstack-starknet/part4/starknet$ nile call blackbox get_owner 42
0x466d9a1ecaab4d0e6cf9f791205cc82f05d1f5c77474314749f10cc2bcbc796
If you were pointing your frontend at the testnet contract I mentioned earlier, you won't be able to see the transaction in the contract's explorer page (at least, not at time of writing). Rather, you'll see it show up on your account's contract page in the explorer.
You can however query the testnet contract to see if the transaction updated the state:
✨ Exercise: Can you figure out how to accept Vehicle ID using an input box?
Never heard of chakra ui
before, but a quick google came up with an Input component.
Using the docs I was able to come up with a quick and dirty implementation:
const [value, setValue] = React.useState('');
const handleChange = (event: any) => setValue(event.target.value);
return (
<Box>
<Text as="h2" marginTop={4} fontSize="2xl">
Register Vehicle
</Text>
<Box d="flex" flexDirection="column">
<Code marginTop={4} w="fit-content">
contract:
{/* {`${CONTRACT_ADDRESS.substring(0, 4)}...${CONTRACT_ADDRESS.substring(
CONTRACT_ADDRESS.length - 4
)}`} */}
<Link
isExternal
textDecoration="none !important"
outline="none !important"
boxShadow="none !important"
href={`https://voyager.online/contract/${CONTRACT_ADDRESS}`}
>
{CONTRACT_ADDRESS}
</Link>
</Code>
// Show an input box that calls an event handler on change
{connected && (
<Input
my={4}
variant="flushed"
placeholder="Vehicle ID"
onChange={handleChange}
/>
)}
{connected && (
<Button
my={4}
w="fit-content"
onClick={() => {
registerVehicle(value);
}}
>
Register Vehicle
</Button>
)}
{!connected && (
<Box
backgroundColor={colorMode === "light" ? "gray.200" : "gray.500"}
padding={4}
marginTop={4}
borderRadius={4}
>
<Box fontSize={textSize}>
Connect your wallet to register a vehicle.
</Box>
</Box>
)}
</Box>
</Box>
);
It doesn't do any input validation like we'd want in prod, but good enough to demo. Try registering some arbitrary integer like 5
, then checking the owner.
Reading from contract state shows us it went through!
(venv) sam@sam:~/fullstack-starknet/part4/starknet$ nile call blackbox get_owner 5
0x466d9a1ecaab4d0e6cf9f791205cc82f05d1f5c77474314749f10cc2bcbc796
So the frontend can be used to register a vehicle, and the rapberry pi can sign & commit state attestations on chain using the registered account. Though, we still need a way to send a set_signer()
transaction to let the owner change what account has the signing rights.
Doesn't really make much sense to put that logic in the onboard raspberry pi. Instead, it should be a new React component on the frontend of our management dashboard.
Some quick copypasta from the last component brings us to this starter template:
import {
Box,
Button,
Input,
Text,
useBreakpointValue,
useColorMode,
} from "@chakra-ui/react";
import { stark } from "starknet";
import { CONTRACT_ADDRESS } from "./consts";
import { useStarknet } from "context";
import React from "react";
const UpdateSigner = () => {
const { connected, library, account } = useStarknet();
const { colorMode } = useColorMode();
const textSize = useBreakpointValue({
base: "xs",
sm: "md",
});
// TODO: implement the contract interaction
const updateSigner = async (vehicleId: string, newSignerAddress: string) => {
};
return (
<Box>
<Text as="h2" marginTop={4} fontSize="2xl">
Update Signing Authority
</Text>
<Text marginTop={4}>
Vehicle signing key lost? Compromised and not able to be updated in the account?
<br />
Use your vehicle owner's account to authorize a different account to sign state commitments.
</Text>
{connected && (
<Box d="flex" flexDirection="column">
// TODO: add the frontend inputs here
</Box>
)}
{!connected && (
<Box d="flex" flexDirection="column">
<Box
backgroundColor={colorMode === "light" ? "gray.200" : "gray.500"}
padding={4}
marginTop={4}
borderRadius={4}
>
<Box fontSize={textSize}>
Connect your wallet to change the authorized signer account.
</Box>
</Box>
</Box>
)}
</Box>
);
};
export default UpdateSigner;
✨ Exercise: Using the building blocks from the last section, add a
UpdateSigner
component belowRegisterVehicle
. It should take in a vehicle ID and a new signer account address as input, and invoke ourset_signer(vehicle_id, signer_address)
function.
import {
Box,
Button,
Input,
Text,
useBreakpointValue,
useColorMode,
} from "@chakra-ui/react";
import { stark } from "starknet";
import { CONTRACT_ADDRESS } from "./consts";
import { useStarknet } from "context";
import React from "react";
const UpdateSigner = () => {
const { connected, library, account } = useStarknet();
const { colorMode } = useColorMode();
const textSize = useBreakpointValue({
base: "xs",
sm: "md",
});
const { getSelectorFromName } = stark;
const selector = getSelectorFromName("set_signer");
// Similar to last time, we just invoke a different function
const updateSigner = async (vehicleId: string, newSignerAddress: string) => {
const accountAddressParam = BigInt(newSignerAddress!).toString(10);
const setSignerResponse = await library.addTransaction({
type: "INVOKE_FUNCTION",
contract_address: CONTRACT_ADDRESS,
entry_point_selector: selector,
calldata: [
vehicleId,
accountAddressParam,
],
});
// eslint-disable-next-line no-console
console.log(setSignerResponse);
};
// A couple state variables for our inputs & their setters.
const [vehicleId, setVehicleId] = React.useState('');
const [vehicleSignerAddress, setSignerAddress] = React.useState('');
const handleVehicleIdChange = (event: any) => setVehicleId(event.target.value);
const handleSignerAddressChange = (event: any) => setSignerAddress(event.target.value);
return (
<Box>
<Text as="h2" marginTop={4} fontSize="2xl">
Update Signing Authority
</Text>
<Text marginTop={4}>
Vehicle signing key lost? Compromised and not able to be updated in the account?
<br />
Use your vehicle owner's account to authorize a different account to sign state commitments.
</Text>
{connected && (
<Box d="flex" flexDirection="column">
// Same input types, just more of em
<Input
my={4}
placeholder="Vehicle ID"
onChange={handleVehicleIdChange}
/>
<Input
my={4}
placeholder="Signer Account Address (0x...)"
onChange={handleSignerAddressChange}
/>
<Button
my={4}
w="fit-content"
onClick={() => {
updateSigner(vehicleId, vehicleSignerAddress);
}}
>
Update
</Button>
</Box>
)}
{!connected && (
<Box d="flex" flexDirection="column">
<Box
backgroundColor={colorMode === "light" ? "gray.200" : "gray.500"}
padding={4}
marginTop={4}
borderRadius={4}
>
<Box fontSize={textSize}>
Connect your wallet to change the authorized signer account.
</Box>
</Box>
</Box>
)}
</Box>
);
};
export default UpdateSigner;
You may have noticed one thing that didn't work right away – the Transaction List. I'm uhh still trying to figure that one out myself 😅.
I was able to take a stab at porting cairopal to use starknet-react, in this PR. Though since you may read this before that change goes in, you can check out the starknet-react demo site in the meantime. The transaction manager is used here:
https://github.com/auclantis/starknet-react/blob/main/website/src/components/Demo.tsx#L146-L178