DEV Community

Cover image for Developing a Full-Stack Project on Stacks with Clarity Smart Contracts and Stacks.js Part III: Frontend
CiaraMaria
CiaraMaria

Posted on • Edited on

Developing a Full-Stack Project on Stacks with Clarity Smart Contracts and Stacks.js Part III: Frontend

Frontend

In the terminal from the root gm directory run:

cd frontend
Enter fullscreen mode Exit fullscreen mode
npm install
Enter fullscreen mode Exit fullscreen mode

Before we start writing code, let's take a quick look at the features we'll be using from Stacks.js.

Stacks.js is a full featured JavaScript library for dApps on Stacks.

@stacks/connect allows devs to connect web applications to Stacks wallet browser extensions.

@stacks/transactions allows for interactions with the smart contract functions and post conditions.

@stacks/network is a network and API library for working with Stacks blockchain nodes.

In your index.js file, replace the boilerplate code with:

import Head from "next/head";
import ConnectWallet from "../components/ConnectWallet";
import styles from "../styles/Home.module.css";

export default function Home() {
  return (
    <div className={styles.container}>
      <Head>
        <title>gm</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <h1 className={styles.title}>gm</h1>

        <div className={styles.components}>
          {/* ConnectWallet file: `../components/ConnectWallet.js` */}
          <ConnectWallet />
        </div>
      </main>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now run npm start and navigate to localhost:3000.

Image description

We have a super simple landing page with Hiro wallet login. We won't focus on styling for this example.

Looking at the code you'll see the ConnectWallet component has been created already. This is included as part of the Stacks.js Starters.

Let's go into the components directory. Here we see two files:

  • ConnectWallet.js
  • ContractCallVote.js

If you're curious about ContractCallVote you can add the component to index.js and try it out. For this example we won't be using it.

ConnectWallet.js contains an authenticate function that creates a userSession. This will give us the information we need to send to our contract after a user signs in.

Inside this file comment out these lines:

<p>mainnet: {userSession.loadUserData().profile.stxAddress.mainnet}</p>
<p>testnet: {userSession.loadUserData().profile.stxAddress.testnet}</p>
Enter fullscreen mode Exit fullscreen mode

This doesn't affect functionality, we just don't want the addresses on the page.

Now, we are going to create a new component.

Inside the component directory create a file called ContractCallGm.js

import { useCallback, useEffect, useState } from "react";
import { useConnect } from "@stacks/connect-react";
import { StacksMocknet } from "@stacks/network";
import styles from "../styles/Home.module.css";

import {
  AnchorMode,
  standardPrincipalCV,
  callReadOnlyFunction,
  makeStandardSTXPostCondition,
  FungibleConditionCode
} from "@stacks/transactions";
import { userSession } from "./ConnectWallet";
import useInterval from "@use-it/interval";

const ContractCallGm = () => {
  const { doContractCall } = useConnect();
  const [ post, setPost ] = useState("");
  const [ hasPosted, setHasPosted ] = useState(false);

  function handleGm() {
    const postConditionAddress = userSession.loadUserData().profile.stxAddress.testnet;
    const postConditionCode = FungibleConditionCode.LessEqual;
    const postConditionAmount = 1 * 1000000;
    doContractCall({
      network: new StacksMocknet(),
      anchorMode: AnchorMode.Any,
      contractAddress: "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM",
      contractName: "gm",
      functionName: "say-gm",
      functionArgs: [],
      postConditions: [
        makeStandardSTXPostCondition(
          postConditionAddress,
          postConditionCode,
          postConditionAmount
        )
      ],
      onFinish: (data) => {
        console.log("onFinish:", data);
        console.log("Explorer:", `localhost:8000/txid/${data.txId}?chain=testnet`)
      },
      onCancel: () => {
        console.log("onCancel:", "Transaction was canceled");
      },
    });
  }

  const getGm = useCallback(async () => {

    if (userSession.isUserSignedIn()) {
      const userAddress = userSession.loadUserData().profile.stxAddress.testnet
      const options = {
          contractAddress: "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM",
          contractName: "gm",
          functionName: "get-gm",
          network: new StacksMocknet(),
          functionArgs: [standardPrincipalCV(userAddress)],
          senderAddress: userAddress
      };

      const result = await callReadOnlyFunction(options);
      console.log(result);
      if (result.value) {
        setHasPosted(true)
        setPost(result.value.data)
      }
    }
  });

  useEffect(() => {
    getGm();
  }, [userSession.isUserSignedIn()])

  useInterval(getGm, 10000);

  if (!userSession.isUserSignedIn()) {
    return null;
  }

  return (
    <div>
      {!hasPosted &&
        <div>
          <h1 className={styles.title}>Say gm to everyone on Stacks! 👋</h1>
          <button className="Vote" onClick={() => handleGm()}>
            gm
          </button>
        </div>  
      }
      {hasPosted && 
        <div>
          <h1>{userSession.loadUserData().profile.stxAddress.testnet} says {post}!</h1>
        </div>
      }
    </div>
  ); 
};

export default ContractCallGm;
Enter fullscreen mode Exit fullscreen mode

There are a few things going on here so let's break it down.

The first function handleGm is where the bulk of the work is being done.

  function handleGm() {
    const postConditionAddress = userSession.loadUserData().profile.stxAddress.testnet;
    const postConditionCode = FungibleConditionCode.LessEqual;
    const postConditionAmount = 1 * 1000000;
    doContractCall({
      network: new StacksMocknet(),
      anchorMode: AnchorMode.Any,
      contractAddress: "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM",
      contractName: "gm",
      functionName: "say-gm",
      functionArgs: [],
      postConditions: [
        makeStandardSTXPostCondition(
          postConditionAddress,
          postConditionCode,
          postConditionAmount
        )
      ],
      onFinish: (data) => {
        console.log("onFinish:", data);
        console.log("Explorer:", `localhost:8000/txid/${data.txId}?chain=testnet`)
      },
      onCancel: () => {
        console.log("onCancel:", "Transaction was canceled");
      },
    });
  }
Enter fullscreen mode Exit fullscreen mode

This function will execute on click of a button.

The first portion of this function is making the actual contract call to our say-gm function via doContractCall.

We pass to it the required options:

  1. network: this is telling doContractCall what network to use to broadcast the function. There is mainnet, testnest, and devnet. We will be working with devnet and the network config for that is new StacksMocknet().
  2. anchorMode: this specifies whether the tx should be included in an anchor block or a microblock. In our case, it doesn't matter which.
  3. contractAddress: this is the standard principal that deploys the contract (notice it is the same as the one provided in Devnet.toml).
  4. functionName: this is the function you want to call
  5. functionArgs: any parameters required for the function being called.
  6. postConditions: Post conditions are a feature unique to Clarity which allow a developer to set conditions which must be met for a transaction to complete execution and materialize to the chain.

If you want to learn more about Post Conditions, check out this great post.

We are using a standard STX post condition which is saying that the user will transfer less than or equal to 1 STX or the transaction will abort.

Upon successful broadcast, onFinish gets executed. We are simply logging some data.

Now let's take a look at the next function:

const getGm = useCallback(async () => {

    if (userSession.isUserSignedIn()) {
      const userAddress = userSession.loadUserData().profile.stxAddress.testnet
      const options = {
          contractAddress: "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM",
          contractName: "gm",
          functionName: "get-gm",
          network: new StacksMocknet(),
          functionArgs: [standardPrincipalCV(userAddress)],
          senderAddress: userAddress
      };

      const result = await callReadOnlyFunction(options);
      console.log(result);
      if (result.value) {
        setHasPosted(true)
        setPost(result.value.data)
      }
    }
  });
Enter fullscreen mode Exit fullscreen mode

This is making a call to our read-only function get-gm. You'll notice the formatting is slightly different, but the required options are the same ones we just discussed.

The main difference is that this is a callback function which we will call on an interval to check whether the user has called say-gm. Remember, get-gm will return none unless say-gm has successfully executed.

That's really all there is to it! The rest of the code is just React + JSX. Make sure you include this component in index.js and you're ready to move on to full interaction!

Boot up DevNet and once it's running, you can start your frontend app with npm start and navigate to localhost:3000.

Click Connect Wallet to login.

This will open a pop-up with your web wallet accounts.

Image description

Notice that it shows Devnet in the top right corner and the account that you configured to DevNet earlier now has a balance of 100M STX. This is your test faucet from the Devnet.toml file.

After logging in, your page should update and look like this:

Image description

Let's make a call to our say-gm function by clicking the purple button! Open your console in browser so you can see the logs.

Image description

You'll get the above tx request. It shows the postCondition we defined, the function being called, and fees.

Upon confirm, if everything went well you should see something like this in the browser console:

Image description

The onFinish and Explorer logs are coming from the handleGm function. The {type:} is coming from the getGm callback. As you can see, it pinged every 10 seconds and it took just less than a minute for our tx call to broadcast and reflect.

Once that result comes in, the page should update to reflect that UserPost has updated on chain to map your address with the string "gm".

Image description

There it is! A front to back Stacks application from scratch.

One last fun feature to explore: copy the URL from the Explorer log and paste it in your browser. This will give you a visual of what the transaction call would look like on the Stacks Explorer.

I do hope this has been helpful in understanding the fundamentals of creating a full-stack project on Stacks from developing and testing the smart contracts to setting up a site that users can use to make on-chain calls and simulating what that might look like. Let me know what you think! 🚀

Shameless plug: find me on Twitter @themariaverse

Top comments (1)

Collapse
 
raphabtc profile image
Rapha

Excellent post! TY the MariaVerse!