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
There are sample journeys, tasks, and verifiers in src/
. Let's install dependences:
cd proof-of-competence
npm install
Run this Next.js project locally:
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.
{
"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:
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.
//.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:
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:
{
"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:
// 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
:
// 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
:
{
"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
:
// 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()
) inconst 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"
}
},
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
},
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)
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"}]
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
}
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)
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!
Great post! Thank you.