Lets BUIDL on Lens! π
π What is Lens?
Lens Protocol is a decentralized social network that has a low carbon footprint and an established web3 team behind it. Each user retains ownership over their profile and the content they create.
Prerequisites: Node plus Yarn and Git
clone/fork π scaffold-eth:
git clone https://github.com/mugrebot/scaffold-eth-lens-challengecheckout the
scaffold-nextjsbranch
cd scaffold-eth-lens-challengeinstall and start your π·β Hardhat chain:
yarn install
yarn chainin a second terminal window, π° deploy your contract (once you write them!):
yarn generate
cd scaffold-eth-lens-challenge
yarn deployπ Edit your smart contract YourContract.sol in packages/hardhat/contracts
πΌ Edit your deployment scripts in packages/hardhat/deploy
π± Open http://localhost:3000 to see the app
In this challenge you'll use the Lens API to fetch and render a social media feed, navigate to view an individual profile, and fetch and view the user's publications.
The Lens API can be tested at any time here using any of the GraphQL queries in the API docs.
Watch this intro video by Nader Dabit on the Lens API.
First, change into the services directory and install these dependencies for the GraphQL client into the packages/services folder:
cd packages/services
yarn add @apollo/client graphqlin a third terminal window, start your π± frontend:
cd ../../
yarn startπ You will edit your frontend app.jsx in packages/react-app/src
Creating a basic GraphQL API is simple, we can do it in just a couple of lines of code. We'll also define the first GraphQL query we'll be using in our app.
Create a new file named api.js in the packages/react-app/src/helpers and add the following code:
import { ApolloClient, InMemoryCache, gql } from "@apollo/client";
const API_URL = "https://api-mumbai.lens.dev";
/* create the API client */
export const client = new ApolloClient({
uri: API_URL,
cache: new InMemoryCache(),
});
export const authenticate = gql`
mutation Authenticate($address: EthereumAddress!, $signature: Signature!) {
authenticate(request: { address: $address, signature: $signature }) {
accessToken
refreshToken
}
}
`;
export const createProfile = gql`
mutation createProfile($request: CreateProfileRequest!) {
createProfile(request: $request) {
... on RelayerResult {
txHash
}
... on RelayError {
reason
}
__typename
}
}
`;
The queries we've defined here will allow a user to authenticate (https://docs.lens.xyz/docs/authentication-quickstart), generating an authentication token that is required to perform other queries such as creating a profile, following other users, setting default handle, changing profile picture, etc.
we can now authenticate using the api, lets add the following code exists in to our app.jsx file, note that the imports should be near the other imports, the state variables with the other state variables, and the functions might be seperated along the app.jsx file as opposed to how you see them below!
import {
authenticate,
challenge,
client,
exploreProfiles,
profileaddress,
getPublications,
createProfile,
} from "./helpers/api.js";
const [address, setAddress] = useState();
const [token, setToken] = useState();
const [profileId, setProfileId] = useState();
const [user_selected_handle, setUser_selected_handle] = useState();
const [open, setOpen] = useState(false);
const [confirmLoading, setConfirmLoading] = useState(false);
const [modalText, setModalText] = useState("Content of the modal");
const [handle, setHandle] = useState();
const [signer, setUserSigner] = useState();
async function Login() {
try {
/* first request the challenge from the API server */
const challengeInfo = await client.query({
query: challenge,
variables: { address },
});
const signature = await signer.signMessage(challengeInfo.data.challenge.text);
console.log(address);
/* authenticate the user */
const authData = await client.mutate({
mutation: authenticate,
variables: {
address,
signature,
},
});
/* if user authentication is successful, you will receive an accessToken and refreshToken */
const {
data: {
authenticate: { accessToken },
},
} = authData;
console.log({ accessToken });
setToken(accessToken);
} catch (err) {
console.log("Error signing in: ", err);
}
}
So far we should be able to "login" and receive an authentication token, it'll be up to you to create the query on app.jsx
Since passing something as a header isn't included in the authentication step, we will give that first part to you - write the rest!
async function createProfileRequest() {
if (user_selected_handle === undefined) {
setOpen(true);
console.log("modal should open");
} else {
try {
//lets package the request we are going to make! it requires a handle that the user will select via modal, profilePictureUri: null, followModule: null,
const request = {
handle: `${user_selected_handle}`,
profilePictureUri: null,
followModule: null,
};
//this part will model the authentication query
const createProfile_const = await client.mutate({
//x-access-token header puts in the request with your authentication token.
context: {
headers: {
"x-access-token": token,
},
},
mutation: /*what mutation do we need to create a profile?*/,
variables: {
/* what variable(s) to we need?*/,
},
});
console.log("attempting to createprofile for: ", user_selected_handle);
if ((/* what does this query return on success? */) != undefined) {
console.log(
"create profile successful:",
`${request.handle}.test`,
"created at txHash:",
/* what does this query return on success? */,
);
setHandle(`${request.handle}.test`);
return createProfile_const;
} else {
console.log("create profile failed, try again!:", createProfile_const.data?.createProfile?.reason);
setOpen(true);
}
} catch (err) {
console.log("Error creating profile: ", err);
setOpen(true);
}
}
}
This next part will involve some information that we have provided for you in packages/react-app/src/constants.js
It'll involve LENS_HUB, the address of the lens contract we want to read/write from (note this is a proxy contract, we highly recommend that you check these out as they can be very useful)
It will involve the ABI, Application Binary Interface, which gives us the information needed to talk to the smart contract, like what functions/parameters and what they return
Let's find the function post in the abi in constants.js
{
lock: true,
address: "{{LENS_HUB}}",
name: "post",
abi: {
inputs: [
{
components: [
{ internalType: "uint256", name: "profileId", type: "uint256" },
{ internalType: "string", name: "contentURI", type: "string" },
{ internalType: "address", name: "collectModule", type: "address" },
{ internalType: "bytes", name: "collectModuleInitData", type: "bytes" },
{ internalType: "address", name: "referenceModule", type: "address" },
{ internalType: "bytes", name: "referenceModuleInitData", type: "bytes" },
],
internalType: "struct DataTypes.PostData",
name: "vars",
type: "tuple",
},
],
name: "post",
outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
stateMutability: "nonpayable",
type: "function",
},
params: [""],
},
This is how you instantiate a contract object using ethers.js - note this is extremely redundant and there is a simpler way to parse the abi information, we challenge you to do this, but for a learning example this works! Hint: the contract we want is [1] and the function we want is [30]
const postContract = new ethers.Contract(LENS_HUB, [LENS_ABI?.children[1].children[30].abi], localProvider);
const postContractWSigner = postContract.connect(userSigner);
Here are the parameters we will need to pass to the post function (from Lens Developer Docs)
const Id = await lens_id.getProfileIdByHandle(`${handle}`);
console.log(Id);
//convert ID to hex
const profileId = `${ethers.utils.hexlify(Id)}`;
const contentURI = "https://ipfs.io/ipfs/Qmby8QocUU2sPZL46rZeMctAuF5nrCc7eR1PPkooCztWPz";
const collectModule = "0x0BE6bD7092ee83D44a6eC1D949626FeE48caB30c";
const collectModuleInitData = "0x0000000000000000000000000000000000000000000000000000000000000001";
const referenceModule = "0x0000000000000000000000000000000000000000";
const referenceModuleInitData = "0x00";
Lets pass this info to the post function - and see what we get, we have gone ahead and made a tab called "Your Lens Profile" if you have gotten this far try clicking the button, it should show your handle as well as your first post gm πΏ gm πΏ
First, we need to optimize our build before we deploy it to
yarn build
Use a file storage like IPFS or Surge to ship your app!
yarn surge
Documentation, tutorials, challenges, and many more resources, visit: docs.scaffoldeth.io
π Read the docs: https://docs.soliditylang.org
π Go through each topic from solidity by example editing YourContract.sol in π scaffold-eth
π§ Learn the Solidity globals and units
Join the telegram support chat π¬ to ask questions and find others building with π scaffold-eth!
π Please check out our Gitcoin grant too!
