DEV Community

fangjun
fangjun

Posted on • Edited on

How to use Proof-of-Competence Web3 on-boarding framework

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). 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:

git clone git@github.com:wslyvh/proof-of-competence.git
Enter fullscreen mode Exit fullscreen mode

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

cd proof-of-competence
npm install
Enter fullscreen mode Exit fullscreen mode

Run this Next.js project locally:

npx next dev
Enter fullscreen mode Exit fullscreen mode

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.

{
    "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"
    }]
}
Enter fullscreen mode Exit fullscreen mode

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:

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

http://localhost:3000/api/journey/score?journey=basicweb3&address=0x00
Enter fullscreen mode Exit fullscreen mode

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.

//.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...
Enter fullscreen mode Exit fullscreen mode

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:

import { Task } from "types"

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

Step 2: add task to the journey

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

    {
        "name": "has ETH",
        "description": "Own 0.1+ ETH.",
        "points": 400,
        "verifier": "has-ETH-mainnet",
        "params": {
          "amount":0.1
        }
    },
Enter fullscreen mode Exit fullscreen mode

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:

// 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
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

// 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
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

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

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:

// 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"}]
Enter fullscreen mode Exit fullscreen mode

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:

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

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.

    {
        "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
    },
Enter fullscreen mode Exit fullscreen mode

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:
const provider = new AlchemyProvider(task.chainId || 1, process.env.NEXT_PUBLIC_ALCHEMY_API_KEY)
Enter fullscreen mode Exit fullscreen mode

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.

// 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"}]
Enter fullscreen mode Exit fullscreen mode

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

    {
        "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
    }
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (2)

Collapse
 
wslyvh profile image
wslyvh

This is awesome! Thanks for the amazing write up. I'd really love to chat more to see how we can add even more verifiers and quests to it!

Collapse
 
0xshah profile image
shah-alchemy

Great post! Thank you.