Build a Frontend

Functions

Core Functions

Before we start building our frontend, we need to develop some functions that will handle the core logic of our application independently of any frontend frameworks. Once these functions are implemented, then we can integrate them with our chosen frontend framework to create a fully functional application.

These functions will receive the account state as their input, which is defined as follows:

import type { Signer } from 'ethers'
import type { Web3Provider } from '@ethersproject/providers'
interface Account {
address: string
provider: Web3Provider
signer: Signer
}

Additionally, these functions also require a Token contract instance. This can be created using the following code:

import { Contract } from 'ethers'
const Token = new Contract('TOKEN_ADDRESS', ['TOKEN_ABI'])

Function: getBalance

const getBalance = async ({ address, signer }: Account): Promise<BigNumber> => {
return await Token.connect(signer).balanceOf(address)
}

To retrieve the Token balance of a specific address, you can create a function called getBalance. This function will call the balanceOf method of the Token contract using the provided signer to fetch the balance of the given address. The function then returns this balance as a BigNumber object. This function can be useful for displaying the current balance of a user's Token holdings in the frontend.

Function: getProof

import { RelicClient } from '@relicprotocol/client'
const getProof = async ({ address, provider }: Account) => {
const client = await RelicClient.fromProvider(provider)
return await client.birthCertificateProver.getProofData({
account: address,
})
}

To retrieve the proof data required for verifying a fact on the Relic Protocol, you can implement a function called getProof. This function uses the RelicClient with the provided provider. It then calls the getProofData method of the prover with the given address to retrieve the proof data. This function is useful for checking which proof data is included in a transaction for Verification process.

Function: setFact

import { RelicClient } from '@relicprotocol/client'
const setFact = async ({ address, provider, signer }: Account) => {
const client = await RelicClient.fromProvider(provider)
const tx = await client.birthCertificateProver.prove({ account: address })
const res = await signer.sendTransaction(tx)
return await res.wait()
}

The setFact function is used to perform the Verification process on the Relic Protocol. It uses the prover’s prove method to generate the Verification transaction. Once the transaction is generated, it is signed with the provided signer and sent to the network.

Function: mint

import { RelicClient } from '@relicprotocol/client'
const mint = async ({ address, signer }: Account) => {
const res = await Token.connect(signer).mint(address)
return await res.wait()
}

The mint function demonstrates the Access process on the Relic Protocol. It utilizes the Token contract's mint function to mint new tokens for the specified address. The function calls the mint function with the provided signer and address, and then waits for the transaction to be confirmed before returning the result of the wait call.

Adapters

In this tutorial, we are using the Web3-Onboard library to simplify the integration between the web application and the wallet. If you are using a different library for wallet integration, you may need to modify the adapters.

Function: toAccount

import type { WalletState } from '@web3-onboard/core'
import { providers } from 'ethers'
const toAccount = (wallet: WalletState): Account => {
const address = wallet.accounts[0].address
const provider = new providers.Web3Provider(wallet.provider)
const signer = provider.getSigner()
return { address, provider, signer }
}

To convert the WalletState object provided by Web3-Onboard to the Account object required by the core functions, we need to use the toAccount function.

Reactivity

Let's define the states for the web application and make the core functions reactive.

In this tutorial, we are using Vue as the frontend framework. However, if you are using a different frontend framework like React, you can still use the functions we defined earlier.

State

import type { ProofData } from '@relicprotocol/client'
import type { TransactionReceipt } from '@ethersproject/abstract-provider'
import { ref } from 'vue'
import { useOnboard } from '@web3-onboard/vue'
const { connectedWallet, connectWallet } = useOnboard()
const [proof, factTxReceipt, mintTxReceipt, balance] = [
ref<ProofData>(),
ref<TransactionReceipt>(),
ref<TransactionReceipt>(),
ref<BigNumber>(),
]
const state = {
connectedWallet, // Ref<WalletState | null>
proof,
factTxReceipt,
mintTxReceipt,
balance,
}

