How to use Proof-of-Competence Web3 on-boarding framework
Tags: blockchain, web3, nextjs, ethersjs
Published: 2022-01-22 15:16:41

Web3 is open, and one's footprints can be verified on-chain. Proof-of-Competence web3 front-end framework (https://github.com/wslyvh/proof-of-competence) provides a good start for building DAPPs to help users measure their own web3 activities. 

It is developed by Wesley([@wslyvh](https://twitter.com/wslyvh)). He explains it as follows:

> Proof of Competence (PoC) is an open, modular framework **to create on-chain quests and on-boarding journeys into Web3**. It uses a pluggable task system which can verify that on-chain actions have occurred. This allows to build up reputation or social (DAO) scores that proof an address is familiar with the specified technologies or protocols.

You can find Proof of Competence front-end framework at: https://www.poc.quest 

This is an 8-part how-to guide for developers to use PoC framework: 

- Installation and explanation
- Add your own PoC journey 
- Write your verifier using Ethers.js

---

## 1. How it works

Users can connect wallet via MetaMask or WalletConnect to Proof-of-Competence(PoC) DAPP to track their own web3 footprint. You get a score for a specific web3 journey. 

PoC is designed with three concepts: 

- **Journey**, a list of tasks for web3 users, stored in JSON format. It will give user a score according to the activities of his address.


- **Task**, a unique task such as having Token/NFT, deployed contract and etc.


- **Verifier**, verify a task using public information(e.g. on-chain, Subgraphs, or public blockchain explorers). To use PoC framework, we need to write verifier for our own task in the  `/src/verifiers`.

PoC framework also provides restful API using Next.js API routers so that we can use the scores via API.

- **PoC API**, restful API using Next.js. 

PoC framework uses on-chain data through:

- Infura API,
- Alchemy API,
- Etherscan API, etc.
 
It is developed with these Node.js and web3 components: 

- **Next.js**, the React framework by Vercel. 
- **Chakra UI**, modular React UI Components.
- **Ethers.js**, library to interact with Ethereum.
- **Web3-react**, React UI for connecting wallet.

---

## 2. How to install it locally

Download the project source code:

```bash
git clone git@github.com:wslyvh/proof-of-competence.git
```

There are sample journeys, tasks, and verifiers in `src/`. Let's install dependences:

```bash
cd proof-of-competence
npm install
```

Run this Next.js project locally:

```bash
npx next dev
```

You can view the DAPP at `http://localhost:3000` in your browser. Try the journeys in it.

---

## 3. Add your own web3 journey with tasks

Let's copy `src/journeys/useWeb3.json` to `src/journeys/basicWeb3.json` and edit our journey based on it. Basic Web3 User journey has two tasks: have address, have ENS.  

```json
{
    "name": "Basic Web3 User",
    "version": 1,
    "description": "Onboarding new developers into the Web3 space",
    "website": "https://www.ethereum.org/",
    "twitter": "fjun99",
    "tasks": [{
        "name": "Have a wallet address",
        "description": "You need a wallet address to enter Web3 universe.",
        "points": 100,
        "verifier": "active-address"
    },
    {
        "name": "ENS: Ethereum Name Service",
        "description": "Register your ENS name at https://ens.domains/ with a reverse lookup.",
        "points": 100,
        "verifier": "ens-reverse-lookup"
    }]
}
```

You can view our journey at: http://localhost:3000/basicWeb3

There are several verifiers in this project and we use `active-address`, `ens-reverse-lookup`:

- active-address
- first-transaction
- ens-reverse-lookup
- has-poap
- has-nft
- deployed-contract
- vote-on-snapshot

There are also several dummy verifiers to start with: 

- test-always-false
- test-always-true
- test-random-number

We can get the journey and score from restful API at:

```bash
http://localhost:3000/api/journey?name=basicWeb3

http://localhost:3000/api/journey/score?journey=basicweb3&address=0x00
```

**Note** : You can also deploy this project online using GitHub & Vercel. Vercel's tutorial: https://nextjs.org/learn/basics/deploying-nextjs-app. 

We will continue to develop our own task and verifier locally.

---

## 4. Some preparations for development

### 4.1 Add `.env.local` 

We will use Next.js environment variables to store Infura, Alechemy and Etherscan API ID/keys. Variables with prefix `NEXT_PUBLIC_` can be accessed from the browser. More details about Next.js environment variables at: https://nextjs.org/docs/basic-features/environment-variables

Add file `.env.local`. This file should be added to .gitignore if you opensource your project.

```js
//.env.local
NEXT_PUBLIC_ALCHEMY_API_URL=https://eth-mainnet.g.alchemy.com/v2/
NEXT_PUBLIC_ALCHEMY_API_KEY=7C...
NEXT_PUBLIC_INFURA_API_ID=d7...
NEXT_PUBLIC_ETHERSCAN_API_KEY=ab...
```

Please remember to stop and run again after you change the environmental variables.

### 4.2 Notes on Alchemy API

We will use Alchemy API to interact with Ethereum mainnet. Documents can be found at: https://docs.alchemy.com/alchemy/

- **Token API**

We will call `alchemy_getTokenBalances` to get ERC20 token balance.

Reference: https://docs.alchemy.com/alchemy/enhanced-apis/token-api#alchemy_gettokenbalances

- **Chain API / Ethereum API**

We will call `eth_getBalance` to get Ether balance of an address. 

Reference: https://docs.alchemy.com/alchemy/apis/ethereum/eth_getbalance

Using Ethereum API, we can interact with the Ethereum RPC directly. 

Another Option is to interact with Ethereum RPC using `Ethers.js` through Alchemy endpoint. We will use this option to check if a user owns a specific NFT later.

- **NFT API**

In the sample NFT verifier in the project, it calls Alchemy NFT API `getNFTs` to get information about the NFTs of an address. 

Two notes you may need to know:

First, NFT API is in the V1 of Alchemy API. We need to change the endpoint from V2 to V1: `https://eth-mainnet.g.alchemy.com/your-api-key/v2` should be `https://eth-mainnet.g.alchemy.com/your-api-key/v1`.

Second, NFT API need to apply manually, otherwise your call will return:

> "Eth NFT API v1 not enabled on this account - please visit https://alchemyapi.typeform.com/nft-api-access to request access!"

I applied using this form. But I haven't received permission yet. So we will interact directly with an ERC721 contract using `Ethers.js` in section 7.

---

## 5. Add a verifier to measure ETH of your address

### Step 1: add verifier

Add a verifier called `has-ETH-mainnet`. Let's begin with copy `test-always true` directory and change the directory name to `has-ETH`. The `index.ts` in this directory looks like:

```js
import { Task } from "types"

export async function verify(task: Task, address: string): Promise<boolean | number>
{
    return true
}
```

### Step 2: add task to the journey

In `src/journeys/basicWeb3.json`, add a task:

```json
    {
        "name": "has ETH",
        "description": "Own 0.1+ ETH.",
        "points": 400,
        "verifier": "has-ETH-mainnet",
        "params": {
          "amount":0.1
        }
    },
```

Explanation of this task:

- Points: 400 points
- Verifier: has-ETH
- params: 0.1. If the user's address has more than 0.1 ETH, the user gets 400 points.

Let's go to the web page `http://localhost:3000/basicWeb3`, you can see that you get 400 points as the verifier always returns `true` now.

### Step 3: program has-ETH verifier

Let's program the `has-ETH` verifier:

```js
// verifiers/has-ETH/index.ts
// Reminder:add in `.env.local`: NEXT_PUBLIC_ALCHEMY_API_URL, NEXT_PUBLIC_ALCHEMY_API_KEY

import { Task } from "types"

export async function verify(task: Task, address: string): Promise<boolean | number>
{
    if (!address) return false

    try { 
        const amount = task.params['amount']
        const url=`${process.env.NEXT_PUBLIC_ALCHEMY_API_URL}${process.env.NEXT_PUBLIC_ALCHEMY_API_KEY}`  
        const body={
            "jsonrpc":"2.0",
            "method":"eth_getBalance",
            "params":[address,"latest"],
            "id":0
        }

        const response = await fetch(url,{
            method: 'POST', 
            body: JSON.stringify(body) 
          })
        const data = await response.json()
        if(data.error) return false
        if(data.result/1e18 > amount) return true

        return false
    }
    catch(e) {
        return false
    }
}
```

We call `eth_getBalance` using Alchemy Ethereum API. If result > amount, the verifier returns true.

In my test address, there is about 0.15 ETH in it. The task is passed and I get 400 points.

Let's change the amount in the journey to 0.2 ETH. The task is failed and I don't get 400 points.

TODO NOTE: We program this example quickly and we should consider modifying this verifier so that it can be used in testnet (Ropsten), sidechain (Polygon), L2 (Arbitrum/Optimism) and other EVM. 

---

## 6. Add a verifier to measure ERC20 token

In this section, you will add a verifier `has-token-ERC20` to measure ERC20 token in your address. 

Add directory `has-token-ERC20` and then edit `index.ts`:

```js
// verifiers/has-token-ERC20/index.ts
// Reminder:add in `.env.local`: NEXT_PUBLIC_ALCHEMY_API_URL, NEXT_PUBLIC_ALCHEMY_API_KEY

import { Task } from "types"

export async function verify(task: Task, address: string): Promise<boolean | number>
{
    if (!address) return false

    try { 
        const contractAddress = task.params['addressERC20']
        const amount = task.params['amount']
        const url=`${process.env.NEXT_PUBLIC_ALCHEMY_API_URL}${process.env.NEXT_PUBLIC_ALCHEMY_API_KEY}`  
        const body={
            "jsonrpc":"2.0",
            "method":"alchemy_getTokenBalances",
            "params":[address,[contractAddress]],
            "id":42
        }

        const response = await fetch(url,{
            method: 'POST', 
            body: JSON.stringify(body) 
          })

        const data = await response.json()
        if(!Array.isArray(data.result.tokenBalances) ) return false

        const balance = data.result.tokenBalances[0];
        if(balance.error) return false
        if(balance.tokenBalance/1e18 > amount) return true;

        return false
    }
    catch(e) {
        return false
    }
}
```

Let's add a task to measure whether the user's address has ERC20 `ENS Token` and the amount should be 10+:

ENS ERC20 Token contract address: `0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72`

Add a task in `basicWeb3.json`:

```json
    {
        "name": "has 10+ ENS Token",
        "description": "Own 10+ ENS ERC20 Token.",
        "points": 400,
        "verifier": "has-token-ERC20",
        "params": {
          "addressERC20": "0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72",
          "amount":10
        }
    },
```

As I have ENS ERC20 token in my test address, I can pass this task and get 400 points.

---

## 7. Add a verifier to measure ERC721 NFT

In this section, you will add a verifier `has-nft-ERC721` to check where you have at least one specific ERC721 token in your address.

We will use `Ethers.js` to call an ERC721 contract function `balanceOf(address)` to check. Add `has-nft-ERC721/index.ts`:

```js
// has-nft-ERC721/index.ts
// Reminder:add in `.env.local`: NEXT_PUBLIC_ALCHEMY_API_KEY

import { Task } from "types"
import { ethers } from "ethers"
import { AlchemyProvider } from '@ethersproject/providers'

export async function verify(task: Task, address: string): Promise<boolean | number>
{
    if (!address || !task.params) return false

    if(!task.params['tokenAddress']) return false
    const contractAddress =task.params['tokenAddress'].toString()
    if(!ethers.utils.isAddress(contractAddress)) return false

    try { 
        const provider = new AlchemyProvider(task.chainId || 1, process.env.NEXT_PUBLIC_ALCHEMY_API_KEY)
        const contract = await new ethers.Contract( contractAddress, abi, provider)
        const balanceOfNFT= await contract.balanceOf(address)
        if (balanceOfNFT > 0)  
            return true

        return false
    }
    catch(e) {
        return false
    }
}

const abi =
    [{"constant":true,
    "inputs":[{"internalType":"address","name":"owner","type":"address"}],
    "name":"balanceOf",
    "outputs":[{"internalType":"uint256","name":"","type":"uint256"}],
    "payable":false,"stateMutability":"view","type":"function"}]
```

Explanation of what we do in this verifier:

- connect to an ERC721 contract with `ethers.Contract(contractAddress, abi, provider)`.
- call `contract.balanceOf(address)` to get NFT amount of user's address.
- we provide ABI segment of the need contract function (`balanceOf()`) in `const abi`.

Add a task in `basicWeb3.json`:

```json
    {
        "name": "Own an ERC721 NFT",
        "description": "Own an ENS NFT(ERC721).",
        "points": 400,
        "verifier": "has-nft-ERC721",
        "params": {
          "tokenAddress": "0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea85"
        }
    },
```

`0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea85` is the contract address of ENS ERC721 NFT.

---

## 8. Extend task/verifier for other chains using `chainId`

We can extend task/verifier for other chains using `chainId`. Let's add another task in `basicWeb3.json` to check whether there is a specific NFT on polygon in the user's address.

```json
    {
        "name": "Own an ERC721 NFT on polygon",
        "description": "Own an NFT on Polygon network.",
        "points": 400,
        "verifier": "has-nft-ERC721",
        "params": {
          "tokenAddress": "0x7eb476Cd0fE5578106A01DC2f2E392895C6BC0A5"
        },
        "chainId":137
    },
```

Explanations of what we do: 

- We add 'chainId' for this task and 137 is for Polygon mainnet.
- Verifier get web3 provider according to `chainId` in line: 

```js
const provider = new AlchemyProvider(task.chainId || 1, process.env.NEXT_PUBLIC_ALCHEMY_API_KEY)
```

We can also refactor the `has-token-ERC20` verifier to support other chains. 

We may rename it to `has-token-ERC20-using-Alchemy`. Then, we add a new verifier `has-token-ERC20` which use `Ethers.js` to interact with blockchain directly.

```js
// has-token-ERC20/index.ts
// Reminder:add in `.env.local`: NEXT_PUBLIC_ALCHEMY_API_KEY

import { Task } from "types"
import { ethers } from "ethers"
import { AlchemyProvider } from '@ethersproject/providers'

export async function verify(task: Task, address: string): Promise<boolean | number>
{
    if (!address || !task.params) return false

    if(!task.params['tokenAddress']) return false
    const contractAddress =task.params['tokenAddress'].toString()
    if(!ethers.utils.isAddress(contractAddress)) return false

    let amount:number = 0
    if('amount' in task.params) 
        amount = Number(task.params['amount'])

    try { 
        const provider = new AlchemyProvider(task.chainId || 1, process.env.NEXT_PUBLIC_ALCHEMY_API_KEY)
        const contract = await new ethers.Contract( contractAddress , abi , provider )
        const balanceOf= await contract.balanceOf(address)
        if (balanceOf/1e18 > amount)  
            return true

        return false
    }
    catch(e) {
        return false
    }
}

const abi =
    [{"constant":true,
    "inputs":[{"name":"who","type":"address"}],
    "name":"balanceOf",
    "outputs":[{"name":"","type":"uint256"}],
    "payable":false,"stateMutability":"view","type":"function"}]
```

We add a task in the `basicWeb3` journey using this verifier:

```json
    {
        "name": "has some WETH on Polygon",
        "description": "has some WETH (ERC20 Token) on Polygon",
        "points": 400,
        "verifier": "has-token-ERC20",
        "params": {
          "tokenAddress": "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619",
          "amount":0
        },
        "chainId":137
    }
```

---

## The End of the beginning

Now, you can begin to add tasks and verifiers to your protocol for on-boarding web3 users.

My suggestion for this framework is:

>It may be restructured to be two parts: 
>
>1: an aggregating site for all journeys, verifiers, and API which developers can use to integrate PoC to other things such as Discord.
>
>2: a light/handy framework (perhaps a SDK / npm package) developers can easily use on their own site for on-boarding users to their protocols or applications.