For this tutorial, you'll be working with Lens API, a GraphQL API that enables us to quickly interact with Lens social graph without having to worry about writing extensive Solidity or indexing, querying, and validating data.
This tutorial is based on Nader Dabit's YouTube video, Fullstack Web3 with Lens Protocol.
Fork and clone our starter repo: https://github.com/womenbuildweb3/lens-api-starter
You can find similar instructions on forking and cloning a repo here: https://www.30daysofweb3.xyz/en/curriculum/3-writing-your-smart-contract/1-dev-setup
After you've cloned the repo, open this project in your IDE and then install all dependencies by running this command in your terminal:
npm install # or yarn install
Then start the development server:
npm run dev # or yarn dev
In the root directory of your project, create a file called api.js
.
Copy and paste the following variable.
export const recommendedProfiles = ` query RecommendedProfiles { recommendedProfiles { id name bio attributes { displayType traitType key value } followNftAddress metadata isDefault picture { ... on NftImage { contractAddress tokenId uri verified } ... on MediaSet { original { url mimeType } } __typename } handle coverPicture { ... on NftImage { contractAddress tokenId uri verified } ... on MediaSet { original { url mimeType } } __typename } ownedBy dispatcher { address canUseRelay } stats { totalFollowers totalFollowing totalPosts totalComments totalMirrors totalPublications totalCollects } followModule { ... on FeeFollowModuleSettings { type amount { asset { symbol name decimals address } value } recipient } ... on ProfileFollowModuleSettings { type } ... on RevertFollowModuleSettings { type } } } } `;
This query returns several popular profiles.
Let’s configure our urql client so we can call our query. At the top of the file, add
import { createClient } from 'urql'; const APIURL = "https://api-mumbai.lens.dev"; export const client = new createClient({ url: APIURL, });
Next, from your root directory, find the pages folder and go to your index.js
file. It should look like this:
import { useState, useEffect } from "react"; import Link from "next/link"; import Image from "next/image"; import { client, recommendedProfiles } from "../api"; export default function Home() { useEffect(() => { fetchProfiles(); }, []); async function fetchProfiles() { try { const response = await client.query(recommendedProfiles).toPromise(); console.log({ response }); } catch (err) { console.log(err); } } return ( <div></div> ); }
In your terminal, run npm run dev
to run your project locally then visit http://localhost:3000 in your browser. Open your browser’s console to see the data we fetched from our recommendedProfiles query. It should look something like this -
Don't worry if a lot of these results have blank fields.
Now that we’ve confirmed that our query is working, let’s display the profiles. Update your Home component to look like this -
import { useState, useEffect } from "react"; import Link from "next/link"; import Image from "next/image"; import { client, recommendedProfiles } from "../api"; import Layout from "../components/Layout"; import Head from "next/head"; import HeroSection from "../components/HeroSection"; export default function Home() { const [profiles, setProfiles] = useState([]); useEffect(() => { fetchProfiles(); }, []); async function fetchProfiles() { try { const response = await client.query(recommendedProfiles).toPromise(); console.log({ response }); setProfiles(response.data.recommendedProfiles); } catch (err) { console.log(err); } } return ( <Layout> <Head> <title>Lensbook</title> </Head> <HeroSection /> <div className="my-16 space-y-12 sm:grid sm:grid-cols-2 sm:gap-x-6 sm:gap-y-12 sm:space-y-0 md:grid-cols-3 md:gap-x-8 lg:grid-cols-4"> {profiles.map((profile) => ( <Link href={`/profile/${profile.id}`} key={profile.id}> <a className="flex flex-col items-center"> {profile.picture && profile.picture.original && profile.picture.original.url.includes("lens.infura-ipfs.io") ? ( <div className="relative w-60 h-60 bg-emerald-900 rounded"> <Image src={profile.picture.original.url} layout="fill" objectFit="cover" alt={profile.handle} className="rounded" /> </div> ) : ( <div className="bg-emerald-900 w-60 h-60 rounded" /> )} <div className="mt-4 text-lg leading-6 font-medium text-center space-y-1"> <h3>{profile.name}</h3> <p className="text-emerald-600">{profile.handle}</p> </div> <div className="text-gray-600 mt-2 grid grid-cols-2 gap-x-2 text-sm sm:text-base text-center"> <p> <span className="text-gray-900 font-medium"> {profile.stats.totalFollowers} </span>{" "} Followers </p> <p> <span className="text-gray-900 font-medium"> {profile.stats.totalFollowing} </span>{" "} Following </p> </div> </a> </Link> ))} </div> </Layout> ); }
Your homepage should now show a list of popular lens profiles.
Next, we can create a page that will show the details of any profile we click on. To do this, we can use dynamic routing. Create a new folder inside the pages folder named profile
. Inside this folder, create a new file called [id].js
and add the code below.
import { useRouter } from "next/router"; import Layout from "../../components/Layout"; import Head from "next/head"; import Image from "next/image"; export default function Profile() { const router = useRouter(); const { id } = router.query; return ( <div> ID: {id} </div> ); }
Now if you click on any profile on your homepage, you should see that user's id on the profile page.
We will want to show more details about the user here, so we’ll need to set up a query to fetch that data.
In our api.js
file, we can add a new query called getProfileById. We copied this query from the Lens docs here: https://docs.lens.xyz/docs/get-profile
export const getProfileById = ` query Profile($id: ProfileId!) { profile(request: { profileId: $id }) { id name bio attributes { displayType traitType key value } followNftAddress metadata isDefault picture { ... on NftImage { contractAddress tokenId uri verified } ... on MediaSet { original { url mimeType } } __typename } handle coverPicture { ... on NftImage { contractAddress tokenId uri verified } ... on MediaSet { original { url mimeType } } __typename } ownedBy dispatcher { address canUseRelay } stats { totalFollowers totalFollowing totalPosts totalComments totalMirrors totalPublications totalCollects } followModule { ... on FeeFollowModuleSettings { type amount { asset { symbol name decimals address } value } recipient } ... on ProfileFollowModuleSettings { type } ... on RevertFollowModuleSettings { type } } } } `;
Back in our /pages/profile/[id].js
file, we can import our client and query at the top of the file, as well as useState
and useEffect
.
import { useState, useEffect } from "react"; import { client, getProfileById } from "../../api";
Inside your Profile
function, create an async function called fetchProfile
, where we can fetch our data. We can use useEffect
to run this function if the id
variable isn’t null. Make sure to add the id
to the dependency array.
useEffect(() => { if (id) { fetchProfile(); } }, [id]); async function fetchProfile() { }
In the fetchProfile function, we can use a try…catch statement to catch any errors while fetching the profile data.
async function fetchProfile(){ try { const response = await client.query(getProfileById, { id }).toPromise(); console.log("PROFILE:", response); } catch(error) { console.log("ERROR:", error); } }
Now you should be able to see the profile data logged in the console.
Next, instead of just logging this data, you can save it to the state. At the top of the Profile
function, use the useState
hook to declare the variables profile
and setProfile
:
const [profile, setProfile] = useState()
Now you can use setProfile
to store the profile data from your response. To test it out, you can try rendering the handle of the profile if the profile variable isn’t null.
async function fetchProfile(){ try { const response = await client.query(getProfileById, { id }).toPromise(); console.log("PROFILE:", response); setProfile(response.data.profile); } catch(error) { console.log("ERROR:", error); } } return ( <div> {profile && <div>{profile.handle}</div>} </div> )
Next, you can update our profile page UI to display more profile information. We added some styling here for you too to make the page look nicer.
return ( <Layout> <Head> <title>{profile ? profile.handle : "Lensbook"}</title> </Head> <div className="my-12"> {profile && ( <div className="flex flex-wrap md:flex-nowrap items-start w-full"> <div className="w-full md:w-auto mb-4 md:mr-8"> {profile.picture && profile.picture.original && profile.picture.original.url.includes("lens.infura-ipfs.io") ? ( <div className="relative w-60 h-60 bg-emerald-900 rounded mx-auto"> <Image src={profile.picture.original.url} layout="fill" objectFit="cover" alt={profile.handle} className="rounded" /> </div> ) : ( <div className="bg-emerald-900 w-60 h-60 rounded mx-auto" /> )} </div> <div className="grow-1 w-full"> <div className="text-center md:text-left"> <h1 className="text-3xl font-bold text-gray-900 sm:text-4xl sm:tracking-tight mb-1"> {profile.name} </h1> <h2 className="text-xl font-bold text-emerald-500 sm:text-2xl sm:tracking-tight mb-2"> {profile.handle} </h2> <div className="flex flex-wrap gap-x-2 text-gray-600 text-sm sm:text-base mb-4 justify-center md:justify-start"> <p> <span className="text-gray-900 font-medium"> {profile.stats.totalFollowers} </span>{" "} Followers </p> <p> <span className="text-gray-900 font-medium"> {profile.stats.totalFollowing} </span>{" "} Following </p> </div> <p className="mb-4">{profile.bio}</p> {/* Add connect and follow buttons here */} </div> {/* Add publications here */} </div> </div> )} </div> </Layout> )
Next, we want to be able to show all of the publications this user created. From the Lens docs, “Publications come in three primary types: posts, comments, and mirrors. Posts are the base object, with mirror and comment providing additional functionality.” If you’re familiar with Twitter, think of posts like tweets and mirrors like retweets. Publications would be like the Tweets & Replies section of a someone's Twitter profile.
Back in our api.js
file, we can add this new query to fetch the publications for a certain user. Here we name it getPublicationsById
. Note: we copied this query from the lens docs here: https://docs.lens.xyz/docs/get-publications. This is a pretty large query, and you probably won’t want to use all of this data. We recommend removing any fields you don’t end up using.
export const getPublicationsById = ` query Publications ($id: ProfileId!) { publications(request: { profileId: $id, publicationTypes: [POST, COMMENT, MIRROR], limit: 10 }) { items { __typename ... on Post { ...PostFields } ... on Comment { ...CommentFields } ... on Mirror { ...MirrorFields } } pageInfo { prev next totalCount } } } fragment MediaFields on Media { url mimeType } fragment ProfileFields on Profile { id name bio attributes { displayType traitType key value } isFollowedByMe isFollowing(who: null) followNftAddress metadata isDefault handle picture { ... on NftImage { contractAddress tokenId uri verified } ... on MediaSet { original { ...MediaFields } } } coverPicture { ... on NftImage { contractAddress tokenId uri verified } ... on MediaSet { original { ...MediaFields } } } ownedBy dispatcher { address } stats { totalFollowers totalFollowing totalPosts totalComments totalMirrors totalPublications totalCollects } followModule { ... on FeeFollowModuleSettings { type amount { asset { name symbol decimals address } value } recipient } ... on ProfileFollowModuleSettings { type } ... on RevertFollowModuleSettings { type } } } fragment PublicationStatsFields on PublicationStats { totalAmountOfMirrors totalAmountOfCollects totalAmountOfComments } fragment MetadataOutputFields on MetadataOutput { name description content media { original { ...MediaFields } } attributes { displayType traitType value } } fragment Erc20Fields on Erc20 { name symbol decimals address } fragment CollectModuleFields on CollectModule { __typename ... on FreeCollectModuleSettings { type followerOnly contractAddress } ... on FeeCollectModuleSettings { type amount { asset { ...Erc20Fields } value } recipient referralFee } ... on LimitedFeeCollectModuleSettings { type collectLimit amount { asset { ...Erc20Fields } value } recipient referralFee } ... on LimitedTimedFeeCollectModuleSettings { type collectLimit amount { asset { ...Erc20Fields } value } recipient referralFee endTimestamp } ... on RevertCollectModuleSettings { type } ... on TimedFeeCollectModuleSettings { type amount { asset { ...Erc20Fields } value } recipient referralFee endTimestamp } } fragment PostFields on Post { id profile { ...ProfileFields } stats { ...PublicationStatsFields } metadata { ...MetadataOutputFields } createdAt collectModule { ...CollectModuleFields } referenceModule { ... on FollowOnlyReferenceModuleSettings { type } } appId hidden reaction(request: null) mirrors(by: null) hasCollectedByMe } fragment MirrorBaseFields on Mirror { id profile { ...ProfileFields } stats { ...PublicationStatsFields } metadata { ...MetadataOutputFields } createdAt collectModule { ...CollectModuleFields } referenceModule { ... on FollowOnlyReferenceModuleSettings { type } } appId hidden reaction(request: null) hasCollectedByMe } fragment MirrorFields on Mirror { ...MirrorBaseFields mirrorOf { ... on Post { ...PostFields } ... on Comment { ...CommentFields } } } fragment CommentBaseFields on Comment { id profile { ...ProfileFields } stats { ...PublicationStatsFields } metadata { ...MetadataOutputFields } createdAt collectModule { ...CollectModuleFields } referenceModule { ... on FollowOnlyReferenceModuleSettings { type } } appId hidden reaction(request: null) mirrors(by: null) hasCollectedByMe } fragment CommentFields on Comment { ...CommentBaseFields mainPost { ... on Post { ...PostFields } ... on Mirror { ...MirrorBaseFields mirrorOf { ... on Post { ...PostFields } ... on Comment { ...CommentMirrorOfFields } } } } } fragment CommentMirrorOfFields on Comment { ...CommentBaseFields mainPost { ... on Post { ...PostFields } ... on Mirror { ...MirrorBaseFields } } } `;
Now in pages/profile/[id].js
you can update the import at the top to include this function.
import { client, getProfileById, getPublicationsById } from "../../api"
You will also add a new variable to keep track of our publications. To keep it short, we’ll call it pubs and set it to an empty array with useState
.
const [pubs, setPubs] = useState([])
Now you can fetch the publications in your fetchProfile function:
async function fetchProfile() { try { const response = await client.query(getProfileById, { id }).toPromise(); console.log("PROFILE:", response); setProfile(response.data.profile); const publications = await client.query(getPublicationsById, { id }).toPromise(); console.log("PUBS!", publications); setPubs(publications.data.publications.items); } catch (error) { console.log("ERROR:", error); } }
Next, you can map the publications to show a list of each one:
{/* Add publications here */} {pubs.length > 0 && ( <div className="border-t-2 border-gray-100 my-8 py-8 flex flex-col space-y-8"> {pubs.map((p, index) => ( <div key={p.id}> <p className="font-bold">{p.__typename}</p> <p>{p.metadata.content}</p> <p>{p.metadata.name}</p> </div> ))} </div> )}
Before we continue, we want to make sure we have a wallet that can connect to Polygon testnet and has some Matic, so we can interact with Lens smart contracts.
If you haven't set up your wallet yet, visit our tutorial here: https://www.30daysofweb3.xyz/en/curriculum/1-getting-started/5-set-up-your-wallet
You will also need to have test MATIC in your wallet. You can request test MATIC from the polygon mumbai faucet here: https://faucet.polygon.technology/.
If you need help, check to see if your question has already been asked in #section-9-help. If you don't see it in there, post a question with any details that would make it easy for a team member to help you.
We want to add a follow button so users can follow a profile they like. To follow a profile with Lens Protocol, a user will need to interact with the Lens contract.
You can find all of the deployed contract addresses from Lens Protocol here: https://docs.lens.xyz/docs/deployed-contract-addresses.
For this tutorial, we will be interacting with LensHub contracts that are deployed on Mumbai Testnet.
In the pages/profile/[id].js
file, add a variable called CONTRACT_ADDRESS
and set it to the lens contract address.
const CONTRACT_ADDRESS = "0x60Ae865ee4C725cd04353b5AAb364553f56ceF82"
Then in the root directory of your project, create a file called abi.json
.
Open this link, and scroll down to where it says “Contract ABI”.
Copy and paste the ABI into abi.json
.
In the pages/profile/[id].js
, import the ABI.
import ABI from "../../abi.json";
Before we can add the follow button, we will need to enable the user to connect their wallet.
First, create a new state variable to reference the connected wallet.
const [accounts, setAccounts] = useState(null);
Second, add this connectWallet function after fetchProfile
.
async function connectWallet() { const accounts = await window.ethereum.request({ method: "eth_requestAccounts", }); console.log("accounts: ", accounts); setAccounts(accounts); }
Then in your UI, add a Connect Wallet button that calls the connectWallet
function on click.
{/* Add connect and follow buttons here */} <button onClick={connectWallet} type="button" className="inline-flex items-center px-4 py-2 border border-transparent text-base font-medium rounded-md text-emerald-700 bg-emerald-100 hover:bg-emerald-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-emerald-500" > Connect Wallet </button>
Save your file then confirm that you can connect your wallet by clicking Connect Wallet button and seeing the accounts array in your console.
Now that the user can connect their wallet, we can support follow functionality.
Import ethers at the top of the file.
import { ethers } from 'ethers'
If you recall from earlier in the curriculum, ethers.js is a JavaScript library that allows developers to interact with Ethereum.
Next, add this followUser
function after connectWallet
.
async function followUser() { const provider = new ethers.providers.Web3Provider(window.ethereum); const signer = provider.getSigner(); const contract = new ethers.Contract(CONTRACT_ADDRESS, ABI, signer); try { const tx = await contract.follow([id], [0x0]); await tx.wait(); console.log("Followed user successfully"); } catch (err) { console.log("Failed to follow user due to", err); } }
Let’s go through this function line by line.
First, we create a provider that allows us to connect to the Ethereum network through the user's wallet.
const provider = new ethers.providers.Web3Provider(window.ethereum);
Next, we get the signer from the provider which will be used to sign and send transactions.
const signer = provider.getSigner();
Then we create a contract object so we can interact with the LensHub smart contract.
const contract = new ethers.Contract(CONTRACT_ADDRESS, ABI, signer);
Next, we want to call a function that will let us follow users. We can browser the functions defined in the LensHub smart contract at https://docs.lens.xyz/docs/functions.
Let’s jump to follow() at https://docs.lens.xyz/docs/functions#follow.
The follow() function takes in profileIds as a parameter. profileIds is an array of IDs of the profile you want to follow. Given an array of profile IDs, the follow() function will then mint a follow NFT for each profile ID.
Back to the followUser function, note that we call the contract’s follow() function and pass in the id of the profile page we’re currently on.
const tx = await contract.follow([id], [0x0]);
Then we wait for that transaction to complete.
await tx.wait();
We wrap this in a try..catch
block in case the transaction fails.
Now that we understand how followUser works, add a Follow button that calls followUser on click below the profile information. Update your code like so -
{/* Add connect and follow buttons here */} {accounts ? ( <button onClick={followUser} type="button" className="inline-flex items-center px-4 py-2 border border-transparent text-base font-medium rounded-md shadow-sm text-white bg-emerald-600 hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-emerald-500" > Follow {profile.handle} </button> ) : ( <button onClick={connectWallet} type="button" className="inline-flex items-center px-4 py-2 border border-transparent text-base font-medium rounded-md text-emerald-700 bg-emerald-100 hover:bg-emerald-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-emerald-500" > Connect Wallet </button> )}
Here, we check whether we've connected our wallet. If the wallet is connected, we will show the Follow button.
Save your updated file, connect your wallet, and then click Follow. Confirm that you are indeed following this profile by checking your console.
Social media is just one application of a social graph, and now with Lens, you have the tools to build applications that reflect the meaningful relationships we have on-chain.
To be eligible to receive a Lens handle and apply for the accelerator, be sure to submit both your web3rsvp and Lens projects in the project submission form that can be found at the end of the curriculum. If you have already submitted your web3rsvp project, re-visit the form and update your response.
The BUIDL Accelerator is coming up fast. More details will be presented officially in the coming days, but some alpha:
START BUILDING SOMETHING. In addition to our no-strings-attached grants, we will also be awarding $20K in grants to teams building with Lens. So hop into the server, find a team, and start brainstorming!
Adapted from Nader Dabit's Lens API tutorial
Writers: Sarah Schwartz, Sarah Z, Editors: Cami