Back to Blog

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

This is Part 3 of 3 of the Series “Developing a Full-Stack Project on the Stacks Blockchain with Clarity Smart Contracts and Stacks.js”. If you haven’t already, start at the beginning with Part 1: Intro and Project Setup.

Frontend

In the terminal from the root gm directory run:

cd frontend
npm install

Before starting to write code, let’s take a quick look at the library being used.

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>
  );
}

Now run npm start and navigate to localhost:3000.

Screenshot of the web page at localhost 3000. It has header text that reads "gm" and a button to Connect wallet.

You’ll see a super simple landing page with Hiro wallet login. I 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:

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 gives the account information required to send to the 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>

This doesn’t affect functionality, I just don’t want the addresses on the page.

Now, I am going to create a new component by creating a file called ContractCallGm.js inside the component directory.

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;

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");
      },
    });
  }

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.

I am 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. It is 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)
      }
    }
  });

This is making a call to the 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 gets called 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 straightforward React + JSX. Make sure to include this component in index.js.

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.

Screenshot of the Hiro Connect Wallet screen. The page includes the Hiro Wallet version, Devnet in the top right corner, and the account that you configured to DevNet earlier in the process.

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, the page should update and look like this:

After logging in to the Hiro Wallet, the web page should display "gm" with the option to "Disconnect Wallet", a prompt "Say gm to everyone on Stacks!" with a small purple button labeled "gm" that allows the user to call the stay-gm function.

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

Screen shot of the tx request returned in the console by making a call  to the say-gm function. It Includes the Account you've sent the command from, the postCondition defined, that the say-gm fuction is being called and a fee of 0.0732 stx. A large purple button allows the user to "confirm".

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

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

Image of the logs in the browser console after confirming the tx request above. It includes the onFinish and Explorer logs, along with timestamps.

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. This is a big benefit to using DevNet during development, it is not restricted to the true block mining time like Testnet is.

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”.

The web page after a successful call to say-gm. It displays the GM header, a button to disconnect and a message that shows "your-specific-address says gm!"

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

Conclusion

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 us know what you think!

We have more blog posts if you're interested.
How can a Subdomain Boost my Business?
Interact with Stacks blockchain on Testnet
Developing a Full-Stack Project on the Stacks Blockchain with Clarity Smart Contracts and Stacks.js Part II: Backend