Try   HackMD

StarkNet Frontends w/ Cairopal & Argent X

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:

  • connects to the Argent X extension
  • sends transactions through a user's account in that extension
  • registers vehicles with the system
  • enables changing the vehicle signing authority

Getting started

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

template

Looks pretty sleek without needing to modify much. Right out of the box, we can connect our account in the Argent X browser extension.

uses argent

Two foundational components are included as well:

  • Transaction List - a view into a user's past actions with our contract
  • Mint Tokens Button - a way to invoke contract functions, automatically proxied through our connected account contract

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.

Add Account


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.

Registration Button

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:

Sign Vehicle Registration

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:
testnet explorer query

✨ 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

Updating the signer

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 below RegisterVehicle. It should take in a vehicle ID and a new signer account address as input, and invoke our set_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;

update component

Transaction List

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