To make the web application reactive, we need to define the application state. Here are some key state variables we can define:

  • connectedWallet: This is the wallet's state provided by web3-onboard library.
  • proof: This variable can be used to save the return value of the getProof function.
  • factTxReceipt: This variable can be used to save the return value of the setFact function.
  • mintTxReceipt: This variable can be used to save the return value of the mint function.
  • balance: This variable can be used to save the return value of the getBalance function.

Reactify

import type { Ref } from 'vue'
const reactify =
<T>({
wallet,
result,
}: {
wallet: Ref<WalletState | null>
result?: Ref<T>
}) =>
(f: (w: WalletState) => Promise<T>) =>
async () => {
const _wallet = wallet.value
if (_wallet !== null) {
const value = await f(_wallet)
if (result) {
result.value = value
}
}
}

The reactify function is designed to simplify the process of integrating the core functions into your Vue.js application with reactivity. This function acts as a factory that generates a reactive function by wrapping the core functions. It takes the wallet's state as well as a state variable for storing the return value of the core function.

View

The following code creates Vue components with reactivity by utilizing the previously defined functions:

<script setup lang="ts">
import type { Ref } from 'vue'
import type { BigNumber, Signer } from 'ethers'
import type { Web3Provider } from '@ethersproject/providers'
import type { WalletState } from '@web3-onboard/core'
import type { TransactionReceipt } from '@ethersproject/abstract-provider'
import type { ProofData } from '@relicprotocol/client'
import { ref } from 'vue'
import { Contract, providers } from 'ethers'
import { useOnboard } from '@web3-onboard/vue'
import { RelicClient } from '@relicprotocol/client'
interface Account {
address: string
provider: Web3Provider
signer: Signer
}
const Token = new Contract('TOKEN_ADDRESS', ['TOKEN_ABI'])
const getBalance = async ({
address,
signer,
}: Account): Promise<BigNumber> => {
return await Token.connect(signer).balanceOf(address)
}
const getProof = async ({ address, provider }: Account) => {
const client = await RelicClient.fromProvider(provider)
return await client.birthCertificateProver.getProofData({
account: address,
})
}
const setFact = async ({ address, provider, signer }: Account) => {
const client = await RelicClient.fromProvider(provider)
const tx = await client.birthCertificateProver.prove({ account: address })
const res = await signer.sendTransaction(tx)
return await res.wait()
}
const mint = async ({ address, signer }: Account) => {
const res = await Token.connect(signer).mint(address)
return await res.wait()
}
const toAccount = (wallet: WalletState): Account => {
const address = wallet.accounts[0].address
const provider = new providers.Web3Provider(wallet.provider)
const signer = provider.getSigner()
return { address, provider, signer }
}
const reactify =
<T>({
wallet,
result,
}: {
wallet: Ref<WalletState | null>
result?: Ref<T>
}) =>
(f: (w: Account) => Promise<T>) =>
async () => {
const _wallet = wallet.value
if (_wallet !== null) {
const value = await f(toAccount(_wallet))
if (result) {
result.value = value
}
}
}
const { connectWallet, connectedWallet } = useOnboard()
const [proof, factTxReceipt, mintTxReceipt, balance] = [
ref<ProofData>(),
ref<TransactionReceipt>(),
ref<TransactionReceipt>(),
ref<BigNumber>(),
]
const state = {
connectedWallet,
proof,
factTxReceipt,
mintTxReceipt,
balance,
}
</script>
<template>
<main>
<div class="app">
<h1>dApp</h1>
<template v-if="connectedWallet !== null">
<div class="entry">
<h2>Token Balance</h2>
<pre>{{ state.balance.value }}</pre>
<button
@click="
reactify({
wallet: state.connectedWallet,
result: state.balance,
})(getBalance)()
"
>
Get Balance
</button>
</div>
<div class="entry">
<h2>Proof</h2>
<pre>{{ state.proof.value }}</pre>
<button
@click="
reactify({ wallet: state.connectedWallet, result: state.proof })(
getProof
)()
"
>
Get Proof
</button>
</div>
<div class="entry">
<h2>Verify</h2>
<pre>{{ state.factTxReceipt.value }}</pre>
<button
@click="
reactify({
wallet: state.connectedWallet,
result: state.factTxReceipt,
})(setFact)()
"
>
Set Fact
</button>
</div>
<div class="entry">
<h2>Access</h2>
<pre>{{ state.mintTxReceipt.value }}</pre>
<button
@click="
reactify({
wallet: state.connectedWallet,
result: state.mintTxReceipt,
})(mint)()
"
>
Mint Token
</button>
</div>
</template>
<template v-else>
<div>
<button @click="connectWallet()">Connect Wallet</button>
</div>
</template>
</div>
</main>
</template>

