Back to homepage

Step-by-step Web3 DApp built with Solidity and Vue.js (part 2)

Published

This is the second part of this series in which I build a basic web3 application step-by-step. Make sure to check part one or part three

Creating the front end

I'll use my web3 template as a starting point. In summary the web3 template is a Vue.js/Typescript project and it already contains a button to identify users with Metamask. You can find it in the following repo and learn more about it in this article.

Web app - smart contract integration

In order to interact with a smart contract from a web application, we need to follow these steps:

  1. Compile smart contract and generate ABI
  2. Authenticate user with Metamask
  3. Create a Web3Provider or Signer using Ethers
  4. Create a reference to the smart contract with Ethers using the contract address, ABI and Web3Provider
  5. Use the contract reference to call the public methods exposed by it.

I explained how to compile the contract in the previous article and I've also explined how to trigger the Metamask aithentication here so we'll just have to focus on the rest.

Reading messages from the contract

Here is the script portion of the view that takes care of all the steps mentioned above to retrieve messages from our smart contract:

<template>
  <!-- Template code goes here -->
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue';
import { useWalletStore } from '@/stores/wallet';
import { ethers } from 'ethers';

interface Message {
  from: string;
  text: string;
  datetime: Date;
}

// Contract ABI
import MessagePortal from '@/artifacts/solidity/contracts/MessagePortal.sol/MessagePortal.json';

export default defineComponent({
  name: 'WaveMe',
  components: {},

  setup() {
    const walletStore = useWalletStore();
    // address of the contract loaded from an environment variable
    const contractAddress = process.env.VUE_APP_MSG_CONTRACT || '';
    // stores all messages
    const allMessages = ref<Message[]>([]);

    const retrieveMessages = async function () {
      allMessages.value = [];

      //@ts-expect-error Window.ethers not TS
      if (typeof window.ethereum !== 'undefined') {
        //@ts-expect-error Window.ethers not TS
        const provider = new ethers.providers.Web3Provider(window.ethereum);
        // Contract reference
        const contract = new ethers.Contract(
          contractAddress,
          MessagePortal.abi,
          provider
        );
        try {
          // call contract public method
          const data = await contract.getAllMessages({});
          console.log('allMessages :>> ', data);
          // loops messages to format the date and add them to the array
          data.forEach((msg: any) => {
            allMessages.value.push({
              from: msg.from,
              text: msg.text,
              datetime: new Date(msg.datetime * 1000),
            });
          });
        } catch (error) {
          console.error(error);
        }
      }
    };

    return {
      allMessages,
      walletStore,
      retrieveMessages,
    };
  },
  mounted() {
    if (this.walletStore.walletData.address != '') {
      console.log('There is a wallet connected!');
      this.retrieveMessages();
    }
  },
  computed: {
    accAvailable() {
      return useWalletStore().walletData;
    },
  },
  watch: {
    accAvailable(newVal, old) {
      console.log(`updating from ${old} to ${newVal}`);
      this.retrieveMessages();
    },
  },
});
</script>

