# Lens Protocol # Overview of Lens Protocol ## The State of Social Media We live in a time where millions of users access centralized social media applications daily. Popular sites like Twitter and Meta help us stay connected to friends, family, news updates, and more. In this section, we'll look at the [Lens Protocol](lens.xyz) and how it can be used to build decentralized social media applications on the Polygon blockchain! We'll also explore why the Lens Protocol is a great toolkit for developers and how it can provide a more secure user experience than centralized social media. ## What is The Lens Protocol? The **Lens Protocol** is a decentralized social graph that allows developers to build social media platforms and profiles. What exactly is a social graph, though? A **social graph** is a collection of nodes that represent individual users, organizations, and any connections between those nodes. You can think of it as your friend lists on social media sites, your connections on LinkedIn, and who you're following on Twitter just to list a few examples. ## Decentralizing Social Media: Why Lens is Important Web2 social media platforms like Twitter and Meta can be prone to censorship since they are centralized. A post can be removed by administration for any reason. In some countries, the government may censor the internet and some social media platforms as well. The Lens Protocol solves this problem by not only giving developers a useful toolkit to build social media dApps, but also by giving users control over their own profiles and content. Since the Lens Protocol is decentralized, users can create profiles and post content without the risk of their content being removed. This means that developers can use Lens to build a social network that is more secure and more connected than traditional social media platforms. ## The Lens Toolkit What can developers do with the Lens Protocol? The protocol itself is built with modularity in mind, meaning that it has different modules (bits of code that help perform various functions) to use in the development process, making development even easier. Also, any modules added in the future will allow new features to be implemented seamlessly. Allow me to break down the current modules and their uses: - **Profile**: This module is what allows users to create a Lens profile. When a profile is created, a unique ProfileNFT gets minted. This ProfileNFT contains the history of the user's posts, comments, and any other content that the user has created. - **Post**: Implementing this module allows users to post and share content. IPFS and other media types are supported. - **Collect**: The Collect module will allow users to collect posts from users that they follow. The collect module allows posts to be turned into collectible NFTs. - **Mirror**: The Mirror module will allow users to re-share posts, like a retweet on Twitter. - **Follow**: The follow module will allow users to follow other users. Every time a user follows another user, a unique NFT is issued to the follower. This can allow for additional governance to be implemented into a dApp, as you can see things like who someone's first follower is. - **Comment**: The comment module allows users to comment on posts. Since the Lens Protocol is open source as well, you can even create your own modules. ## What else can Lens do? So, we learned how useful Lens Protocol is for developers and how it also gives the power back to users who want to create and manage their own content. But, what can else Lens do? Using Lens, users can sign into a growing collection of dApps and services that are built on the Lens Protocol. A great example of a dApp built on Lens Protocol is [Lenster](lenster.xyz) (a Community built, open source social network). Lenster is essentially a decentralized Twitter, allowing users to follow others, post content, and build a community. The Lens team also created [LensFrens](https://www.lensfrens.xyz/), a dApp that recommends users to follow based on your web3 footprint. This can be super useful when trying to find other people to follow. These two dApps are just examples of what the Lens Protocol can do. We are still in the early stages of decentralized social media, so more dApps will be created over time to connect to. This is another great thing about the Lens Procol - it's completely open source, so new dApps are currently being built. There are other many great dApps waiting to be built by **you**! You can check out all the [great dApp ideas](https://ideas.lens.xyz) on the official Lens website. ## Next step: Building with Lens Now that we've learned a bit about how the Lens Protocol works, we'll learn how to build a Lens-compatible dApp in the next section! # Building with Lens ## Setup Fork and clone our starter repo: https://github.com/womenbuildweb3/lens-api-starter # Get recommended profiles In the root directory of your project, create a file called `api.js`. Copy and paste the following variable. ```jsx 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 ```jsx 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: ```jsx 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 - ![Screenshot of developer console showing a response object with data for lens profiles](https://i.imgur.com/CopLR6x.png) Don't worry if a lof 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 - ```jsx 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. # View a profile 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. ```jsx 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 ```jsx 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`. ```jsx 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. ```jsx 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. ```jsx 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`: ```jsx 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. ```jsx 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. ```jsx 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. ```jsx 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. ```jsx 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`. ```jsx const [pubs, setPubs] = useState([]) ``` Now you can fetch the publications in your fetchProfile function: ```jsx 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: ```jsx {/* 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> )} ``` # Set up your wallet 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/. Alchemy also powers a free mumbai faucet here: https://mumbaifaucet.com/. # Follow user 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. ```jsx const CONTRACT_ADDRESS = "0x60Ae865ee4C725cd04353b5AAb364553f56ceF82" ``` Then in the root directory of your project, create a file called `abi.json`. Open this [link](https://mumbai.polygonscan.com/address/0x8C1f82e8AAD9399f52DcF224b77f33d5c1719241#code), and scroll down to where it says “Contract ABI”. ![Contract ABI](https://i.imgur.com/AbxpwRh.png) Copy and paste the ABI into `abi.json`. In the `pages/profile/[id].js`, import the ABI. ```jsx 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. ```jsx const [accounts, setAccounts] = useState(null); ``` Second, add this connectWallet function after `fetchProfile`. ```jsx 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. ```jsx {/* 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. ```jsx 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. If you'd prefer, [Alchemy](https://docs.alchemy.com/reference/alchemy-sdk-quickstart) has a SDK that is a superset of several Ethers.js libraries. Next, add this `followUser` function after `connectWallet`. ```jsx 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. ```jsx 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. ```jsx const signer = provider.getSigner(); ``` Then we create a contract object so we can interact with the LensHub smart contract. ```jsx 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. ```jsx const tx = await contract.follow([id], [0x0]); ``` Then we wait for that transaction to complete. ```jsx 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 - ```jsx {/* 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. # Wrapping Up 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. For example, you're not beholden to web2 social media's controls - you can easily just build your own [decentralized Twitter](https://docs.alchemy.com/docs/how-to-create-a-decentralized-twitter-with-lens-protocol). 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!