Creating a new Vue Project

Initialization

To see the previous component, let's create a new Vue project by running the following command and following the instructions:

npm init vue@latest
Vue.js - The Progressive JavaScript Framework ✔ Project name: … frontend ✔ Add TypeScript? … No / Yes ✔ Add JSX Support? … No / Yes ✔ Add Vue Router for Single Page Application development? … No / Yes ✔ Add Pinia for state management? … No / Yes ✔ Add Vitest for Unit Testing? … No / Yes ✔ Add an End-to-End Testing Solution? › No ✔ Add ESLint for code quality? … No / Yes ✔ Add Prettier for code formatting? … No / Yes Scaffolding project in /private/tmp/relic-tutorial/frontend... Done.

Then, you need to install the dependencies required for the web application. To do so, navigate into the frontend directory and run the following command:

cd frontend
npm install @relicprotocol/client @web3-onboard/injected-wallets @web3-onboard/vue
npm install --save-dev vite-plugin-node-polyfills
npm run format

Additionally, you can remove any unused code in your project by running the following command:

rm -rf src/assets src/components src/views/AboutView.vue

Configuration

To use ethers in the browser, you may need to include polyfills for its dependencies. The vite-plugin-node-polyfills can simplify this process by using the following configuration in your vite.config.js file:

// File: frontend/vite.config.ts
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import { nodePolyfills } from 'vite-plugin-node-polyfills'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue(), nodePolyfills()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
})

Since we will be using only one view in this project, we can simplify the router configuration by using the following code:

// File: frontend/src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView,
},
],
})
export default router

To use the web3-onboard library, you need to initialize it. Use the following code to initialize the library. Note that the rpcUrl can be changed based on your machine's environment:

// File: frontend/src/App.vue
<script setup lang="ts">
import { RouterView } from 'vue-router'
import { init } from '@web3-onboard/vue'
import injectedWallets from '@web3-onboard/injected-wallets'
init({
wallets: [injectedWallets()],
chains: [
{
id: '0x1',
token: 'ETH',
label: 'Localhost',
rpcUrl: 'http://127.0.0.1:8545/',
},
],
accountCenter: {
desktop: {
enabled: false,
},
mobile: {
enabled: false,
},
},
})
</script>
<template>
<RouterView />
</template>

Modifying Contracts Deployment Script

const Token = new Contract("TOKEN_ADDRESS", ["TOKEN_ABI"])

It can be a tedious task to manually update the contract address in the code every time it is redeployed. To simplify this process, you can modify the deployment script to dynamically update the address and ABI whenever the contract is redeployed.

// File: scripts/deploy.ts
// https://github.com/NomicFoundation/hardhat-boilerplate/blob/master/scripts/deploy.js
// ...
import path from 'path'
import fs from 'fs'
async function main() {
// ...
const contractsDir = path.join(
__dirname,
'..',
'frontend',
'src',
'contracts'
)
if (!fs.existsSync(contractsDir)) {
fs.mkdirSync(contractsDir)
}
fs.writeFileSync(
path.join(contractsDir, 'contract-address.json'),
JSON.stringify({ Token: token.address }, undefined, 2)
)
const TokenArtifact = artifacts.readArtifactSync('Token')
fs.writeFileSync(
path.join(contractsDir, 'Token.json'),
JSON.stringify(TokenArtifact, null, 2)
)
}
// ...

Now you can create a contract instance using the imported address and ABI from the outside.

import addresses from '@/contracts/contract-address.json'
import contract from '@/contracts/Token.json'
const Token = new Contract(addresses.Token, contract.abi)

Complete Implementation

The complete code for the component is shown below. You can copy and paste this into the file frontend/src/views/HomeView.vue.