Here are the most important parts:

  • import { ethers } from 'ethers': I'll use ethers to create the Web3Provider and Contract reference.
  • import MessagePortal from '@/artifacts/... : imports the JSON file generated after compiling the smart contract, which contains the ABI
  • const contractAddress = process.env.VUE_APP_MSG_CONTRACT: It's important to keep the contract address in a environment variable so we can easily update it.
  • retrieveMessages(): this method takes care of everything, creates the provider, contract reference and calls the public method of the smart contract
  • data.forEach((msg: any) => {...}: this loops over all messages received from the smart contract to format the date and push each one to the allMessages varible.

Displaying messages

One we have all the messages retrieved from the contract, we need to display them in the component template. For that, we can use a v-for and loop over the allMessages varible to display the message, the wallet address of the person that sent it and the date and time:

<template>
  <div>
    <p class="mt-4 font-bold">All messages 📩</p>
    <ul class="flex flex-col max-w-md my-8 mx-auto">
      <li
        class="p-4 border mt-2 rounded-lg"
        v-for="msg in allMessages"
        :key="msg"
      >
        <div class="text-left">
          <p>
            <span class="font-bold"
              >{{ msg.from.slice(0, 2) }}...{{ msg.from.slice(-4) }}</span
            >
            said:
          </p>
          <blockquote class="italic">{{ msg.text }}</blockquote>
        </div>
        <p class="text-sm mt-4 text-right">posted on {{ msg.datetime }}</p>
        <p></p>
      </li>
    </ul>
  </div>
</template>

And this is how it'll look like:

Messages in web3 app

Storing new messages

In order to store new messages, we need a form with a textarea input like the following:

Message form in web3 app

<template>
  <div class="flex flex-col max-w-md mx-auto space-y-4 mt-8">
    <textarea
      v-model="message"
      :disabled="walletStore.walletData == null"
      cols="30"
      rows="5"
      class="border ring-none text-pink-600 font-medium rounded p-2"
      :class="
        walletStore.walletData == null
          ? 'border-gray-200 text-gray-400'
          : 'border-pink-500 text-pink-600 hover:shadow-lg shadow-sm'
      "
    ></textarea>

    <button
      @click="sendMessage"
      :disabled="walletStore.walletData == null || trxInProgress"
      class="px-4 py-2 mt-8 border font-medium rounded"
      :class="
        walletStore.walletData == null
          ? 'border-gray-200 text-gray-400'
          : 'border-pink-500 text-pink-600 hover:shadow-lg shadow-sm'
      "
    >
      {{ trxInProgress ? `Sending...` : `Send message 👋` }}
    </button>
  </div>
</template>

The sendMessage() method is a little different, as this one actually writes content to the blockchain, which has some cost. For that, we need the user to sign the transaction, which requires a signer. Here how the method to post a new message will look like:

const sendMessage = async function () {
  //@ts-expect-error Window.ethers not TS
  if (typeof window.ethereum !== 'undefined') {
    trxInProgress.value = true;
    //@ts-expect-error Window.ethers not TS
    const provider = new ethers.providers.Web3Provider(window.ethereum);
    // get the account that will pay for the trasaction
    const signer = provider.getSigner();
    // as the operation we're going to do is a transaction,
    // we pass the signer instead of the provider
    const contract = new ethers.Contract(
      contractAddress,
      WavePortal.abi,
      signer
    );
    try {
      const transaction = await contract.sendMessage(message.value, {
        gasLimit: 300000,
      });

      console.log('transaction :>> ', transaction);
      // wait for the transaction to actually settle in the blockchain
      await transaction.wait();
      message.value = '';
      trxInProgress.value = false;

      //@ts-expect-error no types
      this.getMessages();
    } catch (error) {
      console.error(error);
      trxInProgress.value = false;
    }
  }
};

The most important parts are:

  • const provider = new ethers.providers.Web3Provider(...): using ethers we get a provider to interact with our smart contract
  • const signer = provider.getSigner(): with the provider, we obtain a signer which we'll use to sign the transaction
  • const contract = new ethers.Contract(): this time we pass the signer to the ethers contract constructor
  • const transaction = await contract.sendMessage(...): create the transaction to send the message.

When the user clicks the send message button, Metamask will prompt and ask the user to sing the transaction, which will require a small fee.

You can find the full code for this app here.

Conclusion

In the first part of this series, we created a smart contract with two methods: one to store messages from users and another one to return them. In the second part we're created a web app that interact with those methods.

We can run this application locally by using a development server for the web app (running npm run dev) and a local ethereum node (using npx hardhat node). In the next part of this series I'll explain how to deploy the web app to a web server and the smart contract to the Rinkeby test network.

TAGS

If you enjoyed this article consider sharing it on social media or buying me a coffee ✌️

Buy Me A Coffee