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
.
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:
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 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:
network
: this is tellingdoContractCall
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 isnew StacksMocknet()
.anchorMode
: this specifies whether the tx should be included in an anchor block or a microblock. In our case, it doesn’t matter which.contractAddress
: this is the standard principal that deploys the contract (notice it is the same as the one provided in Devnet.toml).functionName
: this is the function you want to callfunctionArgs
: any parameters required for the function being called.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.
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:
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.
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:
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”.
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!