Frontend
In the terminal from the root gm
directory run:
cd frontend
npm install
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>
);
}
Now run npm start
and navigate to localhost:3000
.
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>
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;
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 call -
functionArgs
: 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.
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)
}
}
});
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.
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:
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.
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:
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".
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)
Excellent post! TY the MariaVerse!