<script setup lang="ts">
import type { Ref } from 'vue'
import type { BigNumber, Signer } from 'ethers'
import type { Web3Provider } from '@ethersproject/providers'
import type { WalletState } from '@web3-onboard/core'
import type { TransactionReceipt } from '@ethersproject/abstract-provider'
import type { ProofData } from '@relicprotocol/client'
import { ref } from 'vue'
import { Contract, providers } from 'ethers'
import { useOnboard } from '@web3-onboard/vue'
import { RelicClient } from '@relicprotocol/client'
import addresses from '@/contracts/contract-address.json'
import contract from '@/contracts/Token.json'
interface Account {
address: string
provider: Web3Provider
signer: Signer
}
const Token = new Contract(addresses.Token, contract.abi)
const getBalance = async ({
address,
signer,
}: Account): Promise<BigNumber> => {
return await Token.connect(signer).balanceOf(address)
}
const getProof = async ({ address, provider }: Account) => {
const client = await RelicClient.fromProvider(provider)
return await client.birthCertificateProver.getProofData({
account: address,
})
}
const setFact = async ({ address, provider, signer }: Account) => {
const client = await RelicClient.fromProvider(provider)
const tx = await client.birthCertificateProver.prove({ account: address })
const res = await signer.sendTransaction(tx)
return await res.wait()
}
const mint = async ({ address, signer }: Account) => {
const res = await Token.connect(signer).mint(address)
return await res.wait()
}
const toAccount = (wallet: WalletState): Account => {
const address = wallet.accounts[0].address
const provider = new providers.Web3Provider(wallet.provider)
const signer = provider.getSigner()
return { address, provider, signer }
}
const reactify =
<T>({
wallet,
result,
}: {
wallet: Ref<WalletState | null>
result?: Ref<T>
}) =>
(f: (w: Account) => Promise<T>) =>
async () => {
const _wallet = wallet.value
if (_wallet !== null) {
const value = await f(toAccount(_wallet))
if (result) {
result.value = value
}
}
}
const { connectWallet, connectedWallet } = useOnboard()
const [proof, factTxReceipt, mintTxReceipt, balance] = [
ref<ProofData>(),
ref<TransactionReceipt>(),
ref<TransactionReceipt>(),
ref<BigNumber>(),
]
const state = {
connectedWallet,
proof,
factTxReceipt,
mintTxReceipt,
balance,
}
</script>
<template>
<main>
<div class="app">
<h1>dApp</h1>
<template v-if="connectedWallet !== null">
<div class="entry">
<h2>Token Balance</h2>
<pre>{{ state.balance.value }}</pre>
<button
@click="
reactify({
wallet: state.connectedWallet,
result: state.balance,
})(getBalance)()
"
>
Get Balance
</button>
</div>
<div class="entry">
<h2>Proof</h2>
<pre>{{ state.proof.value }}</pre>
<button
@click="
reactify({ wallet: state.connectedWallet, result: state.proof })(
getProof
)()
"
>
Get Proof
</button>
</div>
<div class="entry">
<h2>Verify</h2>
<pre>{{ state.factTxReceipt.value }}</pre>
<button
@click="
reactify({
wallet: state.connectedWallet,
result: state.factTxReceipt,
})(setFact)()
"
>
Set Fact
</button>
</div>
<div class="entry">
<h2>Access</h2>
<pre>{{ state.mintTxReceipt.value }}</pre>
<button
@click="
reactify({
wallet: state.connectedWallet,
result: state.mintTxReceipt,
})(mint)()
"
>
Mint Token
</button>
</div>
</template>
<template v-else>
<div>
<button @click="connectWallet()">Connect Wallet</button>
</div>
</template>
</div>
</main>
</template>

Preview Web Application

Before previewing the application, you need to deploy the contracts. Let's run the Hardhat network node in the terminal using the following command, in project’s root directory:

npx hardhat node

Afterward, in a new terminal, use the following command to deploy the contract on the Hardhat network:

npx hardhat run --network localhost scripts/deploy.ts

Finally, you can preview the app in the browser by running the following command in the Vue project folder. Check if your wallet is connected to the Hardhat network.

npm run dev