Build a web3 application on Stacks

Develop a Web3 Application to interact with our Voting Smart Contract from the browser

  1. Set up the web app and authenticate with a wallet
  2. Call the voting smart contract
  3. Send transactions to the smart contract
  4. Finalize the web3 app

Finalize the web3 app

There will be fundamentally nothing new in this article. Take it as an exercise to practice and review what you previously learned.

Add a bit of Clarity

I hope you didn't forget how to write Clarity code. Go back to the smart contract and add a function called get-sender-vote. This function will tell if the sender already voted and the values of its vote.
It's a one-line read-only function that returns none or (some [<value>]). Even though it's simple, I encourage you to write it yourself since you often need to tweak a contract while building its client app.

Don't forget to add tests for this new function.

πŸ’‘ get-sender-vote function

./contracts/color-vote.clar

(define-read-only (get-sender-vote) (map-get? votes tx-sender))

Get the sender's vote

That's it for the Clarity part. You can go back to that web app code and we will now make use of this new function. In the color vote store, add a function fetchVote() that will call get-sender-vote. If there is a vote, save it in the vote property.
The trick here is to handle the BigInt values that will be returned by the contract. Our store expects Numbers and we want to keep it that way to easily set the values of the vote inputs. BigInts can easily be converted into Numbers with parseInt.

Again, try and implement it by yourself as an exercise.

In my implementation, I changed the getInitialVote function into getVoteMap which accepts optional values to return in the map instead of undefined values.

Solution: fetchVote

./src/stores/useColorVote.ts

const getVoteMap = (values) =>
  new Map(ids.map((id, i) => [id, values ? values[i] : undefined]))

export const useColorVote = create<ColorStore>((set, get) => ({
  vote: getVoteMap(),
  alreadyVoted: false,
  // ...

  async fetchVote() {
    const rawVote = await readOnlyRequest('get-sender-vote')

    const vote = cvToTrueValue(rawVote).map((v) => parseInt(v))
    set({ vote: getVoteMap(voteAsNbs), alreadyVoted: true })
  },
Typesafe version of fetchVote

./src/stores/useColorVote.ts

type ValidVote = [ValidValue, ValidValue, ValidValue, ValidValue]

function isPreviousVoteValid(vote: unknown): vote is ValidVote {
  if (!Array.isArray(vote) || vote.length !== 4) return false
  return vote.reduce((acc, v) => isValueValid(v) && acc, true)
}

export const useColorVote = create<ColorStore>((set, get) => ({
  // ...

  async fetchVote() {
    const rawVote = await readOnlyRequest('get-sender-vote')
    if (!rawVote) return

    const vote = cvToTrueValue(rawVote)
    if (!vote || !Array.isArray(vote)) return

    const voteAsNbs = vote.map((v) => parseInt(v))
    if (!isPreviousVoteValid(voteAsNbs)) return
    set({ vote: getVoteMap(voteAsNbs), alreadyVoted: true })
  },

Call fetchVote

This new function will be called the same way as fetchColors, as early as possible in App.tsx.

./src/App.tsx

export function App() {
  const { session } = useAuth()

  useEffect(() => {
    if (session) {
      const { fetchVote, fetchColors } = useColorVote.getState()
      fetchVote()
      fetchColors()
    }
  }, [session])
  // ...

Update or cancel a vote

Our app is now aware of whether the person already voted or not. This information is useful to know if revote should be called instead of vote or if unvote can be called.

πŸ‘‰ Exercise: edit useColorVote to:

  • Call revote in sendVote if the user already voted
  • Add an unvote function

Note that in all cases (vote, revote and unvote), you want to save the tx ID so that you can display the status of the last transaction.

πŸ’‘ sendVote and unvote functions

./src/stores/useColorVote.ts

  // since we now have two functions that save that tx id
  // I put it in its own function
  saveTx(txId: string) {
    localStorage.setItem('txId', txId)
    set({ txId })
  },

  async sendVote() {
    const { vote, alreadyVoted, saveTx } = get()
    const senderVote = ids.map((id) => vote.get(id))
    if (!senderVote.every(isValueValid)) return

    // update the contract call function
    const txId = await callContract(
      alreadyVoted ? 'revote' : 'vote',
      senderVote.map(uintCV),
    )
    saveTx(txId)
  },

  async unvote() {
    const { alreadyVoted, saveTx } = get()
    if (!alreadyVoted) return

    const txId = await callContract('unvote')
    set({ vote: getVoteMap() })
    saveTx(txId)
  },

πŸ’‘ As always, you may want to have a look at the file on GitHub to easily follow along.

We can update the form's buttons to take into account the changes in the store.

./src/pages/Vote.tsx

  const handleUnvote: JSX.MouseEventHandler<HTMLButtonElement> = (e) => {
    e.preventDefault()
    unvote()
  }

  return (
    // ...
            <div class="mt-6 flex justify-center gap-4">
              {/* display "Revote" if the user already voted */}
              <Button type="submit" disabled={!isValid}>
                {alreadyVoted ? 'Revote' : 'Vote'}
              </Button>

              <Button type="reset">Empty form</Button>

              {/* Allow use to cancel its vote */}
              {alreadyVoted ? (
                <Button type="button" onClick={handleUnvote}>
                  Cancel vote
                </Button>
              ) : null}
            </div>

In the lastVote component, I also added a line to display the function name of the last contract call since it can now have 3 different values.

./src/components/LastVote.tsx

      <p>
        <b>Function</b>:{' '}
        <span className="capitalize">{lastTx.contract_call.function_name}</span>
      </p>

Further improvements ideas:

  • Once the user voted, display the results of the election.
  • The result pages could be implemented with simple routing.
  • If the last tx is pending, fetch it every X seconds to know when it's successful (or if it failed). You don't want to call it too frequently because the API can be overloaded from time to time.
  • Feel free to get in touch with me on GitHub, Twitter, or the Stacks Discord to propose other ideas to add to this list.

You may see these improvements in the latest version of the code on the GitHub repo but I won't cover them in articles.

Conclusion

Here is the end of the first part of this series.
You now master the basics of Clarity and Web3 on Stacks. You can create valuable Smart Contracts and build front-end applications to interact with them.

In the next articles, we will learn concepts such as transferring Stacks on the blockchain and manipulating NFTs. These are keys concepts of Web3 and things will get serious!
We will also go through other important concepts such as deploying smart contracts to the Stacks blockchain.

πŸ’» Read the code on GitHub. The source code of this article is on this branch. There is a PR associated with this article.