This file is a merged representation of a subset of the codebase, containing specifically included files, combined into a single document by Repomix. ================================================================ File Summary ================================================================ Purpose: -------- This file contains a packed representation of the entire repository's contents. It is designed to be easily consumable by AI systems for analysis, code review, or other automated processes. File Format: ------------ The content is organized as follows: 1. This summary section 2. Repository information 3. Directory structure 4. Repository files (if enabled) 4. Multiple file entries, each consisting of: a. A separator line (================) b. The file path (File: path/to/file) c. Another separator line d. The full contents of the file e. A blank line Usage Guidelines: ----------------- - This file should be treated as read-only. Any changes should be made to the original repository files, not this packed version. - When processing this file, use the file path to distinguish between different files in the repository. - Be aware that this file may contain sensitive information. Handle it with the same level of security as you would the original repository. Notes: ------ - Some files may have been excluded based on .gitignore rules and Repomix's configuration - Binary files are not included in this packed representation. Please refer to the Repository Structure section for a complete list of file paths, including binary files - Only files matching these patterns are included: src/pages/chain/**/*.mdx, src/pages/protocol/**/*.mdx, src/pages/storage/**/*.mdx - Files matching patterns in .gitignore are excluded - Files matching default ignore patterns are excluded - Files are sorted by Git change count (files with more changes are at the bottom) Additional Info: ---------------- ================================================================ Directory Structure ================================================================ src/ pages/ chain/ integrations/ connect-wallet.mdx ethers.mdx viem.mdx web3py.mdx resources/ changelog.mdx contracts.mdx developer-portal.mdx differences-from-ethereum.mdx network-information.mdx smart-contracts/ foundry.mdx hardhat.mdx tools/ account-abstraction/ safe.mdx thirdweb.mdx zksync.mdx bigquery/ costs.mdx examples.mdx introduction.mdx schemas.mdx setup.mdx bridging/ across.mdx lens.mdx zksync.mdx cross-chain/ ccip.mdx zksync-elastic.mdx data-indexers/ covalent.mdx dune.mdx rindexer.mdx the-graph.mdx thirdweb.mdx defi/ uniswap.mdx faucets/ alchemy.mdx lenscan.mdx thirdweb.mdx on-ramp/ halliday.mdx thirdweb.mdx oracles/ chainlink.mdx redstone.mdx rpc/ alchemy.mdx chainstack.mdx drpc.mdx public.mdx quicknode.mdx tenderly.mdx thirdweb.mdx smart-contract-development/ smart-contract-development.mdx tenderly.mdx thirdweb.mdx best-practices.mdx overview.mdx running-a-node.mdx running-an-archive-node.mdx using-lens-chain.mdx protocol/ accounts/ actions.mdx block.mdx create.mdx feedback.mdx fetch.mdx funds.mdx manager.mdx metadata.mdx mute.mdx notifications.mdx apps/ authorization-workflows.mdx create.mdx fetch.mdx index.mdx manage.mdx best-practices/ content-licensing.mdx custom-fragments.mdx erc20-approval.mdx error-handling.mdx mentions.mdx metadata-standards.mdx pagination.mdx team-management.mdx transaction-lifecycle.mdx bigquery/ costs.mdx examples.mdx introduction.mdx schemas.mdx setup.mdx concepts/ account.mdx actions.mdx app.mdx feed.mdx graph.mdx group.mdx rules.mdx sponsorship.mdx username.mdx feeds/ bookmarks.mdx boost-engagement.mdx custom-feeds.mdx delete-post.mdx edit-post.mdx feed-rules.mdx feedback.mdx fetch-posts.mdx moderating.mdx post-actions.mdx post-rules.mdx post.mdx timelines.mdx getting-started/ graphql.mdx react.mdx typescript.mdx graphs/ custom-graphs.mdx follow-rules.mdx follow-unfollow.mdx graph-rules.mdx relationships.mdx groups/ banned-accounts.mdx create.mdx fetch-members.mdx fetch.mdx join.mdx manage.mdx membership-approvals.mdx rules.mdx migration/ api.mdx database.mdx from-polygon.mdx overview.mdx sdk.mdx resources/ changelog.mdx contracts.mdx sponsorships/ fetch.mdx funding.mdx managing.mdx sponsoring-transactions.mdx tools/ balances.mdx s3.mdx sns-notifications.mdx tutorials/ post-an-image.mdx usernames/ assign.mdx create.mdx custom-namespaces.mdx fetch.mdx namespace-rules.mdx reserved-usernames.mdx authentication.mdx index.mdx user-rewards.mdx storage/ resources/ changelog.mdx glossary.mdx usage/ delete.mdx download.mdx edit.mdx getting-started.mdx upload.mdx index.mdx ================================================================ Files ================================================================ ================ File: src/pages/chain/integrations/connect-wallet.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Connect Wallet This guide will walk you through integrating wallet connection into your application using ConnectKit. ConnectKit provides an easy-to-use interface for connecting Ethereum wallets into web applications. We will configure it specifically for the Lens Testnet. ## Installation Install ConnectKit, its required dependencies, and the Lens Chain SDK with package manager of choice: ```bash npm install connectkit wagmi viem@2.x @tanstack/react-query @lens-chain/sdk@latest ``` ## API Keys ConnectKit uses WalletConnect's SDK, which requires a projectId. You can create one for free at [WalletConnect Cloud](https://cloud.reown.com). ## Implementation ### Setting Up Web3Provider Create a new file `Web3Provider.tsx` and define the required configuration. ```ts filename="Web3Provider.tsx" "use client"; import { WagmiProvider, createConfig, http } from "wagmi"; import { chains } from "@lens-chain/sdk/viem"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ConnectKitProvider, getDefaultConfig } from "connectkit"; const config = createConfig( getDefaultConfig({ chains: [chains.mainnet, chains.testnet], transports: { [chains.mainnet.id]: http(chains.mainnet.rpcUrls.default.http[0]!), [chains.testnet.id]: http(chains.testnet.rpcUrls.default.http[0]!), }, walletConnectProjectId: process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID, appName: "Lens Testing App", appDescription: "A sample app integrating Lens Testing wallet connection.", appUrl: "https://yourapp.com", appIcon: "https://yourapp.com/icon.png", }) ); const queryClient = new QueryClient(); export const Web3Provider = ({ children }) => { return ( {children} ); }; ``` ### Wrapping Your App with Web3Provider Modify `App.tsx` to use the newly created `Web3Provider`: ```ts filename="App.tsx" import { Web3Provider } from "./Web3Provider"; import { ConnectKitButton } from "connectkit"; const App = () => { return ( ); }; export default App; ``` ### Accessing Connected Wallet Information To interact with the connected wallet, use the `useAccount` hook from `wagmi`: ```ts import { useAccount } from "wagmi"; const WalletInfo = () => { const { address, isConnecting, isDisconnected } = useAccount(); if (isConnecting) return
Connecting...
; if (isDisconnected) return
Disconnected
; return
Connected Wallet: {address}
; }; export default WalletInfo; ```
## Additional Build Tooling Setup Some build tools require additional setup to work with ConnectKit. ### Next.js ConnectKit uses [WalletConnect](https://walletconnect.com)'s SDK to help with connecting wallets. WalletConnect 2.0 pulls in Node.js dependencies that Next.js does not support by default. You can mitigate this by adding the following to your next.config.js file: ```ts filename="next.config.js" module.exports = { webpack: (config) => { config.resolve.fallback = { fs: false, net: false, tls: false }; return config; }, }; ``` ### Next.js App Router If using Next.js App Router, or any framework that supports React Server Components, you will need to include the `"use client"` directive at the beginning of your Web3Provider file. ```ts filename="Web3Provider.tsx" "use client" ... export const Web3Provider = ({ children }) => { return ( ... ); }; ``` ## Resources - [Lens Chain Example Repos](https://github.com/lens-protocol/network-examples/) - [Next.js Additional Setup](https://docs.family.co/connectkit/getting-started#getting-started-nextjs) - [ConnectKit Documentation](https://docs.family.co/) - [Viem Guide](./viem) - [Ethers Guide](./ethers) By following these steps, you have successfully integrated ConnectKit with the Lens Testnet to enable wallet connections in your application. ================ File: src/pages/chain/integrations/ethers.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Ethers SDK Interact with Lens Chain using [ethers.js](https://docs.ethers.io/). A template web app can be found [here](https://github.com/lens-protocol/network-examples). --- The Lens Chain SDK provides support for [ethers.js](https://docs.ethers.io/), a widely-used JavaScript library for interacting with EVM blockchains. This is achieved by building upon [ZKsync-ethers v6](https://docs.zksync.io/sdk/js/ethers/v6/getting-started), the official ZKsync SDK for ethers.js. This section assumes you are familiar with the ethers concepts of _Provider_ and _Signer_: - A **Provider** is a read-only connection to the blockchain, which allows querying the blockchain state, such as account, block or transaction details, querying event logs or evaluating read-only code using call. - A **Signer** wraps all operations that interact with an account. An account generally has a private key located somewhere, which can be used to sign a variety of types of payloads. ## Getting Started ### Install ZKsync-ethers First, install [ZKsync-ethers](<(https://docs.zksync.io/sdk/js/ethers/v6/getting-started)>) package: ```bash filename="npm" npm install zksync-ethers ethers@6 ``` ```bash filename="yarn" yarn add zksync-ethers ethers@6 ``` ```bash filename="pnpm" pnpm add zksync-ethers ethers@6 ``` ethers.js version 6 is a peer dependency of `zksync-ethers` package. Make sure to install it alongside ZKsync-ethers. ### Install SDK Then, install the `@lens-chain/sdk` package: ```bash filename="npm" npm install @lens-chain/sdk@latest ``` ```bash filename="yarn" yarn add @lens-chain/sdk@latest ``` ```bash filename="pnpm" pnpm add @lens-chain/sdk@latest ``` ### Create Your Providers Next, create the providers to interact with the Lens Chain and the corresponding L1 (an Ethereum chain): To interact with the Lens Chain, use the SDK's `getDefaultProvider` function. This creates a specialized [JsonRpcProvider](https://docs.ethers.org/v6/api/providers/jsonrpc/#JsonRpcProvider) instance that supports the `lens` and `zks` RPC method namespaces. For interactions with Ethereum L1, create an ethers Provider connected to the corresponding network (Sepolia for testnets). The example below uses the [getDefaultProvider](https://docs.ethers.org/v6/api/providers/#getDefaultProvider) function from the `ethers` package. ```ts filename="providers.ts" import { getDefaultProvider, Network } from "@lens-chain/sdk/ethers"; import { ethers } from "ethers"; // Lens Chain (L2) export const lensProvider = getDefaultProvider(Network.Testnet); // Ethereum L1 export const ethProvider = ethers.getDefaultProvider("sepolia"); ``` To interact with [EIP-1193](https://eips.ethereum.org/EIPS/eip-1193) wallets in the browser (like MetaMask), use the [BrowserProvider](https://docs.zksync.io/sdk/js/ethers/v6/providers/browserprovider). For interactions with Lens Chain only, use the SDK's `getDefaultProvider` function. This creates a specialized [JsonRpcProvider](https://docs.ethers.org/v6/api/providers/jsonrpc/#JsonRpcProvider) instance that supports the `lens` and `zks` RPC method namespaces. ```ts filename="providers.ts" // convenience type for window.ethereum?: Eip1193Provider import "@lens-chain/sdk/globals"; import { BrowserProvider, getDefaultProvider, Network } from "@lens-chain/sdk/ethers"; import { Eip1193Provider } from "ethers"; // Lens Chain (L2) export const lensProvider = getDefaultProvider(Network.Testnet); // User's network export const browserProvider = new BrowserProvider(window.ethereum as Eip1193Provider); ``` ## Create a Signer Lens Chain leverages [ZKsync-ethers signers](https://docs.zksync.io/sdk/js/ethers/v6/accounts) to facilitate interaction with an account. Below is a list of available signers: - The [Wallet](https://docs.zksync.io/sdk/js/ethers/v6/accounts#wallet) class extends [ethers.Wallet](https://docs.ethers.org/v6/api/wallet/#Wallet), incorporating additional ZKsync features. - The [EIP712Signer](https://docs.zksync.io/sdk/js/ethers/v6/accounts#eip712signer) class is used to sign EIP712-typed ZKsync transactions. - The [Signer](https://docs.zksync.io/sdk/js/ethers/v6/accounts#signer) and [L1Signer](https://docs.zksync.io/sdk/js/ethers/v6/accounts#l1signer) classes are designed for browser integration. - The [SmartAccount](https://docs.zksync.io/sdk/js/ethers/v6/accounts#smartaccount) class enhances support for account abstraction. It includes factory classes such as: - [ECDSASmartAccount](https://docs.zksync.io/sdk/js/ethers/v6/accounts#ecdsasmartaccount), which uses a single ECDSA key for signing payloads. - [MultisigECDSASmartAccount](https://docs.zksync.io/sdk/js/ethers/v6/accounts#multisigecdsasmartaccount), which uses multiple ECDSA keys for signing payloads. Additionally, ZKsync-ether includes `VoidSigner` and `L1VoidSigner` classes. These are ZKsync's implementations of ethers' [VoidSigner](https://docs.ethers.org/v6/api/providers/abstract-signer/#VoidSigner). They allow an address to be used in any API that accepts a [Signer](https://docs.ethers.org/v6/api/providers/#Signer), even when no credentials are available for actual signing. ```ts filename="ZKsync Signers" import { Wallet, EIP712Signer, Signer, SmartAccount, ECDSASmartAccount, MultisigECDSASmartAccount, } from "zksync-ethers"; ``` For a subset of these, the SDK provides extended versions that include Lens Chain-specific features. ```ts filename="Lens Chain Signers" import { Wallet, Signer } from "@lens-chain/sdk/ethers"; ``` ### Wallet Create a `Wallet` instance using the L1 and/or L2 providers depending on which network you want to interact with. ```ts filename="wallet.ts" import { Wallet } from "@lens-chain/sdk/ethers"; import { lensProvider, ethProvider } from "./providers"; export const wallet = new Wallet( process.env.PRIVATE_KEY as String, lensProvider, ethProvider ); ``` ```ts filename="providers.ts" import { getDefaultProvider, Network } from "@lens-chain/sdk/ethers"; import { ethers } from "ethers"; // Lens Chain (L2) export const lensProvider = getDefaultProvider(Network.Testnet); // Ethereum L1 export const ethProvider = ethers.getDefaultProvider("sepolia"); ``` You can also create an unconnected `Wallet` instance and [connect](https://docs.zksync.io/sdk/js/ethers/v6/accounts#connect) it later: ```ts filename="wallet.ts" import { Wallet } from "@lens-chain/sdk/ethers"; import { lensProvider, ethProvider } from "./providers"; const unconnectedWallet = new Wallet(process.env.PRIVATE_KEY as String); const lensWallet = unconnectedWallet.connect(lensProvider); const ethWallet = unconnectedWallet.connectToL1(ethProvider); ``` ```ts filename="providers.ts" import { getDefaultProvider, Network } from "@lens-chain/sdk/ethers"; import { ethers } from "ethers"; // Lens Chain (L2) export const lensProvider = getDefaultProvider(Network.Testnet); // Ethereum L1 export const ethProvider = ethers.getDefaultProvider("sepolia"); ``` ### Signer Create a `Signer` instance that is connected to the `BrowserProvider` instance. This class is to be used in a browser environment. ```ts filename="signer.ts" import { Signer } from "@lens-chain/sdk/ethers"; import { browserProvider, lensProvider } from "./providers"; const network = await browserProvider.getNetwork(); const signer = Signer.from( await browserProvider.getSigner(), Number(network.chainId), lensProvider ); ``` ```ts filename="providers.ts" import "@lens-chain/sdk/globals"; import { BrowserProvider, getDefaultProvider, Network, } from "@lens-chain/sdk/ethers"; import { Eip1193Provider } from "ethers"; // Lens Chain (L2) export const lensProvider = getDefaultProvider(Network.Testnet); // User's network export const browserProvider = new BrowserProvider( window.ethereum as Eip1193Provider ); ``` ### L1Signer Create an `L1Signer` instance to do ZKsync-related operations on L1. To do so you need to connect it to both the `BrowserProvider` and the `LensProvider`. This class is to be used in a browser environment. ```ts filename="l1Signer.ts" import { L1Signer } from "zksync-ethers"; import { browserProvider, lensProvider } from "./providers"; export const l1Signer = L1Signer.from( await browserProvider.getSigner(), lensProvider ); ``` ```ts filename="providers.ts" import "@lens-chain/sdk/globals"; import { BrowserProvider, getDefaultProvider, Network, } from "@lens-chain/sdk/ethers"; import { Eip1193Provider } from "ethers"; // Lens Chain (L2) export const lensProvider = getDefaultProvider(Network.Testnet); // User's network export const browserProvider = new BrowserProvider( window.ethereum as Eip1193Provider ); ``` ## Actions Ethers.js v6 enables interaction with the Lens Chain through various actions, including sending transactions, signing messages, and managing wallet interactions. ### Transactions To send transactions, you can use the [sendTransaction](https://docs.ethers.org/v6/api/providers/#Signer-sendTransaction) function for gas token transfers ($GRASS) or a [Contract](https://docs.ethers.org/v6/api/contract/) class instance to call contract functions. **Sending a Transaction:** ```ts import { signer } from "./signer"; const tx = await signer.sendTransaction({ to: "0xRecipientAddress", value: parseEther("1.0"), }); await tx.wait(); console.log(`Transaction Hash: ${tx.hash}`); ``` **Executing Contract Function:** ```ts import { Contract } from "ethers"; import { signer } from "./signer"; import { contractAbi } from "./abi"; const contract = new Contract("0xContractAddress", contractAbi, signer); const tx = await contract.functionName(arg1, arg2); await tx.wait(); console.log(`Transaction Hash: ${tx.hash}`); ``` ### Signatures To sign messages for cryptographic authentication, the [signMessage](https://docs.ethers.org/v6/api/providers/#Signer-signMessage) or [signTypedData](https://docs.ethers.org/v6/api/providers/#Signer-signTypedData) functions can be used. **Sign Message** ```ts import { signer } from "./signer"; const message = "Hello, Lens Chain!"; const signature = await signer.signMessage(message); console.log(`Signature: ${signature}`); ``` **Sign Typed Data:** ```ts import { signer } from "./signer"; const domain = { name: "Lens Testnet", version: "1", chainId: 37111, verifyingContract: "0xYourContractAddress", }; const types = { Permit: [ { name: "owner", type: "address" }, { name: "spender", type: "address" }, { name: "value", type: "uint256" }, { name: "nonce", type: "uint256" }, { name: "deadline", type: "uint256" }, ], }; const message = { owner: "0xYourWalletAddress", spender: "0xSpenderAddress", value: BigInt(1000000000000000000), nonce: BigInt(0), deadline: BigInt(Math.floor(Date.now() / 1000) + 3600), }; const signature = await signer.signTypedData(domain, types, message); console.log(`Signature: ${signature}`); ``` --- ## Additional Options ### Custom RPC Node If you want to use a Lens Chain RPC node other than the default one, you can create a custom provider like this: ```ts filename="providers.ts" import { Provider } from "@lens-chain/sdk/ethers"; // Lens Chain (L2) export const lensProvider = new Provider("https://custom-rpc-node.com"); ``` ================ File: src/pages/chain/integrations/viem.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Viem SDK Interact with the Lens Chain using [Viem](https://viem.sh). A template web app can be found [here](https://github.com/lens-protocol/network-examples). --- The Lens Chain SDK offers first-class support for [Viem](https://viem.sh), a popular TypeScript Interface for Ethereum. Specifically, it adopts Viem paradigms and provides stateless, low-level primitives for interacting with Lens Chain. This section presumes that you are familiar with the [client-action architecture](https://viem.sh/docs/clients/intro#clients) of Viem. ## Getting Started ### Install Viem First, install [Viem](https://viem.sh/docs/installation) package: ```bash filename="npm" npm install viem@2 ``` ```bash filename="yarn" yarn add viem@2 ``` ```bash filename="pnpm" pnpm add viem@2 ``` ### Install SDK Then, install the `@lens-chain/sdk` package: ```bash filename="npm" npm install @lens-chain/sdk@latest ``` ```bash filename="yarn" yarn add @lens-chain/sdk@latest ``` ```bash filename="pnpm" pnpm add @lens-chain/sdk@latest ``` ### Create a Client Next, configure your [Client](https://viem.sh/docs/clients/intro) by selecting the desired [Transport](https://viem.sh/docs/clients/intro) and a [Lens Chain Chain](#viem-chains). ```ts filename="publicClient.ts" import { createPublicClient, http } from "viem"; import { chains } from "@lens-chain/sdk/viem"; export const publicClient = createPublicClient({ chain: chains.mainnet, transport: http(), }); ``` ```ts filename="walletClient.ts (EIP-1193)" import "viem/window"; import { Address, createWalletClient, custom } from "viem"; import { chains } from "@lens-chain/sdk/viem"; // For more information on hoisting accounts, // visit: https://viem.sh/docs/accounts/local.html#optional-hoist-the-account const [account] = (await window.ethereum!.request({ method: "eth_requestAccounts", })) as [Address]; export const walletClient = createWalletClient({ account, chain: chains.mainnet, transport: custom(window.ethereum!), }); ``` ```ts filename="walletClient.ts (Private Key)" import { createWalletClient, Hex, http, privateKeyToAccount } from "viem"; import { chains } from "@lens-chain/sdk/viem"; const account = privateKeyToAccount(process.env.PRIVATE_KEY as Hex); export const walletClient = createWalletClient({ account, chain: chains.mainnet, transport: http(), }); ``` ## Actions Viem enables interaction with the Lens Chain through various actions, including sending transactions, signing messages, reading contract data, managing wallet networks, and custom Lens Chain / ZKsync actions. ### Transactions To send transactions, you can use the [sendTransaction](https://viem.sh/docs/actions/wallet/sendTransaction.html) or [writeContract](https://viem.sh/docs/contract/writeContract.html) functions from Viem. The `sendTransaction` function is suitable for simple gas token transfers ($GRASS), while `writeContract` is used for executing contract functions that modify the blockchain state. **Sending a Transaction:** ```ts import { walletClient } from "./walletClient"; const hash = await walletClient.sendTransaction({ to: "0xRecipientAddress", value: 1000000000000000000n, // Amount in wei }); ``` **Executing a Contract Function:** ```ts import { walletClient } from "./walletClient"; import { contractAbi } from "./abi"; const hash = await walletClient.writeContract({ address: "0xContractAddress", abi: contractAbi, functionName: "functionName", args: [arg1, arg2], }); ``` ### Signatures To sign messages for cryptographic authentication, the [signMessage](https://viem.sh/docs/actions/wallet/signMessage.html) or [signTypedData](https://viem.sh/docs/actions/wallet/signTypedData.html) functions can be used. **Sign Message:** ```ts import { walletClient } from "./walletClient"; const signature = await walletClient.signMessage({ message: "Hello, Lens Chain!", }); ``` **Sign Typed Data:** ```ts import { walletClient } from "./walletClient"; const signature = await walletClient.signTypedData({ account: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", domain: { name: "Ether Mail", version: "1", chainId: 1, verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", }, types: { Person: [ { name: "name", type: "string" }, { name: "wallet", type: "address" }, ], Mail: [ { name: "from", type: "Person" }, { name: "to", type: "Person" }, { name: "contents", type: "string" }, ], }, primaryType: "Mail", message: { from: { name: "Cow", wallet: "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", }, to: { name: "Bob", wallet: "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB", }, contents: "Hello, Bob!", }, }); ``` ### Contract Read To call a smart contract view function, use the [readContract](https://viem.sh/docs/contract/readContract.html) function. The [multicall](https://viem.sh/docs/contract/multicall.html) function allows batching view function calls into a single request. **Read Contract Function:** ```ts import { publicClient } from "./publicClient"; import { contractAbi } from "./abi"; const data = await publicClient.readContract({ address: "0xContractAddress", abi: contractAbi, functionName: "functionName", args: [arg1], }); ``` **Multicall:** ```ts import { publicClient } from "./publicClient"; import { contractAbi } from "./abi"; const results = await publicClient.multicall({ contracts: [ { address: "0xContractAddress1", abi: contractAbi, functionName: "functionName1", args: [arg1], }, { address: "0xContractAddress2", abi: contractAbi, functionName: "functionName2", args: [arg2], }, ], }); ``` ### Wallet Network To manage the connected wallet network, the [addChain](https://viem.sh/docs/actions/wallet/addChain.html) or [switchChain](https://viem.sh/docs/actions/wallet/switchChain.html) functions can be used. **Switch Chain to Lens Chain:** ```ts import { chains } from "@lens-chain/sdk/viem"; import { walletClient } from "./walletClient"; await walletClient.switchChain({ id: chains.mainnet.id }); ``` **Add Lens Chain to Wallet:** ```ts import { chains } from "@lens-chain/sdk/viem"; import { walletClient } from "./walletClient"; await walletClient.addChain({ chain: chains.mainnet }); ``` ### Lens Chain Actions ```ts filename="Example" import { sendRawTransactionWithDetailedOutput } from "@lens-chain/sdk/viem"; import { walletClient } from "./walletClient"; const result = await sendRawTransactionWithDetailedOutput(walletClient, { serializedTransaction: "0x02f8500182031180…", }); ``` ```ts filename="Example" import { deployContract } from "viem/zksync"; import { abi } from "./abi"; import { walletClient } from "./walletClient"; const hash = await deployContract(walletClient, { abi, bytecode: "0x608060405260405161083e38038061083e833…", }); ``` ```ts filename="abi.ts" export const abi = [ { inputs: [], stateMutability: "nonpayable", type: "constructor", }, // … ] as const; ``` ```ts filename="Example" import { getL1ChainId } from "viem/zksync"; import { publicClient } from "./publicClient"; const chainId = await getL1ChainId(publicClient); ``` ```ts filename="Example" import { getL1Balance } from "viem/zksync"; import { publicClient } from "./publicClient"; const balance = await getL1Balance(publicClient, { account: "0x5C221E77624690fff6dd741493D735a17716c26B", }); ``` ### ZKsync Actions - [ZKsync EIP-712 Actions](https://viem.sh/zksync/client#eip712walletactions) for enhanced transaction signing. - [ZKsync L2 Public Actions](https://viem.sh/zksync/actions/estimateFee) for Layer 2 operations. - [ZKsync L1 Public Actions](https://viem.sh/zksync/client#publicactionsl1) for Layer 1 interactions. ## Custom RPC Node If you want to use a Lens Chain RPC node other than the default one, you can specify the custom RPC node URL in the `http` transport. ```ts filename="publicClient.ts" import { createPublicClient, http } from "viem"; import { chains } from "@lens-chain/sdk/viem"; export const publicClient = createPublicClient({ chain: chains.mainnet, transport: http("https://custom-rpc-node.com"), }); ``` ```ts filename="walletClient.ts (Private Key)" import { createWalletClient, Hex, http, privateKeyToAccount } from "viem"; import { chains } from "@lens-chain/sdk/viem"; const account = privateKeyToAccount(process.env.PRIVATE_KEY as Hex); export const walletClient = createWalletClient({ account, chain: chains.mainnet, transport: http("https://custom-rpc-node.com"), }); ``` ================ File: src/pages/chain/integrations/web3py.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Web3.py Interact with Lens Chain using [web.py](https://web3py.readthedocs.io/en/stable/). A template Jupyter Notebook can be found [here](https://github.com/lens-protocol/network-examples). ================ File: src/pages/chain/resources/changelog.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: false, showNext: false, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Changelog All notable changes to this project will be documented in this page. --- This page collects any major change to Lens Chain toolsets. While we will try to keep breaking changes to a minimum, we may need to introduce them as we iterate on the implementations. ## Prepare for Mainnet The [Lens Chains SDK package](https://github.com/lens-protocol/metadata) has reached a **stable 1.0 release** — no functional changes, just a transition to a stable version. Update to: ```bash filename="npm" npm install @lens-chain/sdk@latest ``` ```bash filename="yarn" yarn add @lens-chain/sdk@latest ``` ```bash filename="pnpm" pnpm add @lens-chain/sdk@latest ``` ## Relocate Lens Chain SDK The Lens Chain SDK has been relocated under the `@lens-chain` NPM organization: `@lens-chain/sdk`. ```bash filename="npm" npm uninstall @lens-network/sdk npm install @lens-chain/sdk@canary ``` ```bash filename="yarn" yarn remove @lens-network/sdk yarn add @lens-chain/sdk@canary ``` ```bash filename="pnpm" pnpm remove @lens-network/sdk pnpm add @lens-chain/sdk@canary ``` Amend your import statements to reflect the new package name: ```diff - import { chains } from '@lens-network/sdk'; + import { chains } from '@lens-chain/sdk'; ``` ## Developer Preview Announcement ================ File: src/pages/chain/resources/contracts.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Useful Contracts A list of relevant contracts on Lens Chain. --- ## Lens Chain Mainnet | **Contract** | **Address** | | -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | | [GHO](https://explorer.lens.xyz/address/0x000000000000000000000000000000000000800A) | `0x000000000000000000000000000000000000800A` | | [WETH](https://explorer.lens.xyz/address/0xE5ecd226b3032910CEaa43ba92EE8232f8237553) | `0xE5ecd226b3032910CEaa43ba92EE8232f8237553` | | [WGHO](https://explorer.lens.xyz/address/0x6bDc36E20D267Ff0dd6097799f82e78907105e2F) | `0x6bDc36E20D267Ff0dd6097799f82e78907105e2F` | | [USDC](https://explorer.lens.xyz/address/0x88F08E304EC4f90D644Cec3Fb69b8aD414acf884) | `0x88F08E304EC4f90D644Cec3Fb69b8aD414acf884` | | [BONSAI](https://explorer.lens.xyz/address/0xB0588f9A9cADe7CD5f194a5fe77AcD6A58250f82) | `0xB0588f9A9cADe7CD5f194a5fe77AcD6A58250f82` | | [Multicall3](https://explorer.lens.xyz/address/0x6b6dEa4D80e3077D076733A04c48F63c3BA49320) | `0x6b6dEa4D80e3077D076733A04c48F63c3BA49320` | | [Uniswap V3CoreFactory](https://explorer.lens.xyz/address/0xe0704DB90bcAA1eAFc00E958FF815Ab7aa11Ef47) | `0xe0704DB90bcAA1eAFc00E958FF815Ab7aa11Ef47` | | [Uniswap NonfungiblePositionManager](https://explorer.lens.xyz/address/0xC5d0CAaE8aa00032F6DA993A69Ffa6ff80b5F031) | `0xC5d0CAaE8aa00032F6DA993A69Ffa6ff80b5F031` | | [Uniswap NonfungibleTokenPositionDescriptor](https://explorer.lens.xyz/address/0x57A2190bBE9d65F163948b109E2913B3e2544820) | `0x57A2190bBE9d65F163948b109E2913B3e2544820` | | [Uniswap TransparentUpgradeableProxy](https://explorer.lens.xyz/address/0xaF61588C46cCc374794F68eB5Cfa917ccc0BA173) | `0xaF61588C46cCc374794F68eB5Cfa917ccc0BA173` | | [Uniswap V3Migrator](https://explorer.lens.xyz/address/0xeD70F1DE4397e8Db23ec1cF3D91c63bb7b15022F) | `0xeD70F1DE4397e8Db23ec1cF3D91c63bb7b15022F` | | [Uniswap V3Staker](https://explorer.lens.xyz/address/0x7EE352856858FE4865FBc8c0Acc87e655A035bfe) | `0x7EE352856858FE4865FBc8c0Acc87e655A035bfe` | | [Uniswap QuoterV2](https://explorer.lens.xyz/address/0x1eEA2B790Dc527c5a4cd3d4f3ae8A2DDB65B2af1) | `0x1eEA2B790Dc527c5a4cd3d4f3ae8A2DDB65B2af1` | | [Uniswap SwapRouter02](https://explorer.lens.xyz/address/0x6ddD32cd941041D8b61df213B9f515A7D288Dc13) | `0x6ddD32cd941041D8b61df213B9f515A7D288Dc13` | | [Uniswap Multicall2](https://explorer.lens.xyz/address/0x5900c97b683e69CD752aF7DC7003d69315E2a288) | `0x5900c97b683e69CD752aF7DC7003d69315E2a288` | | [Uniswap ProxyAdmin](https://explorer.lens.xyz/address/0x1447327f093877a2a49fC96D180678a71C4C0C9B) | `0x1447327f093877a2a49fC96D180678a71C4C0C9B` | | [Uniswap TickLens](https://explorer.lens.xyz/address/0x5499510c2e95F59b1Df0eC7C1bd2Fa76347df5Be) | `0x5499510c2e95F59b1Df0eC7C1bd2Fa76347df5Be` | | [Uniswap NFTDescriptorLibrary](https://explorer.lens.xyz/address/0x20b01A0cCbe845552074F1028D94e811d20f11a3) | `0x20b01A0cCbe845552074F1028D94e811d20f11a3` | | [Across SpokePool](https://explorer.lens.xyz/address/0xe7cb3e167e7475dE1331Cf6E0CEb187654619E12) | `0xe7cb3e167e7475dE1331Cf6E0CEb187654619E12` | | [Across MulticallHandler](https://explorer.lens.xyz/address/0xc5939F59b3c9662377DdA53A08D5085b2d52b719) | `0xc5939F59b3c9662377DdA53A08D5085b2d52b719` | To deploy and interact with Safe, use the [Safe CLI w/ custom network](https://docs.safe.global/advanced/cli-reference#use-custom-contracts). ## Lens Chain Testnet | **Contract** | **Address** | | ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | | [GRASS](https://explorer.testnet.lens.xyz/address/0x000000000000000000000000000000000000800A) | `0x000000000000000000000000000000000000800A` | | [WETH](https://explorer.testnet.lens.xyz/address/0xaA91D645D7a6C1aeaa5988e0547267B77d33fe16) | `0xaA91D645D7a6C1aeaa5988e0547267B77d33fe16` | | [WGRASS](https://explorer.testnet.lens.xyz/address/0xeee5a340Cdc9c179Db25dea45AcfD5FE8d4d3eB8) | `0xeee5a340Cdc9c179Db25dea45AcfD5FE8d4d3eB8` | | [Multicall3](https://explorer.testnet.lens.xyz/address/0x8A44EDE8a6843a997bC0Cc4659e4dB1Da8f91116) | `0x8A44EDE8a6843a997bC0Cc4659e4dB1Da8f91116` | | [Safe SimulateTxAccessor](https://explorer.testnet.lens.xyz/address/0x4191E2e12E8BC5002424CE0c51f9947b02675a44) | `0x4191E2e12E8BC5002424CE0c51f9947b02675a44` | | [Safe GnosisSafeProxyFactory](https://explorer.testnet.lens.xyz/address/0xDAec33641865E4651fB43181C6DB6f7232Ee91c2) | `0xDAec33641865E4651fB43181C6DB6f7232Ee91c2` | | [Safe DefaultCallbackHandler](https://explorer.testnet.lens.xyz/address/0x08798512808f838a06BCe7c26905f05e94dF6f50) | `0x08798512808f838a06BCe7c26905f05e94dF6f50` | | [Safe CompatibilityFallbackHandler](https://explorer.testnet.lens.xyz/address/0x2f870a80647BbC554F3a0EBD093f11B4d2a7492A) | `0x2f870a80647BbC554F3a0EBD093f11B4d2a7492A` | | [Safe CreateCall](https://explorer.testnet.lens.xyz/address/0xcB8e5E438c5c2b45FbE17B02Ca9aF91509a8ad56) | `0xcB8e5E438c5c2b45FbE17B02Ca9aF91509a8ad56` | | [Safe MultiSend](https://explorer.testnet.lens.xyz/address/0x0dFcccB95225ffB03c6FBB2559B530C2B7C8A912) | `0x0dFcccB95225ffB03c6FBB2559B530C2B7C8A912` | | [Safe MultiSendCallOnly](https://explorer.testnet.lens.xyz/address/0xf220D3b4DFb23C4ade8C88E526C1353AbAcbC38F) | `0xf220D3b4DFb23C4ade8C88E526C1353AbAcbC38F` | | [Safe SignMessageLib](https://explorer.testnet.lens.xyz/address/0x357147caf9C0cCa67DfA0CF5369318d8193c8407) | `0x357147caf9C0cCa67DfA0CF5369318d8193c8407` | | [Safe GnosisSafeL2](https://explorer.testnet.lens.xyz/address/0x1727c2c531cf966f902E5927b98490fDFb3b2b70) | `0x1727c2c531cf966f902E5927b98490fDFb3b2b70` | | [Safe GnosisSafe](https://explorer.testnet.lens.xyz/address/0xB00ce5CCcdEf57e539ddcEd01DF43a13855d9910) | `0xB00ce5CCcdEf57e539ddcEd01DF43a13855d9910` | | [Safe SingletonFactory](https://explorer.testnet.lens.xyz/address/0xaECDbB0a3B1C6D1Fe1755866e330D82eC81fD4FD) | `0xaECDbB0a3B1C6D1Fe1755866e330D82eC81fD4FD` | | [Ping test contract](https://explorer.testnet.lens.xyz/address/0x06a71429d153026a0F3bAdf481c216FDfe2d0629) | `0x06a71429d153026a0F3bAdf481c216FDfe2d0629` | | [Chainlink CCIP Router](https://explorer.testnet.lens.xyz/address/0xf5Aa9fe2B78d852490bc4E4Fe9ab19727DD10298) | `0xf5Aa9fe2B78d852490bc4E4Fe9ab19727DD10298` | | [Chainlink CCIP OnRamp (Lens Testnet -> ETH Sepolia)](https://explorer.testnet.lens.xyz/address/0x211BF55bFA331e4149bdF624722CbCDB862Ff51D) | `0x211BF55bFA331e4149bdF624722CbCDB862Ff51D` | | [Chainlink CCIP Token Admin Registry](https://explorer.testnet.lens.xyz/address/0x10Cb4265e13801cAcEd7682Bb8B5d2ed6E97964E) | `0x10Cb4265e13801cAcEd7682Bb8B5d2ed6E97964E` | | [Uniswap V3CoreFactory](https://explorer.testnet.lens.xyz/address/0x7eAF6b0646DE8CA11658447b427E62674BFEc9d1) | `0x7eAF6b0646DE8CA11658447b427E62674BFEc9d1` | | [Uniswap Interface Multicall](https://explorer.testnet.lens.xyz/address/0x2b7024F475Ca9fB9A97cF9854DCF340F717c4608) | `0x2b7024F475Ca9fB9A97cF9854DCF340F717c4608` | | [Uniswap TickLens](https://explorer.testnet.lens.xyz/address/0x03a573F5fF9CD05322AfcA9F96e9803027D1D6c3) | `0x03a573F5fF9CD05322AfcA9F96e9803027D1D6c3` | | [Uniswap NFTDescriptor](https://explorer.testnet.lens.xyz/address/0x4fE7eBda649AD36e8Fae8b1A0C686807A56E99d3) | `0x4fE7eBda649AD36e8Fae8b1A0C686807A56E99d3` | | [Uniswap NonfungibleTokenPositionDescriptor](https://explorer.testnet.lens.xyz/address/0x6CBEec8Db2c048cf88bBeAE668b9d51074cF1bb8) | `0x6CBEec8Db2c048cf88bBeAE668b9d51074cF1bb8` | | [Uniswap DescriptorProxy](https://explorer.testnet.lens.xyz/address/0xdC1a3903Ead42bB9a9da5318FEcF86506342F0d8) | `0xdC1a3903Ead42bB9a9da5318FEcF86506342F0d8` | | [Uniswap NonfungibleTokenPositionManager](https://explorer.testnet.lens.xyz/address/0xBAd1E96356123BaB341c8e0031d7935ECD65cDc3) | `0xBAd1E96356123BaB341c8e0031d7935ECD65cDc3` | | [Uniswap V3Migrator](https://explorer.testnet.lens.xyz/address/0xcAE70353BE25165F07e686C5D3cc802383E4EA28) | `0xcAE70353BE25165F07e686C5D3cc802383E4EA28` | | [Uniswap V3Staker](https://explorer.testnet.lens.xyz/address/0x2272c44cC4F1b98B60BE406D61f71b61C4cf2a46) | `0x2272c44cC4F1b98B60BE406D61f71b61C4cf2a46` | | [Uniswap QuoterV2](https://explorer.testnet.lens.xyz/address/0x664fEB2FFCEa14d2AA92095c804c8730DFc0c488) | `0x664fEB2FFCEa14d2AA92095c804c8730DFc0c488` | | [Uniswap SwapRouter02](https://explorer.testnet.lens.xyz/address/0x57a894B5d54658340C50be5B99Fd949b038Ec5DA) | `0x57a894B5d54658340C50be5B99Fd949b038Ec5DA` | To deploy and interact with Safe, use the [Safe CLI w/ custom network](https://docs.safe.global/advanced/cli-reference#use-custom-contracts). To interact with Chainlink CCIP, see [CCIP Guides](https://docs.chain.link/ccip/ccip-javascript-sdk). Lens Chain Sepolia Testnet chain selector: `6827576821754315911`. ## Ethereum Sepolia Testnet | **Contract** | **Address** | | ------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------- | | [Chainlink CCIP OnRamp (ETH Sepolia -> Lens)](https://sepolia.etherscan.io/address/0x8E845C651a8E46a33Af6056E9e6cBBc64EC52732) | `0x8E845C651a8E46a33Af6056E9e6cBBc64EC52732` | To interact with Chainlink CCIP, see [CCIP Guides](https://docs.chain.link/ccip/ccip-javascript-sdk). Ethereum Sepolia chain selector: `16015286601757825753`. ================ File: src/pages/chain/resources/developer-portal.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Developer Portal --- Coming soon. ================ File: src/pages/chain/resources/differences-from-ethereum.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} ## Differences from Ethereum Understand key technical distinctions between Lens Chain and the Ethereum mainnet. --- Lens Chain is a Layer 2 blockchain network built on [ZKsync](https://zksync.io/) technology. As a result, Lens Chain is EVM-compatible, meaning it shares ZKsync's differences from Ethereum and other EVM-equivalent blockchains. The primary differences are outlined below: - [EVM Instructions](https://docs.zksync.io/build/developer-reference/ethereum-differences/evm-instructions) - [Nonces Management](https://docs.zksync.io/build/developer-reference/ethereum-differences/nonces) - [Libraries Linking](https://docs.zksync.io/build/developer-reference/ethereum-differences/libraries) - [Precompiles](https://docs.zksync.io/build/developer-reference/ethereum-differences/pre-compiles) - [Account Abstraction](https://docs.zksync.io/build/developer-reference/ethereum-differences/native-vs-eip4337) - [Contract Deployment](https://docs.zksync.io/build/developer-reference/ethereum-differences/contract-deployment) ================ File: src/pages/chain/resources/network-information.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Network Information Quick reference for developers looking to build on Lens Chain. --- ## Mainnet | Name | Value | | ----------------------------------------------- | --------------------------- | | Network Name | `Lens Chain Mainnet` | | Chain ID | `232` | | RPC URL | `https://rpc.lens.xyz` | | WebSocket URL | `wss://rpc.lens.xyz/ws` | | Currency Symbol | `GHO` | | [Block Explorer URL](https://explorer.lens.xyz) | `https://explorer.lens.xyz` | ## Testnet | Name | Value | | ------------------------------------------------------- | ----------------------------------- | | Network Name | `Lens Chain Testnet` | | Chain ID | `37111` | | RPC URL | `https://rpc.testnet.lens.xyz` | | WebSocket URL | `wss://rpc.testnet.lens.xyz/ws` | | Currency Symbol | `GRASS` | | [Block Explorer URL](https://explorer.testnet.lens.xyz) | `https://explorer.testnet.lens.xyz` | ================ File: src/pages/chain/smart-contracts/foundry.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Foundry Learn how to develop smart contracts on Lens Chain using [Foundry](https://github.com/foundry-rs/foundry). --- This guide is focused on migrating an existing Solidity Foundry project to Lens Chain. ## Limitations Before deciding to use Foundry for development on Lens Chain, it's important to note that Foundry's ZKsync support comes from an [open-source Foundry fork](https://github.com/matter-labs/foundry-zksync) currently in **alpha** stage. Be mindful of the following limitations, among others: - **Compile Time**: Compilation may be slower for some users. - **Specific Foundry Features**: Features like `--gas-report` or `--verify` might not function as expected. Efforts are underway to fully support these features. - **Compiling Libraries**: Non-inlinable libraries require deployment and configuration adjustments. ## Configuration Follow the steps below to configure Foundry for development on Lens Chain. #### Install Foundry ZKsync Clone the Foundry ZKsync repository: ```bash git clone git@github.com:matter-labs/foundry-zksync.git ``` Install Foundry ZKsync: ```bash cd foundry-zksync ./install-foundry-zksync ``` Upon successful installation, you will receive a confirmation message similar to the following: ```bash foundryup-zksync: installed - forge 0.0.2 (6e1c282 2024-07-03T00:22:11.972797000Z) foundryup-zksync: installed - cast 0.0.2 (6e1c282 2024-07-03T00:22:22.891893000Z) foundryup-zksync: done! Verifying installation... Forge version 0.0.2 is successfully installed. ``` After installation, the `forge` and `cast` commands will switch to using the Foundry ZKsync fork. To revert to the original Foundry version, use the `foundryup` command. To switch back to the Foundry ZKsync fork, run `foundryup-zksync`. You can verify the active Foundry version at any time by comparing the version hash with those provided in the confirmation message. ```bash filename="forge -V" $ forge -V 0.0.2 (6e1c282 2024-07-03T00:22:11.972797000Z) ``` ```bash filename="cast -V" $ cast -V 0.0.2 (6e1c282 2024-07-03T00:22:22.891893000Z) ``` #### Project Configuration Add a dedicated profile to your `foundry.toml` file for Lens Chain: ```toml filename="foundry.toml" [profile.default] src = 'src' out = 'out' libs = ['lib'] # other options your default profile might have [profile.zksync] src = 'src' libs = ['lib'] solc-version = "0.8.24" fallback_oz = true is_system = false mode = "3" ``` #### GitIgnore Configuration Add the following to your `.gitignore` file: ```text filename=".gitignore" # ZKsync files zkout/ ``` #### Setup Deployment Wallet To deploy contracts on Lens Chain Sepolia Testnet, you'll need $GRASS tokens from the [faucets](../tools/faucets). Then, create a [Foundry keystore](https://book.getfoundry.sh/reference/cast/cast-wallet-import): ```bash FOUNDRY_PROFILE=zksync cast wallet import myKeystore --interactive ``` You will be prompted to enter your Private Key and a password. Upon successful import, you will receive a confirmation message similar to the following: ```bash `myKeystore` keystore was saved successfully. Address: 0x1234567890abcdef1234567890abcdef12345678 ``` `myKeystore` is the name of the keystore you created. You can replace it with a name of your choice. ## Usage ### Compile Contracts To compile your contracts for Lens Chain, use the following command: ```bash FOUNDRY_PROFILE=zksync forge build --zksync ``` Upon successful compilation, you will receive a confirmation message similar to the following: ```bash [⠃] Compiling (zksync)... Compiler run successful with warnings: ┌──────────────────────────────────────────────────────────────────────────────────────────────────┐ │ Warning: Your code or one of its dependencies uses the 'extcodesize' instruction, which is │ │ usually needed in the following cases: │ │ 1. To detect whether an address belongs to a smart contract. │ │ 2. To detect whether the deploy code execution has finished. │ │ ZKsync Era comes with native account abstraction support (so accounts are smart contracts, │ │ including private-key controlled EOAs), and you should avoid differentiating between contracts │ │ and non-contract addresses. │ └──────────────────────────────────────────────────────────────────────────────────────────────────┘ --> lib/forge-std/src/StdCheats.sol ... ``` Address any warnings as necessary. For more information, consult the [ZKsync Best Practices](https://docs.zksync.io/build/developer-reference/best-practices). ### Deploy Contracts Use the `forge create --zksync` command to deploy your contracts to Lens Chain. ```bash filename="forge create --zksync" FOUNDRY_PROFILE=zksync forge create [OPTIONS] \ --rpc-url \ --chain \ --account \ --from \ --zksync ``` For example: ```bash filename="Lens Chain Sepolia Testnet" FOUNDRY_PROFILE=zksync forge create src/Lock.sol:Lock \ --constructor-args "42" \ --account myKeystore \ --from \ --rpc-url https://rpc.testnet.lens.xyz \ --chain 37111 \ --zksync ``` After entering your keystore password twice, a successful deployment will generate a confirmation message as shown below: ```bash filename="Deployment Confirmation" [⠊] Compiling (zksync)... Enter keystore password: Enter keystore password: Deployer: 0x00A58BA275E6BFC004E2bf9be121a15a2c543e71 Deployed to: 0x5CbF18d3379a7FE3cFFcA34801EDc700eAe49a92 Transaction hash: 0xd3eda207aa6930b3e6b271ff77997921570c41e525689ce1b62c50013b6226fb ``` That's it—you have successfully deployed your contract to Lens Chain. ### Running Tests Use the `forge test --zksync` command to run tests. ```bash filename="forge test --zksync" FOUNDRY_PROFILE=zksync forge test --zksync ``` #### Testing With Hardhat This section will walkthrough how integrate Foundry testing into an existing Hardhat project. A template repository can be found [here](https://github.com/lens-protocol/hardhat-foundry-template). 1. Install the Foundry plugin with your package manager of choice: ```bash npm install --save-dev @nomicfoundation/hardhat-foundry ``` 2. Import the plugin in your Hardhat config (`hardhat.config.ts`): ```ts import "@nomicfoundation/hardhat-foundry"; ``` 3. Initialize Foundry project ```bash npx hardhat init-foundry ``` 4. Modify `foundry.toml` to specify the directory where contracts are located, and add configuration for ZKSync. Example cofnfiguration shown below: ```toml [profile.default] src = 'src' out = 'out' libs = ['lib'] [profile.zksync] src = 'contracts' solc-version = "0.8.24" fallback_oz = true is_system = false mode = "3" test = 'test' script = 'script' cache_path = 'cache_forge' libs = ['node_modules', 'lib'] ``` 5. Add test files (.t.sol) to the `test` directory specified in `foundry.toml`, see [Foundry testing guide](https://book.getfoundry.sh/forge/tests) for instructions on writing tests. 6. Run tests with Foundry ```bash FOUNDRY_PROFILE=zksync forge test --zksync ``` --- ## Troubleshooting ### Library Not Loaded If you encounter an error similar to the following when running `./install-foundry-zksync`: ```bash dyld[10022]: Library not loaded: /opt/homebrew/opt/libusb/lib/libusb-1.0.0.dylib Referenced from: <93EBBD45-018B-39DE-8009-A2662BD3CFE4> /Users/brainjammer/.foundry/bin/forge Reason: tried: '/opt/homebrew/opt/libusb/lib/libusb-1.0.0.dylib' (no such file), '/System/Volumes/Preboot/Cryptexes/OS/opt/homebrew/opt/libusb/lib/libusb-1.0.0.dylib' (no such file), '/opt/homebrew/opt/libusb/lib/libusb-1.0.0.dylib' (no such file) foundryup-zksync: command failed: /Users/brainjammer/.foundry/bin/forge --version foundryup-zksync: installed - dyld[10025]: Library not loaded: /opt/homebrew/opt/libusb/lib/libusb-1.0.0.dylib Referenced from: <2E658B56-FE34-30BF-A410-8E6D46C6B1C5> /Users/brainjammer/.foundry/bin/cast Reason: tried: '/opt/homebrew/opt/libusb/lib/libusb-1.0.0.dylib' (no such file), '/System/Volumes/Preboot/Cryptexes/OS/opt/homebrew/opt/libusb/lib/libusb-1.0.0.dylib' (no such file), '/opt/homebrew/opt/libusb/lib/libusb-1.0.0.dylib' (no such file) foundryup-zksync: command failed: /Users/brainjammer/.foundry/bin/cast --version foundryup-zksync: installed - foundryup-zksync: done! Verifying installation... dyld[10027]: Library not loaded: /opt/homebrew/opt/libusb/lib/libusb-1.0.0.dylib Referenced from: <93EBBD45-018B-39DE-8009-A2662BD3CFE4> /Users/brainjammer/.foundry/bin/forge Reason: tried: '/opt/homebrew/opt/libusb/lib/libusb-1.0.0.dylib' (no such file), '/System/Volumes/Preboot/Cryptexes/OS/opt/homebrew/opt/libusb/lib/libusb-1.0.0.dylib' (no such file), '/opt/homebrew/opt/libusb/lib/libusb-1.0.0.dylib' (no such file) Installation verification failed. Forge is not properly installed. ``` You can resolve this issue by installing `libusb`: ```bash brew install libusb ``` ================ File: src/pages/chain/smart-contracts/hardhat.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Hardhat Learn how to develop smart contracts on Lens Chain using [Hardhat](https://hardhat.org/). --- This guide is focused on Solidity Hardhat projects developed in TypeScript and utilizing [ethers.js](https://docs.ethers.io/). ## Migrate an Existing Project In this section, we'll walk you through the process of migrating an existing Solidity Hardhat project to Lens Chain. ### Compile Contracts The first step in migrating an existing project to Lens Chain is to compile the contracts using the `zksolc` compiler. #### Install Dependencies Start by installing the `@matterlabs/hardhat-zksync` and `zksync-ethers` packages: ```bash filename="npm" npm install -D @matterlabs/hardhat-zksync zksync-ethers ``` ```bash filename="yarn" yarn add -D @matterlabs/hardhat-zksync zksync-ethers ``` #### Update hardhat.config.ts Next, update your `hardhat.config.ts` file as follows: - Import the `@matterlabs/hardhat-zksync` plugin. - Configure the Hardhat Network for ZKsync compatibility. - Add the Lens Testnet network configuration. - Include the `zksolc` compiler options. Example configuration: ```ts filename="hardhat.config.ts" highlight="1,9-12,17-24,27" import "@matterlabs/hardhat-zksync"; import "@nomicfoundation/hardhat-toolbox"; import { HardhatUserConfig } from "hardhat/config"; const config: HardhatUserConfig = { solidity: "0.8.24", zksolc: { version: "latest", settings: {}, }, networks: { // ... lensTestnet: { chainId: 37111, ethNetwork: "sepolia", url: "https://rpc.testnet.lens.xyz", verifyURL: "https://block-explorer-verify.testnet.lens.xyz/contract_verification", zksync: true, }, lensMainnet: { chainId: 232, ethNetwork: "sepolia", url: "https://rpc.lens.xyz", verifyURL: "https://verify.lens.xyz/contract_verification", zksync: true, }, hardhat: { zksync: true, }, }, }; export default config; ``` In case of multiple networks configuration, remember to add `zksync: false` to any other networks. #### Compile Now you can compile the contracts using the `zksolc` compiler: ```bash filename="npm" npm hardhat compile ``` ```bash filename="yarn" yarn hardhat compile ``` This command will compile all contracts in the `/contracts` folder and create the folders `artifacts-zk` and `cache-zk`. Include the `artifacts-zk` and `cache-zk` in your `.gitignore` file alongside the typical Hardhat `artifacts` and `cache` folders. Upon successful compilation, you will receive an output similar to the following: ```bash Compiling contracts for ZKsync Era with zksolc v1.5.1 and zkvm-solc v0.8.24-1.0.1 Compiling 15 Solidity files Generating typings for: 16 artifacts in dir: typechain-types for target: ethers-v6 Successfully generated 52 typings! Successfully compiled 15 Solidity files ``` ### Deploy Contracts With the contracts compiled, it's time to deploy them to Lens Chain. A simple contract example will guide us through the deployment process. ```sol filename="contracts/Storage.sol" // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Storage { // This variable will store the number uint256 private storedNumber; // This event will be emitted whenever the stored number is updated event NumberStored(uint256 newNumber); // Constructor to initialize the stored number constructor(uint256 initialNumber) { storedNumber = initialNumber; emit NumberStored(initialNumber); } // Function to store a new number function storeNumber(uint256 newNumber) public { storedNumber = newNumber; emit NumberStored(newNumber); } // Function to retrieve the stored number function retrieveNumber() public view returns (uint256) { return storedNumber; } } ``` #### Setup Deployment Wallet To deploy contracts on Lens Chain Testnet, you'll need $GRASS tokens from the [faucets](../tools/faucets). In a Hardhat Network development environment, you can bypass this requirement by using [pre-configured rich wallets](https://docs.zksync.io/build/test-and-debug/in-memory-node#pre-configured-rich-wallets). #### Deploy Script Contract deployment varies slightly depending on whether you deploy a regular contract or an upgradeable contract. Within the `deploy` folder, create a deployment script using the `Deployer` class from the `@matterlabs/hardhat-zksync` plugin, as illustrated below: ```ts filename="deploy/deploy-storage.ts" import { Deployer } from "@matterlabs/hardhat-zksync"; import { HardhatRuntimeEnvironment } from "hardhat/types"; import { Wallet } from "zksync-ethers"; export default async function (hre: HardhatRuntimeEnvironment) { // Initialize the wallet. const wallet = new Wallet(""); // Create deployer object and load the artifact of the contract we want to deploy. const deployer = new Deployer(hre, wallet); // Load contract const artifact = await deployer.loadArtifact("Storage"); // `initialNumber` is an argument for contract constructor. const initialNumber = 42; const greeterContract = await deployer.deploy(artifact, [initialNumber]); // Show the contract info. console.log( `${ artifact.contractName } was deployed to ${await greeterContract.getAddress()}` ); } ``` Run the `deploy-zksync` task specifying the script and the network: ```bash filename="npm" npm hardhat deploy-zksync --script deploy-storage.ts --network lensTestnet ``` ```bash filename="yarn" yarn hardhat deploy-zksync --script deploy-storage.ts --network lensTestnet ``` Upon successful deployment of the contract you will receive an output similar to the following: ```bash Running deploy script Storage was deployed to 0xda2BFD327d880A42Ec72E3392E10e43bb32B874F ``` Keep note of the contract address as it will be required for verification. Adapt the contract for upgradeability using the [Transparent Proxy pattern](https://docs.zksync.io/build/tooling/hardhat/hardhat-zksync-upgradable#transparent-upgradable-proxies). Begin by installing the necessary OpenZeppelin packages: ```bash filename="npm" npm install -D "@openzeppelin/contracts@^4.9.6" "@openzeppelin/contracts-upgradeable@^4.9.6" ``` ```bash filename="yarn" yarn add -D "@openzeppelin/contracts@^4.9.6" "@openzeppelin/contracts-upgradeable@^4.9.6" ``` A constraint in a [ZKsync dependency](https://docs.zksync.io/build/tooling/hardhat/hardhat-zksync-upgradable#openzeppelin-version) necessitates the use of specific OpenZeppelin contract versions, as previously mentioned. Then, initialize state variables using an `initialize` function rather than a constructor, adhering to the [Transparent Proxy pattern](https://docs.zksync.io/build/tooling/hardhat/hardhat-zksync-upgradable#transparent-upgradable-proxies). ```sol filename="contracts/UpgradeableStorage.sol" highlight="4,6,13-17" // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/proxy/utils/Initializable.sol"; contract UpgradeableStorage is Initializable { // This variable will store the number uint256 private storedNumber; // This event will be emitted whenever the stored number is updated event NumberStored(uint256 newNumber); // Remove constructor in favour of initialize method function initialize(uint256 initialNumber) public initializer { storedNumber = initialNumber; emit NumberStored(initialNumber); } // Function to store a new number function storeNumber(uint256 newNumber) public { storedNumber = newNumber; emit NumberStored(newNumber); } // Function to retrieve the stored number function retrieveNumber() public view returns (uint256) { return storedNumber; } } ``` Within the `deploy` folder, create a deployment script using the `hre.zkUpgrades.deployProxy` method, as illustrated below: ```ts filename="deploy/deploy-upgradeable-storage.ts" import { Deployer } from "@matterlabs/hardhat-zksync"; import { HardhatRuntimeEnvironment } from "hardhat/types"; import { Provider, Wallet } from "zksync-ethers"; // Import ZKsync Node type extensions import "@matterlabs/hardhat-zksync-node/dist/type-extensions"; // An example of a deploy script that will deploy and call a simple contract. export default async function (hre: HardhatRuntimeEnvironment) { // Initialize ZKsync Provider const provider = new Provider(hre.network.config.url); // Initialize the wallet. const wallet = new Wallet("", provider); // Create deployer object and load the artifact of the contract we want to deploy. const deployer = new Deployer(hre, wallet); // Load contract const artifact = await deployer.loadArtifact("UpgradeableStorage"); const initialNumber = 42; // Deploy the contract using a transparent proxy const contract = await hre.zkUpgrades.deployProxy( wallet, artifact, [initialNumber], { initializer: "initialize" } ); await contract.waitForDeployment(); } ``` Re-compile and run the `deploy-zksync` task specifying the script and the network: ```bash filename="npm" npm hardhat compile npm hardhat deploy-zksync --script deploy-upgradeable-storage.ts --network lensTestnet ``` ```bash filename="yarn" yarn hardhat compile yarn hardhat deploy-zksync --script deploy-upgradeable-storage.ts --network lensTestnet ``` Upon successful deployment of the contract you will receive an output similar to the following: ```bash highlight="3" Implementation contract was deployed to 0x6A4439110728FEe3778B71b5B99229E2beee7664 Admin was deployed to 0x0D859C0987d2374Bc1E41e6eF51e56517C2Fa2dE Transparent proxy was deployed to 0x948bD48B3b90fa8523d2cBB7980dc52dE4448318 ``` Keep note of the proxy address as it will be required for verification. #### Verify Contracts Once the contract is deployed, you can verify it on Lens Chain with the `hardhat verify` command. Note that the verification process differs slightly between regular and upgradeable contracts. Verify the contract specifying the contract address and any constructor arguments: ```bash filename="npm" # npm hardhat verify [] --network lensTestnet npm hardhat verify 0xda2BFD327d880A42Ec72E3392E10e43bb32B874F "42" --network lensTestnet ``` ```bash filename="yarn" # yarn hardhat verify [] --network lensTestnet yarn hardhat verify 0xda2BFD327d880A42Ec72E3392E10e43bb32B874F "42" --network lensTestnet ``` Upon successful verification, you will receive an output similar to the following: ```bash lensTestnet Your verification ID is: 70 Contract successfully verified on ZKsync block explorer! ``` That's it—you've successfully deployed and verified a contract on Lens Chain. Verify the contract specifying the proxy address: ```bash filename="npm" # npm hardhat verify --network lensTestnet npm hardhat verify 0x948bD48B3b90fa8523d2cBB7980dc52dE4448318 --network lensTestnet ``` ```bash filename="yarn" # yarn hardhat verify --network lensTestnet yarn hardhat verify 0x948bD48B3b90fa8523d2cBB7980dc52dE4448318 --network lensTestnet ``` Upon successful verification, you will receive an output similar to the following: ```bash Verifying implementation: 0x6A4439110728FEe3778B71b5B99229E2beee7664 Your verification ID is: 67 Contract successfully verified on ZKsync block explorer! Verifying proxy: 0x948bD48B3b90fa8523d2cBB7980dc52dE4448318 Your verification ID is: 68 Contract successfully verified on ZKsync block explorer! Verifying proxy admin: 0x0D859C0987d2374Bc1E41e6eF51e56517C2Fa2dE Your verification ID is: 69 Contract successfully verified on ZKsync block explorer! ``` That's it—you've successfully deployed and verified an upgradeable contract on Lens Chain. --- ## Create a New Project For those beginning a new project, the [Hardhat boilerplate](https://github.com/lens-protocol/lens-network-hardhat-boilerplate) tailored for Lens Chain is highly recommended. It offers a foundational setup for efficiently deploying and testing smart contracts on Lens Chain. Included in the boilerplate are: - `/contracts`: A sample smart contract. - `/deploy`: Scripts for deploying contracts. - `/test`: Examples of tests for your contracts. - `hardhat.config.ts`: A Hardhat configuration file customized for Lens Chain. ### Getting Started Make sure you have the Node.js **>= v20**. If you use [nvm](https://github.com/nvm-sh/nvm) to manage your Node.js versions, you can run `nvm use` from within the project directory to switch to the correct Node.js version. Enable [Corepack](https://www.totaltypescript.com/how-to-use-corepack), if it isn't already; this will allow you to use the correct [Yarn](https://yarnpkg.com/) version: ```bash corepack enable ``` Then, follow these steps to get started: #### Clone the Repository Clone the boilerplate repository into a new project directory: ```bash git clone https://github.com/lens-protocol/lens-network-hardhat-boilerplate.git my-project cd my-project ``` #### Install Dependencies Install the project dependencies: ```bash yarn install ``` #### Setup Environment Create `.env` file from the `.env.example` template: ```bash cp .env.example .env ``` and populate the `PRIVATE_KEY` environment variable: ```text filename=".env" PRIVATE_KEY=0x… ``` with the private key of an account with Lens Chain tokens. Use [network facuets](../tools/faucets) to obtain tokens for testing. ### Usage The project includes several yarn scripts designed to streamline your workflow: - `yarn compile`: Compiles the contracts. - `yarn deploy --script --network lensTestnet`: Deploys and verifies contracts. - `yarn test`: Executes tests against local ZKsync node. - `yarn clean`: Removes build artifacts from the project. - `yarn lint`: Lints the Solidity code. For detailed instructions on how to utilize these scripts, refer to the project's `README.md` file. ### Utils The `deploy/utils.ts` file contains helper functions for deploying contracts. You can use these functions to streamline the deployment process. #### deployContract(contractName, constructorArgs, options) To deploy regular contracts, you can use the `deployContract` helper as demonstrated below: ```ts filename="deploy/deploy-contract.ts" import { HardhatRuntimeEnvironment } from "hardhat/types"; import { deployTransparentProxy, getWallet } from "./utils"; export default async function (hre: HardhatRuntimeEnvironment) { const wallet = getWallet(); await deployTransparentProxy( "", [ /* Constructor arguments */ ], { hre, wallet, verify: true, } ); } ``` #### deployTransparentProxy(contractName, initializationArgs, options) To deploy upgradeable contracts, you can use the `deployTransparentProxy` helper as demonstrated below: ```ts filename="deploy/deploy-my-upgradeable-contract.ts" import { HardhatRuntimeEnvironment } from "hardhat/types"; import { deployTransparentProxy, getWallet } from "./utils"; export default async function (hre: HardhatRuntimeEnvironment) { const wallet = getWallet(); await deployTransparentProxy( "", [ /** Initialization arguments */ ], { hre, wallet, verify: true, } ); } ``` --- ## Testing In this section, we'll walk through developing a test in Hardhat using the `Storage.sol` contract above. To start, create a new file `Storage.test.ts` within the `test` directory. ```ts import { expect } from "chai"; import { ethers } from "hardhat"; describe("Storage", () => { it("stores and retrieves a number", async () => { const Storage = await ethers.getContractFactory("Storage"); const storage = await Storage.deploy(42); await storage.deployed(); expect(await storage.retrieveNumber()).to.equal(42); }); }); ``` To run this test, execute: ```bash yarn test ``` This test confirms that the `storeNumber` and `retrieveNumber` functions work as intended. For more information on testing techniques and best practices, refer to the [Hardhat Testing Guide](https://hardhat.org/hardhat-runner/docs/guides/test-contracts). Foundry can be added to Hardhat project as a faster alternative for running tests. For more details, see [Testing with Hardhat in Foundry](./foundry#testing-with-hardhat). --- ## Troubleshooting ### Failed Deployment This issue should not impact projects created using the [Hardhat boilerplate](#create-a-new-project). If gas estimation for the deployment of an upgradeable contract fails immediately after deploying the implementation and admin contracts, like so: ```bash Implementation contract was deployed to 0xCCa917109e1fCF7c41f32912b09e9Ee67b1B64D5 Admin was deployed to 0x0D859C0987d2374Bc1E41e6eF51e56517C2Fa2dE An unexpected error occurred: Error: missing revert data (action="estimateGas", data=null, reason=null, transaction={ "data": "0x9c4d535b000000000000000000…" ``` This issue often arises from using an incompatible version of OpenZeppelin contracts. To resolve this issue, ensure you are using the correct versions: ```json filename="package.json" "@openzeppelin/contracts": "^4.9.6", "@openzeppelin/contracts-upgradeable": "^4.9.6", ``` For further details, refer to the [ZKsync documentation](https://docs.zksync.io/build/tooling/hardhat/hardhat-zksync-upgradable#openzeppelin-version). ### Intermittend Test Failures This issue should not impact projects created using the [Hardhat boilerplate](#create-a-new-project). If you experience intermittent test failures when running against the Hardhat network, such as: ```bash Error: No contract at address 0x111C3E89Ce80e62EE88318C2804920D4c96f92bb (Removed from manifest) at validateStoredDeployment (node_modules/@openzeppelin/upgrades-core/src/deployment.ts:153:13) at processTicksAndRejections (node:internal/process/task_queues:95:5) at async validateCached (node_modules/@openzeppelin/upgrades-core/src/deployment.ts:95:14) at async resumeOrDeploy (node_modules/@openzeppelin/upgrades-core/src/deployment.ts:74:21) at async /path/to/your/project/repo/node_modules/@matterlabs/hardhat-zksync-upgradable/src/core/impl-store.ts:49:29 at async Manifest.lockedRun (node_modules/@matterlabs/hardhat-zksync-upgradable/src/core/manifest.ts:150:20) at async fetchOrDeployGeneric (node_modules/@matterlabs/hardhat-zksync-upgradable/src/core/impl-store.ts:39:28) at async deployImpl (node_modules/@matterlabs/hardhat-zksync-upgradable/src/proxy-deployment/deploy-impl.ts:74:24) at async deployProxyImpl (node_modules/@matterlabs/hardhat-zksync-upgradable/src/proxy-deployment/deploy-impl.ts:63:12) at async Proxy.deployProxy (node_modules/@matterlabs/hardhat-zksync-upgradable/src/proxy-deployment/deploy-proxy.ts:50:32) ``` This issue is related to a file generated by the `@matterlabs/hardhat-zksync-upgradable` plugin, part of the `@matterlabs/hardhat-zksync` package. A workaround involves deleting the file ```bash ./.upgradable/ZKsync-era-test-node.json ``` before running tests. To streamline this process, incorporate the following scripts into your `package.json`: ```json filename="package.json" "scripts": { "clean:upgradable": "rimraf ./.upgradable/ZKsync-era-test-node.json", "test": "yarn clean:upgradable && hardhat test" }, ``` Additionally, add this line to your `.gitignore`: ```text filename=".gitignore" # ZKsync files .upgradable/ZKsync-era-test-node.json ``` It's advisable to keep the other files in the `.upgradable` directory version-controlled to facilitate seamless contract upgrades on the Lens Network. ### Task Redefinition Failed This issue should not impact projects created using the [Hardhat boilerplate](#create-a-new-project). If you encounter the error below while running `yarn hardhat test`: ```bash Error HH209: Redefinition of task verify:etherscan failed. Unsupported operation adding mandatory (non optional) param definitions in an overridden task. For more info go to https://hardhat.org/HH209 or run Hardhat with --show-stack-traces ``` This issue is often caused by [Yarn Plug'n'Play (PnP)](https://yarnpkg.com/features/pnp) installation strategy and can be resolved by modifying your `.yarnrc.yml` file as follows: ```yaml filename=".yarnrc.yml" nodeLinker: node-modules ``` Delete the following files: ```bash .pnp.cjs .pnp.loader.mjs ``` Optionally: adding the following to your `.gitignore` file: ```text filename=".gitignore" #!.yarn/cache .pnp.* ``` and then running `yarn install` again. ### No Gas Amount Specified If you are migrating a project that involves sending or transferring native tokens (e.g. Ether on Mainnet, Matic on Polygon, etc.), you are likely to encounter the following error: ```bash filename="Compilation Error" Error: You are using '
.send/transfer()' without providing the gas amount. Such calls will fail depending on the pubdata costs. Please use 'payable(
).call{value: }("")' instead, but be careful with the reentrancy attack. `send` and `transfer` send limited amount of gas that prevents reentrancy, whereas `
.call{value: }` sends all gas to the callee. Learn more about reentrancy at https://docs.soliditylang.org/en/latest/security-considerations.html#reentrancy You may disable this error with: 1. `suppressedErrors = ["sendtransfer"]` in standard JSON. 2. `--suppress-errors sendtransfer` in the CLI. --> contracts/Lock.sol:26:7 | 26 | owner.transfer(address(this).balance); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Error HH600: Compilation failed ``` When dealing with transactions that send or transfer native tokens, avoid using the following patterns: ```sol filename="AVOID" payable(addr).send(x) // or payable(addr).transfer(x) ``` These methods may not provide sufficient gas for calls that involve state changes requiring significant L2 gas for data processing. Instead, opt for the `call` method as shown below: ```sol filename="GOOD" (bool success, ) = addr.call{value: msg.value}(""); require(success, "Transfer failed."); ``` This approach is more reliable for ensuring transactions are processed successfully. For further details, see the [ZKsync best practices](https://docs.zksync.io/build/developer-reference/best-practices#use-call-over-send-or-transfer). ================ File: src/pages/chain/tools/account-abstraction/safe.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Safe The Safe\{Core\} SDK is designed to simplify the integration of Safe smart accounts into your projects, providing a modular and customizable approach to account abstraction. It offers several integration options to cater to different development needs: - [Starter Kit](https://docs.safe.global/sdk/starter-kit): A simplified entry point for deploying new accounts and managing Safe transaction flows, including user operations, multi-signature transactions, and message handling. - [Protocol Kit](https://docs.safe.global/sdk/protocol-kit): Enables interaction with Safe smart accounts, allowing for the creation of new accounts, configuration updates, and transaction execution. - [API Kit](https://docs.safe.global/sdk/api-kit): Facilitates communication with the Safe Transaction Service API, enabling transaction sharing among signers and retrieval of account information such as configurations and transaction histories. - [Relay Kit](https://docs.safe.global/sdk/relay-kit): Supports transaction relaying, allowing users to pay transaction fees from their Safe account using native tokens, ERC-20 tokens, or through sponsored transactions. ## Creating a Safe The guide will walkthrough the process of deploying a 2 of 3 Safe Smart Account to Lens Testnet using the [Protocol Kit](https://docs.safe.global/sdk/protocol-kit) SDK. ### Install Dependencies Install the required dependencies with your package manager of choice: ```bash npm install @safe-global/protocol-kit @lens-chain/sdk@latest ``` ### Import Modules ```js import Safe, { PredictedSafeProps, SafeAccountConfig, } from "@safe-global/protocol-kit"; import { chains } from "@lens-chain/sdk/viem"; ``` ### Initialize Protocol Kit ```js const safeAccountConfig = { owners: [ "0x81EdcF8e0a72c3300087891Bb3E992FAf285b2FC", "0x482c27532517af746358D8E35AfCb3b2ca90A72B", "0x28f875a08F320Cb5Fb6317c6C948fCA8663aC7e9", ], // replace with owner addresses threshold: 2, // replace with multi-signature threshold }; const predictedSafe = { safeAccountConfig, }; const protocolKit = await Safe.init({ provider: chains.testnet.rpcUrls.default, signer: process.env.PRIVATE_KEY, // replace with deployer private key predictedSafe, }); ``` ### Predict Safe Address You can predict the Safe addres before deployment with the following method: ```js const safeAddress = await protocolKit.getAddress(); ``` ### Create Deployment Transaction Create the deployment transaction to deploy a new Safe smart account: ```js const deploymentTransaction = await protocolKit.createSafeDeploymentTransaction(); ``` ### Execute Deployment Transaction ```js const client = await protocolKit.getSafeProvider().getExternalSigner(); const transactionHash = await client.sendTransaction({ to: deploymentTransaction.to, value: BigInt(deploymentTransaction.value), data: deploymentTransaction.data, chain: chains.testnet, }); const transactionReceipt = await client.waitForTransactionReceipt({ hash: transactionHash, }); ``` ### Re-Initialize Protocol Kit Once the deployment transaction is executed, connect the new Safe address to the Protocol Kit instance by calling the connect method. Once connected, operations can be performed by following the [Protocol Kit SDK reference](https://docs.safe.global/reference-sdk-protocol-kit/overview). ```js const newProtocolKit = await protocolKit.connect({ safeAddress, }); const isSafeDeployed = await newProtocolKit.isSafeDeployed(); const safeAddress = await newProtocolKit.getAddress(); const safeOwners = await newProtocolKit.getOwners(); const safeThreshold = await newProtocolKit.getThreshold(); ``` ================ File: src/pages/chain/tools/account-abstraction/thirdweb.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # thirdweb ## thirdweb Account Abstraction [thirdweb](https://portal.thirdweb.com/connect/account-abstraction/overview?utm_source=lens&utm_medium=docs) offers a complete platform to leverage account abstraction. Remove the clunky user experience of requiring gas & signatures for every onchain action. - Abstract away gas - Pre-audited account factory contracts - Built-in infra - Sponsorship policies ## Get Started 1. Sign up for a [free thirdweb account](https://thirdweb.com/team?utm_source=lens&utm_medium=docs) 2. Visit [Account Abstraction Documentation](https://portal.thirdweb.com/connect/account-abstraction/how-it-works?utm_source=lens&utm_medium=docs) and [Account Abstraction Playground](https://playground.thirdweb.com/connect/account-abstraction/connect?utm_source=lens&utm_medium=docs) ## thirdweb Engine [thirdweb Engine](https://portal.thirdweb.com/engine?utm_source=lens&utm_medium=docs) is a performant & secure scalable backend server to connect to the blockchain - **Familiar**: developers’ backend can easily interface with the product via HTTP calls - **Robust**: handles the complete set of of blockchain actions (read, write, signing & funds management) - **Reliable**: executes in a performant way every single time regardless of throughput ## Get Started 1. Sign up for a [free thirdweb account](https://thirdweb.com/team?utm_source=lens&utm_medium=docs) 2. Visit the [engine dashboard](https://thirdweb.com/dashboard/engine?utm_source=lens&utm_medium=docs) 3. Check out this [developers guide](https://blog.thirdweb.com/guides/blockchain-api-basics-series-backend-wallets-with-engine?utm_source=lens&utm_medium=docs) ================ File: src/pages/chain/tools/account-abstraction/zksync.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # ZKsync Paymasters **Paymasters** sponsor user transactions, allowing fees to be paid by apps or other entities, including with ERC-20 tokens. This feature enables better user experience by reducing friction for users to interact with Lens Testnet. For more information on creating a paymaster, see the [ZKsync Paymaster 101 guide](https://docs.zksync.io/zksync-era/guides/zksync-101/paymaster). ================ File: src/pages/chain/tools/bigquery/costs.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Understanding Query Costs BigQuery operates on a pay-as-you-go pricing model. Here's what you need to know: --- ## How Costs Are Calculated 1. **Query Costs**: - Charged based on data processed, not data returned - Only scanned columns count towards processing - For current pricing, check the [official BigQuery pricing page](https://cloud.google.com/bigquery/pricing) 2. **Cost Estimation**: - Estimated data to be processed - Estimated cost - Whether it fits in your free tier ## Cost-Saving Best Practices 1. **Write Efficient Queries**: ```sql -- ❌ Expensive: Scans all columns SELECT * FROM `table_name` -- ✅ Better: Select only needed columns SELECT specific_column1, specific_column2 FROM `table_name` ``` 2. **Use LIMIT While Testing**: ```sql -- Always add LIMIT when testing new queries SELECT column1, column2 FROM `table_name` LIMIT 100 ``` 3. **Leverage Table Design**: - Use partitioned tables to filter by date ranges - Use clustered columns for frequently filtered fields - Keep frequently joined columns in the same table when possible 4. **Monitor Usage**: - Set up billing alerts in Google Cloud Console - Review "Query History" for cost patterns - Set project-level quotas to prevent overages **Pro Tip**: Always use the "Query Validation" button before running large queries to check processing costs and avoid unexpected charges. ================ File: src/pages/chain/tools/bigquery/examples.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # BigQuery Usage Examples Learn how to interact with Lens Chain datasets using different programming languages and tools. --- ## Python Using the official Google Cloud client library: ```python filename="Mainnet" from google.cloud import bigquery # Initialize the client client = bigquery.Client() # Query recent publications query = """ SELECT address, FROM `lens-chain-mainnet.public.addresses` addresses WHERE LIMIT 5 """ # Execute the query query_job = client.query(query) # Process results for row in query_job: print(f"Handle: {row.handle}, Posted at: {row.block_timestamp}") ``` ```python filename="Testnet" from google.cloud import bigquery # Initialize the client client = bigquery.Client() # Query recent publications query = """ SELECT address, FROM `lens-chain-testnet.public.addresses` addresses WHERE LIMIT 5 """ # Execute the query query_job = client.query(query) # Process results for row in query_job: print(f"Handle: {row.handle}, Posted at: {row.block_timestamp}") ``` ## Node.js Using the `@google-cloud/bigquery` package: ```javascript filename="Mainnet" const {BigQuery} = require('@google-cloud/bigquery'); async function queryLens() { const bigquery = new BigQuery(); const query = ` SELECT * FROM \`lens-chain-mainnet.public.transactions\` ORDER BY createdAt DESC LIMIT 10 `; const [rows] = await bigquery.query(query); console.log('Latest 10 transactions:', rows); } ``` ```javascript filename="Testnet" const {BigQuery} = require('@google-cloud/bigquery'); async function queryLens() { const bigquery = new BigQuery(); const query = ` SELECT * FROM \`lens-chain-testnet.public.transactions\` ORDER BY createdAt DESC LIMIT 10 `; const [rows] = await bigquery.query(query); console.log('Latest 10 transactions:', rows); } ``` ## REST API Using the BigQuery REST API: ```bash filename="Mainnet" curl -X POST \ -H "Authorization: Bearer $(gcloud auth print-access-token)" \ -H "Content-Type: application/json" \ https://bigquery.googleapis.com/bigquery/v2/projects/lens-public-data/queries \ -d '{ "query": "SELECT COUNT(*) as total_addresses FROM `lens-chain-mainnet.public.addresses`" }' ``` ```bash filename="Testnet" curl -X POST \ -H "Authorization: Bearer $(gcloud auth print-access-token)" \ -H "Content-Type: application/json" \ https://bigquery.googleapis.com/bigquery/v2/projects/lens-public-data/queries \ -d '{ "query": "SELECT COUNT(*) as total_addresses FROM `lens-chain-testnet.public.addresses`" }' ``` ## Common Query Examples ### Get lastest transactions ```sql SELECT * FROM lens-chain-testnet.public.transactions order by "createdAt" desc limit 10; ``` Remember to handle authentication appropriately in your applications. For local development, you can use the [Google Cloud CLI](https://cloud.google.com/sdk/docs/install). ================ File: src/pages/chain/tools/bigquery/introduction.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; import { Tabs } from '@/components/Common/Tabs'; import { TabItem } from '@/components/Common/Tabs'; export default ({ children }) => {children}; # Lens Chain Public Datasets Lens Chain on BigQuery --- ## Overview Lens Chain public datasets provide comprehensive access to all public data through Google BigQuery. This enables developers, analysts, and researchers to query and analyze social data from the Lens ecosystem using standard SQL. ## Data Format In our source most of the data are stored in their raw binary format. When syncing with BigQuery, we transform this byte data into hexadecimal strings with the format `\x{string}`. ### FORMAT_HEX Function To make the data Web3-friendly, we provide a public function called `FORMAT_HEX` that converts BigQuery's `\x` format to the standard Web3 `0x` format. Here's how to use it: ```sql filename="Mainnet" SELECT address as original_address, `lens-chain-mainnet.public.FORMAT_HEX`(address) as web3_formatted_address FROM `lens-chain-mainnet.public.addresses` LIMIT 5; ``` ```sql filename="Testnet" SELECT address as original_address, `lens-chain-testnet.public.FORMAT_HEX`(address) as web3_formatted_address FROM `lens-chain-testnet.public.addresses` LIMIT 5; ``` This will return results like: | original_address | web3_formatted_address | | ------------------------------------------ | ------------------------------------------ | | \x18e768e60d7d0b55a1a541f5771d2b18923fd264 | 0x18e768e60d7d0b55a1a541f5771d2b18923fd264 | | \x0000000000000000000000000000000000000000 | 0x0000000000000000000000000000000000000000 | | \x24662809c579cb21b78f7c7e694868437ef0ff72 | 0x24662809c579cb21b78f7c7e694868437ef0ff72 | | \x3cb3246cfc9f48d00ae431543512033dd1fd7819 | 0x3cb3246cfc9f48d00ae431543512033dd1fd7819 | | \x1555c0020241a007618560f8b5e65f55b2ba1963 | 0x1555c0020241a007618560f8b5e65f55b2ba1963 | All data in these datasets is public information from Lens Chain. The datasets are update**d eve**ry 15 minutes to reflect the latest network activity. ================ File: src/pages/chain/tools/bigquery/schemas.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; import { Tabs } from '@/components/Common/Tabs'; import { TabItem } from '@/components/Common/Tabs'; export default ({ children }) => {children}; {/* Start of the page content */} # Lens Chain BigQuery Schemas Tables Schema of Lens Chain BigQuery. --- ## addresses ```sql filename="Mainnet" SELECT * FROM `lens-chain-mainnet.public.addresses` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-chain-testnet.public.addresses` LIMIT 1; ``` This table stores information about all addresses on the network, including smart contracts. | Column | Type | Description | |--------|------|-------------| | createdAt | timestamp | Timestamp when this record was created | | updatedAt | timestamp | Timestamp when this record was last updated | | address | bytea | The blockchain address (in bytes format) | | bytecode | bytea | Contract bytecode for smart contract addresses | | createdInBlockNumber | bigint | Block number when this address was created | | creatorTxHash | bytea | Transaction hash that created this address | | creatorAddress | bytea | Address that created this address (contract deployer) | | createdInLogIndex | integer | Log index within the block where this address was created | | isEvmLike | boolean | Whether the address follows EVM address format | [Back to top](#lens-chain-bigquery-schemas) ## blocks ```sql filename="Mainnet" SELECT * FROM `lens-chain-mainnet.public.blocks` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-chain-testnet.public.blocks` LIMIT 1; ``` This table contains information about each block in the blockchain. | Column | Type | Description | |--------|------|-------------| | createdAt | timestamp | Timestamp when this record was created | | updatedAt | timestamp | Timestamp when this record was last updated | | number | bigint | Block number | | nonce | varchar | Block nonce value | | difficulty | integer | Mining difficulty at this block | | gasLimit | varchar(128) | Maximum gas allowed in this block | | gasUsed | varchar(128) | Total gas used by all transactions in this block | | baseFeePerGas | varchar(128) | Base fee per gas unit in this block | | l1BatchNumber | bigint | Layer 1 batch number that includes this block | | l1TxCount | integer | Number of L1 transactions in this block | | l2TxCount | integer | Number of L2 transactions in this block | | hash | bytea | Block hash | | parentHash | bytea | Hash of the parent block | | miner | bytea | Address of the miner/validator who produced this block | | extraData | bytea | Additional data included in the block | | timestamp | timestamp | Timestamp when the block was mined | [Back to top](#lens-chain-bigquery-schemas) ## transactions ```sql filename="Mainnet" SELECT * FROM `lens-chain-mainnet.public.transactions` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-chain-testnet.public.transactions` LIMIT 1; ``` This table stores all transactions processed on the network. | Column | Type | Description | |--------|------|-------------| | createdAt | timestamp | Timestamp when this record was created | | updatedAt | timestamp | Timestamp when this record was last updated | | number | bigint | Unique transaction identifier | | nonce | bigint | Transaction nonce (unique per sender address) | | transactionIndex | integer | Index position of transaction in the block | | gasLimit | varchar(128) | Maximum gas allowed for this transaction | | gasPrice | varchar(128) | Gas price in wei | | maxFeePerGas | varchar(128) | Maximum fee per gas (EIP-1559) | | maxPriorityFeePerGas | varchar(128) | Maximum priority fee per gas (EIP-1559) | | value | varchar(128) | Amount of cryptocurrency transferred | | chainId | integer | Chain identifier | | blockNumber | bigint | Block number where this transaction was included | | type | integer | Transaction type | | accessList | jsonb | Access list for EIP-2930 transactions | | l1BatchNumber | bigint | Layer 1 batch number that includes this transaction | | fee | varchar | Transaction fee | | isL1Originated | boolean | Whether the transaction originated from Layer 1 | | receivedAt | timestamp | Timestamp when the transaction was received by the network | | hash | bytea | Transaction hash | | to | bytea | Recipient address | | from | bytea | Sender address | | data | bytea | Transaction data/input | | blockHash | bytea | Hash of the block containing this transaction | | receiptStatus | integer | Transaction receipt status (1 = success, 0 = failure) | | gasPerPubdata | varchar | Gas per public data | | error | varchar | Error message if the transaction failed | | revertReason | varchar | Reason for transaction reversion if applicable | [Back to top](#lens-chain-bigquery-schemas) ## transactionReceipts ```sql filename="Mainnet" SELECT * FROM `lens-chain-mainnet.public.transactionReceipts` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-chain-testnet.public.transactionReceipts` LIMIT 1; ``` This table contains the receipts generated after transaction execution. | Column | Type | Description | |--------|------|-------------| | createdAt | timestamp | Timestamp when this record was created | | updatedAt | timestamp | Timestamp when this record was last updated | | number | bigint | Unique receipt identifier | | transactionIndex | integer | Index position of transaction in the block | | type | integer | Receipt type | | gasUsed | varchar(128) | Amount of gas used by this transaction | | effectiveGasPrice | varchar(128) | Effective gas price for this transaction | | blockNumber | bigint | Block number containing this transaction | | cumulativeGasUsed | varchar(128) | Cumulative gas used up to this transaction in the block | | byzantium | boolean | Whether the receipt uses Byzantium format | | status | integer | Transaction status code | | transactionHash | bytea | Hash of the transaction | | to | bytea | Recipient address | | from | bytea | Sender address | | contractAddress | bytea | Address of newly created contract, if applicable | | root | bytea | State root (pre-Byzantium) | | logsBloom | bytea | Bloom filter for indexed event logs | | blockHash | bytea | Hash of the block containing this transaction | [Back to top](#lens-chain-bigquery-schemas) ## logs ```sql filename="Mainnet" SELECT * FROM `lens-chain-mainnet.public.logs` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-chain-testnet.public.logs` LIMIT 1; ``` This table stores event logs emitted during transaction execution. | Column | Type | Description | |--------|------|-------------| | createdAt | timestamp | Timestamp when this record was created | | updatedAt | timestamp | Timestamp when this record was last updated | | number | bigint | Unique log identifier | | blockNumber | bigint | Block number containing this log | | transactionIndex | integer | Index position of transaction in the block | | removed | boolean | Whether the log was removed due to chain reorganization | | logIndex | integer | Index position of log in the transaction | | transactionHash | bytea | Hash of the transaction that generated this log | | address | bytea | Address that generated this log | | data | bytea | Non-indexed log parameters | | topics | ARRAY | Indexed log topics | | timestamp | timestamp | Timestamp of the block containing this log | [Back to top](#lens-chain-bigquery-schemas) ## transfers ```sql filename="Mainnet" SELECT * FROM `lens-chain-mainnet.public.transfers` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-chain-testnet.public.transfers` LIMIT 1; ``` This table records all asset transfers that occur on the network. | Column | Type | Description | |--------|------|-------------| | createdAt | timestamp | Timestamp when this record was created | | updatedAt | timestamp | Timestamp when this record was last updated | | number | bigint | Unique transfer identifier | | blockNumber | bigint | Block number containing this transfer | | amount | varchar(128) | Amount transferred | | type | USER-DEFINED | Transfer type | | fields | jsonb | Additional fields specific to this transfer | | from | bytea | Sender address | | to | bytea | Recipient address | | transactionHash | bytea | Hash of the transaction that caused this transfer | | tokenAddress | bytea | Address of the token being transferred | | logIndex | integer | Index position of the log that recorded this transfer | | transactionIndex | integer | Index position of transaction in the block | | timestamp | timestamp | Timestamp of the block containing this transfer | | isFeeOrRefund | boolean | Whether this transfer is a fee payment or refund | | isInternal | boolean | Whether this is an internal transfer | | tokenType | USER-DEFINED | Type of token being transferred | [Back to top](#lens-chain-bigquery-schemas) ## addressTransfers ```sql filename="Mainnet" SELECT * FROM `lens-chain-mainnet.public.addressTransfers` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-chain-testnet.public.addressTransfers` LIMIT 1; ``` This table maps addresses to their transfers for efficient querying. | Column | Type | Description | |--------|------|-------------| | createdAt | timestamp | Timestamp when this record was created | | updatedAt | timestamp | Timestamp when this record was last updated | | number | bigint | Unique record identifier | | transferNumber | bigint | Reference to the transfer.number | | address | bytea | The address involved in the transfer | | blockNumber | bigint | Block number containing this transfer | | timestamp | timestamp | Timestamp of the block containing this transfer | | isFeeOrRefund | boolean | Whether this transfer is a fee payment or refund | | logIndex | integer | Index position of the log that recorded this transfer | | tokenAddress | bytea | Address of the token being transferred | | fields | jsonb | Additional fields specific to this transfer | | isInternal | boolean | Whether this is an internal transfer | | tokenType | USER-DEFINED | Type of token being transferred | | type | USER-DEFINED | Transfer type | [Back to top](#lens-chain-bigquery-schemas) ## addressTransactions ```sql filename="Mainnet" SELECT * FROM `lens-chain-mainnet.public.addressTransactions` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-chain-testnet.public.addressTransactions` LIMIT 1; ``` This table maps addresses to transactions for efficient querying. | Column | Type | Description | |--------|------|-------------| | createdAt | timestamp | Timestamp when this record was created | | updatedAt | timestamp | Timestamp when this record was last updated | | number | bigint | Unique record identifier | | transactionHash | bytea | Transaction hash | | address | bytea | Address involved in the transaction | | blockNumber | bigint | Block number containing this transaction | | receivedAt | timestamp | Timestamp when the transaction was received | | transactionIndex | integer | Index position of transaction in the block | [Back to top](#lens-chain-bigquery-schemas) ## tokens ```sql filename="Mainnet" SELECT * FROM `lens-chain-mainnet.public.tokens` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-chain-testnet.public.tokens` LIMIT 1; ``` This table stores information about tokens on the network. | Column | Type | Description | |--------|------|-------------| | createdAt | timestamp | Timestamp when this record was created | | updatedAt | timestamp | Timestamp when this record was last updated | | number | bigint | Unique token identifier | | symbol | varchar | Token symbol | | name | varchar | Token name | | decimals | integer | Number of decimal places | | blockNumber | bigint | Block number when this token was created | | l2Address | bytea | Layer 2 address of this token | | l1Address | bytea | Layer 1 address of this token | | transactionHash | bytea | Hash of the transaction that created this token | | logIndex | integer | Index position of the log that recorded this token creation | | usdPrice | double precision | Current USD price | | liquidity | double precision | Token liquidity | | iconURL | varchar | URL to token icon | | offChainDataUpdatedAt | timestamp | Timestamp when off-chain data was last updated | [Back to top](#lens-chain-bigquery-schemas) ### balances ```sql filename="Mainnet" SELECT * FROM `lens-chain-mainnet.public.balances` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-chain-testnet.public.balances` LIMIT 1; ``` This table tracks token balances for addresses. | Column | Type | Description | |--------|------|-------------| | createdAt | timestamp | Timestamp when this record was created | | updatedAt | timestamp | Timestamp when this record was last updated | | address | bytea | Owner address | | tokenAddress | bytea | Token address | | blockNumber | bigint | Block number at which this balance was recorded | | balance | varchar(128) | Token balance amount | [Back to top](#lens-chain-bigquery-schemas) ## batches ```sql filename="Mainnet" SELECT * FROM `lens-chain-mainnet.public.batches` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-chain-testnet.public.batches` LIMIT 1; ``` This table stores information about Layer 2 batch processing for rollups. | Column | Type | Description | |--------|------|-------------| | createdAt | timestamp | Timestamp when this record was created | | updatedAt | timestamp | Timestamp when this record was last updated | | number | bigint | Batch number | | rootHash | bytea | Root hash of the batch | | l1GasPrice | varchar(128) | Layer 1 gas price at the time of this batch | | l2FairGasPrice | varchar(128) | Layer 2 fair gas price for this batch | | commitTxHash | bytea | Hash of the transaction that committed this batch | | committedAt | timestamp | Timestamp when this batch was committed | | proveTxHash | bytea | Hash of the transaction that proved this batch | | provenAt | timestamp | Timestamp when this batch was proven | | executeTxHash | bytea | Hash of the transaction that executed this batch | | executedAt | timestamp | Timestamp when this batch was executed | | l1TxCount | integer | Number of Layer 1 transactions in this batch | | l2TxCount | integer | Number of Layer 2 transactions in this batch | | timestamp | timestamp | Timestamp of the batch | [Back to top](#lens-chain-bigquery-schemas) ### counterStates ```sql filename="Mainnet" SELECT * FROM `lens-chain-mainnet.public.counterStates` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-chain-testnet.public.counterStates` LIMIT 1; ``` This table tracks the processing state of various tables for synchronization purposes. | Column | Type | Description | |--------|------|-------------| | createdAt | timestamp | Timestamp when this record was created | | updatedAt | timestamp | Timestamp when this record was last updated | | tableName | varchar(64) | Name of the table being tracked | | lastProcessedRecordNumber | bigint | Last processed record number | [Back to top](#lens-chain-bigquery-schemas) ## counters ```sql filename="Mainnet" SELECT * FROM `lens-chain-mainnet.public.counters` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-chain-testnet.public.counters` LIMIT 1; ``` This table maintains various counters for database operations. | Column | Type | Description | |--------|------|-------------| | createdAt | timestamp | Timestamp when this record was created | | updatedAt | timestamp | Timestamp when this record was last updated | | id | uuid | Unique counter identifier | | count | bigint | Current counter value | | tableName | varchar(64) | Table this counter is associated with | | queryString | varchar | Query string used for this counter | [Back to top](#lens-chain-bigquery-schemas) ================ File: src/pages/chain/tools/bigquery/setup.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; # Quick Start Guide Get started with querying Lens Chain data using BigQuery in minutes. --- ## Prerequisites Before you begin, ensure you have: - A Google Cloud account ([Sign up here](https://console.cloud.google.com)) - A Google Cloud project with billing enabled - BigQuery API access enabled New Google Cloud users get $300 in free credits valid for 90 days. For current pricing details, visit the [official BigQuery pricing page](https://cloud.google.com/bigquery/pricing). ## Setup Process ### Create or Select a Project 1. Go to [Google Cloud Console](https://console.cloud.google.com) 2. In the top navigation bar, click on the project dropdown menu 3. For a [new project](https://developers.google.com/workspace/guides/create-project#google-cloud-console): - Click "New Project" - Enter a project name (e.g., "lens-analytics") - Optional: Select an organization and location - Click "CREATE" 4. For an existing project: - Click on the project selector - Use the search bar to find your project - Select the project from the list Make sure you have sufficient permissions (Owner or Editor role) for the project you select. ### Enable Billing 1. Go to the [Google Cloud Console Billing page](https://console.cloud.google.com/billing) 2. Select your project 3. Click "Link a billing account" 4. Either select an existing billing account or create a new one ### Enable BigQuery API 1. Go to [BigQuery API page](https://console.cloud.google.com/apis/library/bigquery.googleapis.com) 2. Click "Enable" ### Access Lens Datasets 1. Open [BigQuery Console](https://console.cloud.google.com/bigquery) 2. In the query editor, paste and run this query to explore a table's schema: ```sql SELECT table_name FROM `lens-chain-testnet.public.INFORMATION_SCHEMA.TABLES`; ``` **Remember**: Always verify your query's cost before running it by clicking the "Query Validation" button. ================ File: src/pages/chain/tools/bridging/across.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; # Across Across is an interoperability protocol powered by intents, enabling a fast and low-cost way to transfer value between chains. The process for intent-based bridging with Across: 1. Users deposit tokens on the origin chain 2. Relayer views this deposit and fulfills the order on the destination chain 3. Relay submits a proof of the relay and deposit to optimisitc oracle and claims reimbursement Across bridging can be accessed via [Mainnet Bridge UI](https://app.across.to/bridge), [Testnet Bridge UI](https://testnet.across.to/bridge), or onchain transactions constructed via [Across SDK](#across-sdk) or [Across API](#across-api). ## Across SDK The [Across SDK](https://docs.across.to/reference/app-sdk-reference) enables developers to interact with the protocol in JavaScript applications. This guide will walkthrough exeucting an example bridge transaction. More details on available SDK methods can be found on [GitHub](https://github.com/across-protocol/toolkit/blob/master/packages/sdk/README.md#acrossclient). ### Install Install using your preferred package manager. ```bash filename="npm" npm install @across-protocol/app-sdk @lens-chain/sdk@latest viem ``` ```bash filename="yarn" yarn add @across-protocol/app-sdk @lens-chain/sdk@latest viem ``` ```bash filename="pnpm" pnpm add @across-protocol/app-sdk @lens-chain/sdk@latest viem ``` ### Setup AcrossClient Initialize AcrossClient and configure the chains you want to support. ```ts import { chains } from "@lens-chain/sdk/viem"; import { createAcrossClient } from "@across-protocol/app-sdk"; import { sepolia } from "viem/chains"; const client = createAcrossClient({ integratorId: "0xdead", // 2-byte hex string chains: [chains.testnet, sepolia], useTestnet: true, }); ``` ### Get Available Routes Get available routes based on network and/or token address parameters. ```ts // available params: originToken, destinationToken, destinationChainId, originChainId, originTokenSymbol, destinationTokenSymbol const options = { originChainId: 37111 }; const routes = await client.getAvailableRoutes(options); ``` ### Get Quote Get price and fee estimate for requested route. ```ts import { parseEther } from "viem"; // Bridge 1 WGRASS from Lens Testnet to ETH Sepolia const route = { originChainId: chains.testnet.id, destinationChainId: sepolia.id, inputToken: "0xeee5a340Cdc9c179Db25dea45AcfD5FE8d4d3eB8", // WGRASS Lens Testnet outputToken: "0x2Be68B15c693D3b5747F9F0D49D30A2E81BAA2Df", // WGRASS Ethereum Sepolia }; const quote = await client.getQuote({ route, inputAmount: parseEther("1"), }); ``` ### Execute Quote Execute deposit transaction for generated quote. To generate transaction manually (for simulations, debugging, etc.), see [here](#across-sdk-simulate-deposit-transaction-data). ```ts import { parseEther, http, createWalletClient } from "viem"; import { privateKeyToAccount } from "viem/accounts"; const account = privateKeyToAccount("PRIVATE_KEY"); const walletClient = createWalletClient({ account, chain: chains.testnet, transport: http(), }); await client.executeQuote({ walletClient, deposit: quote.deposit, onProgress: (progress) => { if (progress.step === "approve" && progress.status === "txSuccess") { // if approving an ERC20, you have access to the approval receipt const { txReceipt } = progress; } if (progress.step === "deposit" && progress.status === "txSuccess") { // once deposit is successful you have access to depositId and the receipt const { depositId, txReceipt } = progress; } if (progress.step === "fill" && progress.status === "txSuccess") { // if the fill is successful, you have access the following data const { fillTxTimestamp, txReceipt, actionSuccess } = progress; // actionSuccess is a boolean flag, telling us if your cross chain messages were successful } }, }); ``` ### Simulate Deposit Transaction Data ```ts import { parseEther, http, createWalletClient } from "viem"; import { privateKeyToAccount } from "viem/accounts"; const account = privateKeyToAccount("PRIVATE_KEY"); const walletClient = createWalletClient({ account, chain: chains.testnet, transport: http(), }); const { request } = await client.simulateDepositTx({ walletClient, deposit: quote.deposit, }); // `request` is viem Transaction which can be simulated with tools such as Tenderly or executed onchain with `walletClient.writeContract(request)`, though it is recommended to execute using "Execute Quote" method above for monitoring callbacks ``` ## Across API The [Across API](https://docs.across.to/reference/api-reference) enables developers to query data for Across bridge interactions. Available methods are listed below: - [Get Available Routes](https://docs.across.to/reference/api-reference#available-routes) - [Get Route Limits](https://docs.across.to/reference/api-reference#limits) - [Get Route Quote](https://docs.across.to/reference/api-reference#suggested-fees) - [Get Transaction Status](https://docs.across.to/reference/api-reference#deposit-status) ## Available Routes A complete list of routes can be queried using the [SDK](#across-sdk#get-available-routes) or [API](#across-api). Common routes are listed below: ### Testnet - [Ethereum Sepolia WGRASS](https://sepolia.etherscan.io/address/0x2Be68B15c693D3b5747F9F0D49D30A2E81BAA2Df) \<-\> [Lens Testnet WGRASS](https://explorer.testnet.lens.xyz/address/0xeee5a340Cdc9c179Db25dea45AcfD5FE8d4d3eB8) ================ File: src/pages/chain/tools/bridging/lens.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; # Lens Bridge The official Lens Bridge allows you to transfer assets between various EVM networks and the Lens Chain. - Mainnet: [https://lens.xyz/bridge](https://lens.xyz/bridge) - Testnet: _Coming soon_ ================ File: src/pages/chain/tools/bridging/zksync.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; # ZKsync Bridge The ZKsync native bridge can be used to bridge assets between Ethereum Sepolia and Lens Testnet. - Mainnet: Coming soon - Testnet: [https://portal.testnet.lens.dev/bridge](https://portal.testnet.lens.dev/bridge) ================ File: src/pages/chain/tools/cross-chain/ccip.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; # Chainlink CCIP Chainlink CCIP is a blockchain interoperability protocol that enables developers to build secure applications that can transfer tokens and messages (data) across chains. For more information, see [Chainlink CCIP Guides](https://docs.chain.link/ccip/ccip-javascript-sdk). ## Mainnet Deployed Contracts Coming soon ## Testnet Deployed Contracts | **Contract** | **Address** | | ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | | [Chainlink CCIP Router](https://explorer.testnet.lens.xyz/address/0xf5Aa9fe2B78d852490bc4E4Fe9ab19727DD10298) | `0xf5Aa9fe2B78d852490bc4E4Fe9ab19727DD10298` | | [Chainlink CCIP OnRamp (Lens Testnet -> ETH Sepolia)](https://explorer.testnet.lens.xyz/address/0x211BF55bFA331e4149bdF624722CbCDB862Ff51D) | `0x211BF55bFA331e4149bdF624722CbCDB862Ff51D` | | [Chainlink CCIP Token Admin Registry](https://explorer.testnet.lens.xyz/address/0x10Cb4265e13801cAcEd7682Bb8B5d2ed6E97964E) | `0x10Cb4265e13801cAcEd7682Bb8B5d2ed6E97964E` | | [Chainlink CCIP OnRamp (ETH Sepolia -> Lens Testnet)](https://sepolia.etherscan.io/address/0x8E845C651a8E46a33Af6056E9e6cBBc64EC52732) | `0x8E845C651a8E46a33Af6056E9e6cBBc64EC52732` | ## Chain Selectors - Mainnet: Coming soon - Lens Chain Testnet: 6827576821754315911 - Ethereum Sepolia Testnet: 16015286601757825753 ================ File: src/pages/chain/tools/cross-chain/zksync-elastic.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; # ZKsync Elastic Chain Lens Testnet is built on the ZKsync stack, enabling a robust, secure infrastructure for zero-knowledge rollups and cross-chain interoperability. Elastic chain enables trustless asset transfers and fast cross-chain interactions between ZKsync chains. For infomation on utilizing Elastic Chain architecture, check out the [ZKsync documentation](https://docs.zksync.io/zk-stack/concepts/zk-chains#types-of-bridges-in-the-elastic-chain-ecosystem). ================ File: src/pages/chain/tools/data-indexers/covalent.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Covalent The [GoldRush API](https://goldrush.dev) from [Covalent](https://www.covalenthq.com/) provides a unified API to query data points across multiple networks, including Lens Testnet. To start using Covalent’s GoldRush, you'll need an API key, which you can generate by signing up at [GoldRush](https://goldrush.dev/platform/auth/register/). Once you have your API key, you can explore integration options: - [GoldRush APIs](https://goldrush.dev/docs/goldrush-apis) - [GoldRush SDKs](https://goldrush.dev/docs/goldrush-sdks) (TypeScript, Python) - [GoldRush UI Kit](https://goldrush.dev/docs/goldrush-ui-kit) (React components) - [GoldRush Decoder](https://goldrush.dev/docs/goldrush-decoder) ## Wallet Balances Example Below is an example fetching token balances for a specific wallet address across one or multiple chains. You can specify chains as a comma separated list of chain tags (Lens Chain tags are `lens-sepolia-testnet` and `lens-mainnet`). ### API ```bash curl --request GET \ --url "https://api.covalenthq.com/v1/allchains/address/INSERT_ADDRESS_HERE/balances/?chains=lens-sepolia-testnet,lens-mainnet&key=INSERT_API_KEY_HERE" \ --header 'accept: application/json' ``` ### JavaScript SDK Install SDK with your package manager of choice: ```bash npm install @covalenthq/client-sdk" ``` ```js import { GoldRushClient } from "@covalenthq/client-sdk"; const client = new GoldRushClient("INSERT_API_KEY_HERE"); const fetchBalances = async (walletAddress) => { try { const response = await client.AllChainsService.getTokenBalances({ chains: "lens-sepolia-testnet", addresses: walletAddress, }); // If the API returns an error property, handle accordingly: if (response.error) { throw response; } console.log(response.data); } catch (error) { console.error(error); } }; fetchBalances("INSERT_ADDRESS_HERE"); ``` ## Transactions Example Similarly, you can fetch transactions across any supported chain. You can specify chains as a comma separated list of chain tags (Lens Chain tags are `lens-sepolia-testnet` and `lens-mainnet`) ### API ```bash curl --request GET \ --url "https://api.covalenthq.com/v1/allchains/transactions/?chains=lens-mainnet,lens-sepolia-testnet&addresses=INSERT_ADDRESS_HERE&key=INSERT_API_KEY_HERE" \ --header 'accept: application/json' ``` ### JavaScript SDK Install SDK with your package manager of choice: ```bash npm install @covalenthq/client-sdk" ``` ```js import { GoldRushClient } from "@covalenthq/client-sdk"; const client = new GoldRushClient("INSERT_API_KEY_HERE"); const fetchTransactions = async (walletAddress) => { try { const txResponse = await client.AllChainsService.getTransactions({ chains: "lens-mainnet,lens-sepolia-testnet", addresses: walletAddress, }); if (txResponse.error) { throw txResponse; } console.log(txResponse.data); } catch (error) { console.error(error); } }; fetchTransactions("INSERT_ADDRESS_HERE"); ``` ================ File: src/pages/chain/tools/data-indexers/dune.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Dune [Dune](https://dune.com/) is a data indexing solution allowing users to create queries and dashboards for indexed blockchain data which will be supported for Lens production release (Dune is not available for test networks). ## Integrations - [Quickstart Querying Guide](https://docs.dune.com/quickstart) - [Client SDKs](https://docs.dune.com/api-reference/overview/sdks) - Python, TypeScript ================ File: src/pages/chain/tools/data-indexers/rindexer.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # rindexer [rindexer](https://rindexer.xyz/) is an open-source, high-performance EVM indexing tool built in Rust. It allows you to index blockchain events using a simple YAML configuration file, eliminating the need for additional coding. For more advanced requirements, rindexer provides a robust framework to build custom indexing solutions. The Lens API utilizes rindexer for its indexing needs. ## Installation Install rindexer by running the following command in your terminal: ```bash curl -L https://rindexer.xyz/install.sh | bash ``` After installation, verify it by checking the version: ```bash rindexer --version ``` ## Setting Up a New No-Code Project ### Create a New Project Navigate to your desired directory and run: ```bash rindexer new no-code ``` This command generates a `rindexer.yaml` file, which serves as the core configuration for your project. ### Configure the rindexer.yaml File Edit the `rindexer.yaml` file in the generated project to define your contract indexing settings. Below is an example configuration for indexing an event from a [demo contract](https://explorer.testnet.lens.xyz/address/0xb7462EaCd5487514b6b789CF1Fca3081020F4e21) on the Lens Testnet: ```yaml filename="rindexer.yaml" name: PingIndexer description: Indexer for the Ping contract on Lens Testnet repository: https://github.com/yourusername/ping-indexer project_type: no-code networks: - name: lensTestnet chain_id: 37111 rpc: https://rpc.testnet.lens.xyz storage: postgres: enabled: true contracts: - name: Ping details: - network: lensTestnet address: 0xb7462EaCd5487514b6b789CF1Fca3081020F4e21 abi: ./abis/ping.abi.json include_events: - Pong ``` See the [YAML Config](https://rindexer.xyz/docs/start-building/yaml-config) docs for a complete reference on available fields. ### Add Contract ABI Place the contract ABI file (`ping.abi.json` in the example) in the directory designated in the `rindexer.yaml` file. ### Running the Indexer Once your configuration is set up, run the indexer using: ```bash rindexer start ``` This will begin indexing based on the configuration defined in `rindexer.yaml`. ### Viewing Indexed Data By default, a GraphQL endpoint will be exposed on `http://localhost:3001/graphql` with a playground UI available at `http://localhost:3001/playground`. Data can also be queried by connecting directly to the PostgreSQL database: ```bash docker exec -it rindexer-db psql -U postgres -d rindexer ``` ### Customizing the Indexer If your indexing needs extend beyond the no-code mode, you can scaffold a Rust-based indexer using: ```bash rindexer new rust ``` ### Deploying the Indexer A rindexer instance can be deployed to expose a database or endpoint by following docs guides for [Railway](https://rindexer.xyz/docs/deploying/railway), [AWS](https://rindexer.xyz/docs/deploying/aws), or [GCP](https://rindexer.xyz/docs/deploying/gcp). ================ File: src/pages/chain/tools/data-indexers/the-graph.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # The Graph [The Graph](https://thegraph.com/) is a decentralized protocol that enables efficient indexing and querying of blockchain data via GraphQL APIs. Lens Testnet is currently supported in The Graph Studio with the network tag: `lens-testnet`. This guide provides a walkthrough for developing and deploying a subgraph for a demo contract on the Lens Testnet. ## Create a New Subgraph in Subgraph Studio. Navigate to [Subgraph Studio](https://thegraph.com/studio/) and connect your crypto wallet which will manage and deploy subgraph instance. This will generate a Subgraph Slug, which will be used in subsequent steps. ## Initialize Subgraph Indexing Open your terminal and navigate to your desired directory. Install The Graph CLI with your package manager of choice if not already installed: ```bash npm install -g @graphprotocol/graph-cli@latest ``` Initialize the subgraph using the Graph CLI: ```bash graph init --studio ``` Replace the `` with the slug obtained from Subgraph Studio. Follow the prompts to configure your subgraph: - `ethereum` as the protocol - `lens-testnet` as the network - Contract address(es) to index - Contract ABI path, start block, and name (if not automatically fetched from block explorer) - It is recommended to select 'Index contract events as entities' as 'true', which will simplify the process of creating entity mappings ## Update Subgraph Configuration After initializing the subgraph, files will be generated that define the mapping from contract events to GraphQL entities, which can be customized based on your needs: - The schema (`schema.graphql`) contains generated entities which will be exposed from the GraphQL endpoint. - The subgraph manifest (`subgraph.yaml`) contains configuration for the data source(s) that will be indexed by the subgraph. - Mapping files are generated in the `src` directory, which define how schema entities are created, updated, or deleted as contract events are indexed. See [The Graph Development docs](https://thegraph.com/docs/en/subgraphs/developing/creating/starting-your-subgraph/#start-building) for more information on features and customization for these files. ## Deploy the Subgraph Follow the steps on the Subgraph Studio dashboard to authenticate and deploy the subgraph: ```bash graph auth --studio graph codegen && graph build graph deploy ``` ## Querying the Subgraph Once deployed, navigate to Subgraph Studio to find the deployed GraphQL endpoint and test queries against the indexed data. Use GraphQL queries to fetch indexed events and entity data from the contract within your integrating application. See [The Graph Querying Docs](https://thegraph.com/docs/en/subgraphs/querying/introduction/) for more information on available GraphQL APIs. ================ File: src/pages/chain/tools/data-indexers/thirdweb.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} ## Insights thirdweb Insight is a fast, reliable and fully customizable way for developers to index, transform & query onchain data across 30+ chains. Insight includes out-of-the-box APIs for transactions, events, tokens. Developers can also define custom API schemas, or blueprints, without the need for ABIs, decoding, RPC, or web3 knowledge to fetch blockchain data. Start building with [thirdweb Insight](https://portal.thirdweb.com/insight?utm_source=lens&utm_medium=docs). thirdweb Insight can be used to: - Fetching all assets (ERC20, ERC721, ERC115) for a given wallet address. - Fetching all sales of skins on your in-game marketplace - Fetching monthly protocol fees in the last 12 months - Fetching the total cost of all accounts created using ERC-4337 - Fetching metadata for a given token (ERC20, ERC721, ERC115) - Fetching daily active wallets for your application or game - And so much more ## Get Started Sign up for a [free thirdweb account](https://thirdweb.com/team?utm_source=lens&utm_medium=docs), visit the [thirdweb Insight documentation](https://portal.thirdweb.com/insight/get-started?utm_source=lens&utm_medium=docs) ================ File: src/pages/chain/tools/defi/uniswap.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; # Uniswap Uniswap V3 is deployed on the Lens Testnet, allowing developers to create and interact with liquidity pools. Deployed contract addresses can be found [here](../resources/contracts.mdx). ## Creating a Liquidity Pool A ready-to-use script for creating Uniswap V3 pools is available [here](https://github.com/defispartan/echofi-deploy/blob/lens-netwok/src/scripts/createUniswapPools.ts). ### Steps to Run the Script 1. Clone the repository ```bash git clone https://github.com/defispartan/echofi-deploy.git cd echofi-deploy ``` 2. Install dependencies ```bash yarn install ``` 3. Configure the script Open `src/scripts/createUniswapPools.ts` Set the required pool and deployer details at the top of the script. 4. Deploy the pool ```bash yarn hardhat run src/scripts/createUniswapPools.ts --no-compile ``` ## Interacting with pools Once a pool is deployed, you can interact with it using a library like ethers.js or viem. Common interactions include: ### Querying Pool Information ```js import { ethers } from "ethers"; const provider = new ethers.JsonRpcProvider(""); const poolAddress = ""; const poolContract = new ethers.Contract( poolAddress, UNISWAP_V3_POOL_ABI, provider ); async function getPoolState() { const slot0 = await poolContract.slot0(); console.log("Current price sqrtX96:", slot0.sqrtPriceX96.toString()); } getPoolState(); ``` ### Swapping Tokens ```js const swapRouterAddress = ""; const swapRouterContract = new ethers.Contract( swapRouterAddress, SWAP_ROUTER_ABI, signer ); async function swapTokens() { const tx = await swapRouterContract.exactInputSingle({ tokenIn: "", tokenOut: "", fee: 3000, recipient: signer.address, deadline: Math.floor(Date.now() / 1000) + 60 * 10, amountIn: ethers.parseUnits("1", 18), amountOutMinimum: 0, sqrtPriceLimitX96: 0, }); await tx.wait(); console.log("Swap executed successfully"); } swapTokens(); ``` For additional resources on integrating Uniswap V3 contracts, refer to the [Uniswap V3 docs](https://docs.uniswap.org/contracts/v3/overview). ================ File: src/pages/chain/tools/faucets/alchemy.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; export default ({ children }) => {children}; {/* Start of the page content */} # Alchemy Faucet To use the [Alchemy Lens Testnet Faucet](https://www.alchemy.com/faucets/lens-sepolia), log in to your Alchemy account, enter your wallet address, and request tokens. You can request 0.5 tokens every 72 hours with a free Alchemy account. To prevent bots and abuse, this faucet requires a minimum Ethereum mainnet balance of 0.001 ETH on the wallet address being used. Alchemy Faucet ================ File: src/pages/chain/tools/faucets/lenscan.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; export default ({ children }) => {children}; {/* Start of the page content */} # Lenscan Community Faucet The [Lenscan Community Faucet](https://testnet.lenscan.io/faucet) can be used to claim GRASS tokens every 24 hours based on the difficulty level of the interactive maze you complete for authentication purposes. Lenscan Faucet ================ File: src/pages/chain/tools/faucets/thirdweb.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; export default ({ children }) => {children}; {/* Start of the page content */} # thirdweb Faucet To use the [thirdweb Lens Testnet Faucet](https://thirdweb.com/lens-network-sepolia-testnet), login to your thirdweb account, complete re-captcha, and you can request 0.01 tokens every 24 hours. Need extra help? See [this guide](https://blog.thirdweb.com/faucet-guides/how-to-get-free-grass-token-grass-from-the-lens-network-faucet/). Thirdweb Faucet ================ File: src/pages/chain/tools/on-ramp/halliday.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; # Halliday This guide will show you how to integrate Halliday Payments into your Lens Chain application to acquire GHO on Lens Chain. Halliday provides a simple way to acquire tokens via onramp, bridge, and swap functionality. ## API Key Obtain API key from [Halliday Dashboard](https://dashboard.halliday.xyz/keys). ## Install SDK Install the Halliday Payments SDK in the root folder of a web project. ```bash npm install @halliday-sdk/commerce ``` ## Widget Integrate Halliday Payments into an existing application with a few lines of code. This example is in a React.js project. ```tsx import {openHalliday} from "@halliday-sdk/commerce"; ; ``` ## Configuration Options The `openHalliday` function accepts several configuration options: - `apiKey`: Halliday API key from [dashboard](https://dashboard.halliday.xyz/keys) - `destinationChainId`: The chain ID where tokens will be received (232 for Lens Chain) - `destinationTokenAddress`: The contract address of the destination token - `services`: Array of services to enable (["ONRAMP"] for just onramp functionality) - `windowType`: "EMBED" for iframe integration or "POPUP" for popup window - `targetElementId`: The HTML element ID where the iframe will be rendered - `customStyles`: Optional styling configuration to match your application's theme For more advanced features and customization options, refer to the [Halliday Documentation](https://docs.halliday.xyz). ================ File: src/pages/chain/tools/on-ramp/thirdweb.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; # thirdweb Pay [thirdweb Pay](https://thirdweb.com/) is a fiat on-ramp solution that allows users to buy crypto and execute transactions using credit cards, debit cards, or tokens via cross-chain routing. It enables seamless onboarding, in-app purchases, and Web3 payments without requiring users to leave the application. ## Integration Options - **[Embed Pay Modal](https://portal.thirdweb.com/connect/pay/get-started#option-2-embed-pay)** – Quickly integrate a pre-built payment modal for users to purchase crypto. - **[Send a Transaction](https://portal.thirdweb.com/connect/pay/get-started#option-3-send-a-transaction-with-pay)** – Allow users to purchase crypto and directly execute an on-chain transaction. - **[Build a Custom Fiat Experience](https://portal.thirdweb.com/connect/pay/guides/build-a-custom-experience)** – Fully customize the fiat-to-crypto experience, from UI to transaction flow. ## Testing For testing thirdweb Pay in a development environment, follow the guide here: [Testing thirdweb Pay](https://portal.thirdweb.com/connect/pay/testing-pay) ================ File: src/pages/chain/tools/oracles/chainlink.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; # Chainlink More details coming soon on [Chainlink](https://chain.link/) oracle support for Lens Chain. ================ File: src/pages/chain/tools/oracles/redstone.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; # Redstone More details coming soon on [Redstone](https://redstone.finance/) oracle support for Lens Chain. ================ File: src/pages/chain/tools/rpc/alchemy.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; export default ({ children }) => {children}; {/* Start of the page content */} # Alchemy RPC [Alchemy](https://dashboard.alchemy.com/) provides access to Lens RPC endpoints via its dashboard, making development easier with reliable infrastructure. Alchemy RPC setup Alchemy offers reliable and robust infrastructure tailored for developers, making it perfect for production environments. To access Alchemy RPC endpoints: 1. Login to your Alchemy account. 2. From the Alchemy dashboard, select **Create New App**. 3. Select **Lens** within the **Choose Chains** tab. 4. Select **Node API** within the **Activate Services** tab. 5. Once the project is created, an API key is assigned and can be used with the Alchemy RPC endpoint: `https://lens-sepolia.g.alchemy.com/v2/insert_api_key_here`. For more details on JSON-RPC methods, see the [Alchemy Ethereum API documentation](https://docs.alchemy.com/reference/ethereum-api-quickstart). ================ File: src/pages/chain/tools/rpc/chainstack.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; export default ({ children }) => {children}; {/* Start of the page content */} # Chainstack [Chainstack](https://chainstack.com/build-better-with-lens/) provides secure, production-ready Lens node access with uptime SLA and flat-rate pricing. Chainstack ## Get Started Go to [chainstack.com/build-better-with-lens](https://chainstack.com/build-better-with-lens/) and fill out request form for dedicated RPC access. ## Features - Dedicated Lens Chain mainnet & testnet nodes - Fully managed infrastructure - Unlimited requests, flat monthly rate - SOC 2 certified with 99.99% uptime SLA For more details on dedicated nodes, see the [Chainstack documentation](https://docs.chainstack.com/docs/dedicated-node). ================ File: src/pages/chain/tools/rpc/drpc.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; export default ({ children }) => {children}; {/* Start of the page content */} # dRPC [dRPC](https://drpc.org/) provides access to Lens RPC endpoints via a decentralized, high-performance network of independent providers. With low latency and a pay-as-you-go pricing model, it’s optimized for reliability and scale across 90+ chains. dRPC To access dRPC endpoints: 1. Login to dRPC Dashboard. 2. From the dRPC dashboard, select **Add Key**. 3. Enter application info and **Create**. 4. Once the project is created, an API key is assigned and can be used with the dRPC endpoint: `https://lb.drpc.org/ogrpc?network=lens&dkey=insert_api_key_here`. For more details on JSON-RPC methods, see the [dRPC documentation](https://drpc.org/docs). ================ File: src/pages/chain/tools/rpc/public.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; # Public RPC The Lens Public RPC endpoint offers open-access interaction with Lens Testnet for developers. ```bash https://rpc.testnet.lens.xyz ``` It is suitable for testing environments, but not recommended for production due to rate limits. ================ File: src/pages/chain/tools/rpc/quicknode.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; export default ({ children }) => {children}; {/* Start of the page content */} # QuickNode RPC [QuickNode](https://dashboard.quicknode.com/) provides fast, reliable access to Lens RPC endpoints, empowering you to read, write, and build onchain with industry-leading infrastructure. QuickNode RPC setup QuickNode delivers robust, production-ready infrastructure trusted by top teams for its speed, reliability, and scalability making it the perfect choice for developers and enterprises alike. To access QuickNode RPC endpoints: 1. Go to [QuickNode Dashboard](https://dashboard.quicknode.com) and create or log in to your account. 2. Navigate to the **Endpoints** tab. 3. Click **Create Endpoint**. 4. Select **Lens** as the network. 5. Once your endpoint is created, you’ll receive a unique RPC URL and credentials. Use this URL in your application to connect to the Lens network via QuickNode. There is also public RPC endpoint for Lens Chain available at https://light-icy-dinghy.lens-mainnet.quiknode.pro/, with rate limits of 20/second, 2000/minute, 100000/day. For more details on available JSON-RPC methods, see the [QuickNode documentation](https://www.quicknode.com/docs). ================ File: src/pages/chain/tools/rpc/tenderly.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; # Tenderly RPC [Tenderly](https://tenderly.co/) enables developers to interact with Lens RPC endpoints and offers additional RPC methods for simulations and debugging. Tenderly RPC setup To access Tenderly RPC endpoints: 1. Login to your Tenderly account. 2. From the Tenderly Dashboard, open the **Node RPCs** section in the side panel. 3. Click **Create Node RPC**. 4. Select **Lens Testnet**. 5. Copy RPC endpoint from dashboard, it will be in format: `https://lens-sepolia.gateway.tenderly.co/insert_api_key_here` Tenderly Node RPCs include additional methods for simulation and debugging: - `tenderly_simulateTransaction`: Simulate the outcome of a transaction before signing. - `tenderly_simulateBundle`: Simulate multiple transactions in a single request. - `tenderly_traceTransaction`: Get a decoded trace of an existing transaction. - `tenderly_estimateGas`: Get information on gas usage for a given transaction. - `tenderly_estimateGasBundle`: Get information on gas usage for a bundle of transactions. - `tenderly_decodeInput`: Heuristically decode external function calls. - `tenderly_decodeError`: Heuristically decode custom errors. - `tenderly_decodeEvent`: Heuristically decode emitted events. - `tenderly_functionSignatures`: Retrieve function interfaces based on 4-byte function selectors. - `tenderly_errorSignatures`: Retrieve error interfaces based on 4-byte selectors. - `tenderly_eventSignature`: Retrieve event interfaces based on 32-byte event signatures. For more details on JSON-RPC methods, see the [Tenderly Node documentation](https://docs.tenderly.co/node) ================ File: src/pages/chain/tools/rpc/thirdweb.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; # thirdweb RPC [thirdweb](https://thirdweb.com) enables developers to interact with Lens RPC endpoints and offers tooling for fullstack Web3 development. Thirdweb RPC setup To access thirdweb RPC endpoints: 1. Login to your thirdweb account 2. From the thirdweb Dashboard, select **Add New** -> **Project** 3. Once the project is created, an API key is assigned and can be used with the thirdweb RPC endpoint: `https://37111.rpc.thirdweb.com/insert_api_key_here`. ================ File: src/pages/chain/tools/smart-contract-development/smart-contract-development.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; # Smart Contract Development Build and deploy smart contracts on Lens Chain with industry-standard development tools. --- ## Tenderly The best in-class smart contract development toolkit supports Lens Chain. ## thirdweb All thirdweb features work out the box with Lens Chain. ================ File: src/pages/chain/tools/smart-contract-development/tenderly.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; # Tenderly [Tenderly](tenderly.co) is a comprehensive Web3 development platform that offers a suite of tools and infrastructure to enhance the smart contract development experience on the Lens Testnet. By integrating with Tenderly, developers can streamline their workflow, from development and testing to deployment and monitoring. Key features include: - **Simulation API**: With Tenderly's Simulation API, developers can simulate single or bundled transactions on the latest block of the blockchain. This feature allows for previewing transaction outcomes, understanding asset changes, and accurate gas estimation before actual execution. - **Monitoring and Alerts**: Tenderly enables real-time monitoring of on-chain events, contracts, and wallets. Developers can set up alerts for various triggers and send notifications to multiple destinations, ensuring timely responses to important events on the Lens Testnet. - **Virtual TestNets**: Tenderly's Virtual TestNets provide fully controlled mainnet replicas, enabling collaborative dApp development, testing, and staging. Developers can simulate the Lens Testnet environment, allowing for thorough testing before deployment. ================ File: src/pages/chain/tools/smart-contract-development/thirdweb.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; # thirdweb [thirdweb Contracts](https://portal.thirdweb.com/contracts) is an end-to-end development suite which provides tools to develop, deploy, and manage smart contracts on Lens Testnet. Key features include: - [Explore (Contract Library)](https://portal.thirdweb.com/contracts/explore/overview): Audited and expansive library of pre-built contracts deployable with one-click - [Modular Contracts](https://portal.thirdweb.com/contracts/modular-contracts/overview): Framework to build highly customizable and secure smart contracts - [Deployment Tool](https://portal.thirdweb.com/contracts/deploy/overview): Client-side deploy tool to securely deploy any contract to any EVM compatible chain thirdweb is committed to ensuring that the next wave of consumer applications can focus on creating excellent user experiences with blockchain as a powerful backend tool, without worrying about infrastructure or associated costs. ## Comprehensive cohort for founders - Completely free with no hidden costs - Access to thirdweb engineers and experts - Personalized solutions for your use case - Perks package and grant opportunities Checkout the thirdweb [startup program](https://thirdweb.com/community/startup-program?utm_source=lens&utm_medium=docs) to learn more. ================ File: src/pages/chain/best-practices.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Best Practices Coming soon. ================ File: src/pages/chain/overview.mdx ================ export const meta = { showBreadcrumbs: false, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Lens Chain Scalable SocialFi on Ethereum. --- Lens is a high-performance blockchain stack built for SocialFi, combining modular Social Primitives, fast settlement, and decentralized storage.
The Lens stack is composed of Lens Chain, Social Protocol, and Storage Nodes. As a layer 2 leveraging ZKsync, Avail, and Ethereum's security, Lens delivers the fastest, most cost-effective, and scalable blockchain for developers. In addition to exceptional performance, Lens Chain offers significant UX improvements such as account abstraction, USD gas fees, and facilitates easy onboarding via email and phone verification.
Modular, flexible, customizable and onchain Social Primitives are at the core of Lens. These primitives provide developers with flexible, onchain building blocks for creating new experiences without reinventing the wheel. Through customizable Rules, developers can define how Groups, Feeds, Graphs, and Usernames interact, unlocking novel monetization models and access control mechanisms.
Lens also features custom Storage Nodes offering decentralized storage with the speed and cost efficiency of centralized solutions, while delivering enterprise-grade performance and scalability. Storage Nodes are designed to address the limitations of traditional decentralized storage while maintaining user ownership, flexibility, and security.
## Benefits ================ File: src/pages/chain/running-a-node.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Running a Lens Node Learn how to run a Lens node. --- ## Prerequisites - [Docker](https://docs.docker.com/get-docker/) - [Docker Compose](https://docs.docker.com/compose/install/) ## Setup Instructions ### Clone the Lens Node Repository ```bash git clone https://github.com/lens-protocol/lens-chain-node && cd lens-chain-node ``` ### Start the Node - For a Testnet instance: ```bash docker-compose --file testnet-external-node.yml up -d ``` - For a Mainnet instance: ```bash docker-compose --file mainnet-external-node.yml up -d ``` Note: The mainnet configuration implements snapshot recovery, to disable this functionality you can set the `EN_SNAPSHOTS_RECOVERY_ENABLED` environment variable to `false` in the `mainnet-external-node.yml` file. Further information on snapshot recovery can be found [here](https://matter-labs.github.io/zksync-era/core/latest/guides/external-node/07_snapshots_recovery.html) ### Verify the Node Is Running ```bash docker ps -f "name=lens-node-external-node" ``` The status of the container should be `Up`. ### Make Your First Request To make a request to the API, you can use the following command: ```bash curl --request POST \ --url http://localhost:3060/ \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "zks_getMainContract", "params": [] }' ``` Example response: ```json { "jsonrpc": "2.0", "result": "0x9cff734c0529e89e2294b592d9f4d310754ec8ca", "id": 1 } ``` --- ### Checking the Node State You can check the logs of the Docker container to see if there are any errors: ```bash docker logs -f --tail 100 lens-node-external-node-1 ``` You can also check the healthcheck endpoint to see if the node is healthy: ```bash curl http://localhost:3081/health ``` Example response: ```json { "status": "ready", "components": { "tree": { "status": "ready", "details": { "leaf_count": 5135, "min_l1_batch_number": 0, "mode": "lightweight", "next_l1_batch_number": 3454, "root_hash": "0xef63592d85ef5cd1986af2af0ba4040fc13392c1c5639b2cd7347fd6793adea7", "stage": "main_loop" } }, "prometheus_exporter": { "status": "ready" }, "consistency_checker": { "status": "ready", "details": { "first_checked_batch": 3431 } }, "commitment_generator": { "status": "ready", "details": { "l1_batch_number": 3453 } }, "batch_status_updater": { "status": "ready", "details": { "last_committed_l1_batch": 3430, "last_executed_l1_batch": 3430, "last_proven_l1_batch": 3430 } }, "ws_api": { "status": "ready" }, "http_api": { "status": "ready" }, "main_node_http_rpc": { "status": "ready" }, "reorg_detector": { "status": "ready", "details": { "last_correct_l1_batch": 3453, "last_correct_l2_block": 14072 } } } } ``` ### Resetting the Node State ```bash docker-compose --file -external-node.yml down --volumes ``` ### API Access - The HTTP JSON-RPC API will be exposed on port `3060` (localhost:3060). - The WebSocket API will be exposed on port `3061` (localhost:3061). ## System Requirements The following are minimal requirements: - **CPU:** A relatively modern CPU is recommended. - **RAM:** 32 GB - **Storage:** - **Testnet Nodes:** 60 GB - **Mainnet Nodes:** 700+ GB, with the state growing. - **Network:** 100 Mbps connection (1 Gbps+ recommended) ## Advanced Setup For additional configurations like monitoring, backups, recovery from DB dump or snapshot, and custom PostgreSQL settings, please refer to the [ansible-en-role repository](https://github.com/matter-labs/ansible-en-role). ## Running an Archive Node Running an archive node requires additional resources and configurations. Please refer to the [running and archive node page](https://lens.xyz/docs/chain/running-an-archive-node) for detailed instructions. ================ File: src/pages/chain/running-an-archive-node.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Running a Lens Archive Node Learn how to run a Lens Archive node. Is an Archive Node the right choice for you? If you need a node with the entire rollup history, using a Postgres dump is the only option, and pruning should be disabled. --- ## System Requirements The following are minimal requirements: - **CPU:** A relatively modern CPU is recommended, with approximately 16 cores. - **RAM:** 64GB for running the node (Temporarily 1TB during setup if you require the `zks_getProof` method). - **Storage:** - **Mainnet Nodes:** 2 TB, with the state growing. - **Network:** 100 Mbps connection (1 Gbps+ recommended) ## Setup Instructions If you require the `zks_getProof` [method](https://docs.zksync.io/zksync-protocol/api/zks-rpc#zks_getproof), ensure you have the necessary system resources before proceeding. External node rebuilds the tree when it launches for the first time. Batch 0 on the Lens chain is extremely large. For bootstrap roughly 1TB RAM is required to fit that all in. That much memory is required only **once**, to generate Rocks DB state. After that you can cut it back down to a normal spec. If you do not require the `zks_getProof` [method](https://docs.zksync.io/zksync-protocol/api/zks-rpc#zks_getproof), you can skip the tree rebuild and therefore avoid the need for a large amount of RAM during the initial setup. See **Restoring without the Merkle tree** below for more details. ### Clone the Lens Node Repository ```bash git clone https://github.com/lens-protocol/lens-chain-node && cd lens-chain-node ``` ### Disable the s3 snapshot restore and disable pruning ```bash EN_SNAPSHOTS_RECOVERY_ENABLED: "false" EN_PRUNING_ENABLED: "false" ``` ### Download the latest database dump ```bash curl -O https://lens-chain-db-dump.s3.us-east-1.amazonaws.com/endb.dump ``` ### Add the database dump to the docker volume mounts ```bash volumes: - mainnet-postgres:/db - /path/to/endb.dump:/host/endb.dump:ro ``` ### Start Postgres ```bash docker compose --file mainnet-external-node.yml up -d postgres ``` ### Restore the database dump to postgres ```bash docker exec -u postgres lens-chain-node-postgres-1 pg_restore \ -v -C --no-owner --no-privileges -d postgres "/host/endb.dump" ``` The restore process can take a few hours, resource dependent. ### Start the External Node ```bash docker compose --file mainnet-external-node.yml up -d external-node ``` The process to rebuild the merkle tree takes approximately 3-4 hours and uses a significant amount of RAM ~1TB Once the merkle tree is rebuilt, and the node is synced, you can scale back the RAM attached to the node to the standard configuration. ### OPTIONAL: Starting the External Node without the Merkle Tree (AKA Treeless Mode) This option is useful for environments with limited resources or when the `zks_getProof` [method](https://docs.zksync.io/zksync-protocol/api/zks-rpc#zks_getproof) is not required. Full Documentation on Treeless Mode can be found [here](https://github.com/matter-labs/zksync-era/blob/main/docs/src/guides/external-node/09_treeless_mode.md). First we disable the merkle tree rebuild: ```bash command: ["--components=core,api,da_fetcher,tree_fetcher"] ``` Then we start the node: ```bash docker compose --file mainnet-external-node.yml up -d external-node ``` ### Additional Documentation Additional documentation can be found in the [ZkSync Era Repository](https://github.com/matter-labs/zksync-era/blob/main/docs/src/guides/external-node/00_quick_start.md) ================ File: src/pages/chain/using-lens-chain.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Using Lens Chain Lens Chain can be added to any EVM-compatible wallet. --- ## Lens Chain Mainnet Lens Chain Mainnet operates as a Layer 2 network secured by the Ethereum Mainnet. Its native gas token is GHO. You can bridge funds into GHO using one of the [bridges](./tools/bridging/zksync). ## Lens Chain Testnet Lens Chain Testnet operates as a Layer 2 network secured by the Ethereum Sepolia testnet. Its native gas token is GRASS. You can obtain testnet GRASS tokens from the [faucets](./tools/faucets/alchemy). A warning such as _This token symbol doesn't match the network name or chain ID entered_ is to be expected at this stage. ## Manually Add to MetaMask Alternatively, you can add the Lens Chain to MetaMask by following these steps: 1. Open the MetaMask browser extension. 2. Select _Settings_ from the dropdown menu. 3. Navigate to _Networks_ and click the _Add network_ button. 4. In the _Add a network manually_ form, enter the following information: | Field | Value | | ----------------------------------------------- | --------------------------- | | Network Name | `Lens Chain Mainnet` | | New RPC URL | `https://rpc.lens.xyz` | | Chain ID | `232` | | Currency Symbol | `GHO` | | [Block Explorer URL](https://explorer.lens.xyz) | `https://explorer.lens.xyz` | | Field | Value | | ------------------------------------------------------- | ----------------------------------- | | Network Name | `Lens Chain Testnet` | | New RPC URL | `https://rpc.testnet.lens.xyz` | | Chain ID | `37111` | | Currency Symbol | `GRASS` | | [Block Explorer URL](https://explorer.testnet.lens.xyz) | `https://explorer.testnet.lens.xyz` | After adding this information, you should be able to connect to Lens Chain by selecting it from the network selection dropdown menu in MetaMask. ================ File: src/pages/protocol/accounts/actions.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Account Actions This guide explains how to use Account Actions and how to implement custom ones. --- Account Actions are contracts that extend the Lens Protocol functionality by allowing Accounts to execute actions on other Accounts. ## Tipping Account Action Lens includes a built-in `TippingAccountAction`, which is enabled by default and allows an Account to tip another Account using native GHO (GRASS on the Lens Testnet) or ERC20 token. The `TippingAccountAction` supports a referral scheme that lets you reward accounts or apps that helped surface the tipped Account. A maximum of **20%** of the tipped amount can be allocated to referrals, but only if they're explicitly included when the tip is sent. A **1.5%** Lens treasury fee is deducted from the total amount paid by the tipper before calculating referral and recipient shares. ### Payment Source Tips can use funds from either the **Signer** or the **Lens Account**. - **Signer** refers to the Account Owner or Account Manager, depending on the [authentication role](../authentication). - **Lens Account** is the account you are currently logged in with. ### Referral Fee Breakdown Let's say a user tips an Account with **100 GHO** using the `TippingAccountAction`. Here's how the amount is split: - **1.5 GHO** (1.5%) is deducted for the **Lens treasury fee** - **98.5 GHO** remains The user includes two referral recipients, fully allocating the maximum **20% referral fee** between them: - `0xc0ffee` with a **30% share** of the referral portion - `0xbeef` with a **70% share** of the referral portion From the remaining **98.5 GHO**: - **19.7 GHO** (20%) is distributed as referrals: - **5.91 GHO** to `0xc0ffee` - **13.79 GHO** to `0xbeef` - **78.8 GHO** is sent to the **tipped Account** If the referral split adds up to less than 100% (e.g. a single referral with 50%), only the corresponding portion of the 20% referral fee will be used. The unused remainder goes to the tipped Account. ## Custom Account Actions Custom Account Actions must be configured on a per-account basis before they can be executed. ### Configuring Account Actions To configure a custom Account Action, follow these steps. You MUST be authenticated as Account Owner or Account Manager of the Account you intend to configure an Account Action for. #### Configure the Action First, use the `configureAccountAction` action to configure a new Account Action. ```ts filename="Custom Account Action" import { blockchainData, evmAddress } from "@lens-protocol/client"; import { configureAccountAction } from "@lens-protocol/client/actions"; const result = await configureAccountAction(sessionClient, { action: { unknown: { address: evmAddress("0x1234…"), params: [ { raw: { // 32 bytes key (e.g., keccak(name)) key: blockchainData("0xac5f04…"), // an ABI encoded data data: blockchainData("0x00"), }, }, ], }, }, }); ``` First, use the `configureAccountAction` mutation to configure a new Account Action. ```graphql filename="Mutation" mutation { configureAccountAction( request: { action: { unknown: { address: "0x1234…" params: [ { raw: { key: "0x4f…" # 32-byte key (e.g., keccak(name)) data: "0x00" # ABI-encoded data } } ] } } } ) { ... on ConfigureAccountActionResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on AccountOperationValidationFailed { reason } ... on TransactionWillFail { reason } } } ``` ```json filename="ConfigureAccountActionResponse" { "data": { "configureAccountAction": { "hash": "0x…" } } } ``` #### Handle Result Then, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,9" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await configureAccountAction(sessionClient, { action: { // … }, }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,9" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await configureAccountAction(sessionClient, { action: { // … }, }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Then, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Coming soon ### Disabling Account Actions To disable a custom Account Action, follow these steps. You MUST be authenticated as the Account Owner or Account Manager of the Account for which you intend to configure an Account Action. #### Disable the Action First, use the `disableAccountAction` action to disable an Account Action. You can re-enable it later using the `enableAccountAction` action. ```ts filename="Disable Account Action" import { blockchainData, evmAddress } from "@lens-protocol/client"; import { disableAccountAction } from "@lens-protocol/client/actions"; const result = await disableAccountAction(sessionClient, { unknown: { address: evmAddress("0x1234…"), }, }); ``` ```ts filename="Enable Account Action" import { blockchainData, evmAddress } from "@lens-protocol/client"; import { enableAccountAction } from "@lens-protocol/client/actions"; const result = await enableAccountAction(sessionClient, { unknown: { address: evmAddress("0x1234…"), }, }); ``` You can provide any parameters required by the custom Account Action contract using the `params` field. First, use the `disableAccountAction` mutation to configure a new Account Action. You can re-enable it later using the `enableAccountAction` mutation. ```graphql filename="Disable Account Action" mutation { disableAccountAction(request: { unknown: { address: "0x1234…" } }) { ... on DisableAccountActionResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on AccountOperationValidationFailed { reason } ... on TransactionWillFail { reason } } } ``` ```graphql filename="Enable Account Action" mutation { enableAccountAction(request: { unknown: { address: "0x1234…" } }) { ... on EnableAccountActionResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on AccountOperationValidationFailed { reason } ... on TransactionWillFail { reason } } } ``` You can provide any parameters required by the custom Account Action contract using the `params` field. #### Handle Result Then, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,9" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await disableAccountAction(sessionClient, { unknown: { address: evmAddress("0x1234…"), }, }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,9" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await disableAccountAction(sessionClient, { unknown: { address: evmAddress("0x1234…"), }, }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Then, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Coming soon ## Executing Account Actions To execute an Account Action, follow these steps. You MUST be authenticated as the Account Owner or Account Manager to execute an Account Action. ### Inspect Account Actions First, inspect the target `account.actions` field to determine what Account Actions are available on it. ```ts filename="Account Actions" for (const action of account.actions) { switch (action.__typename) { case "TippingAccountAction": // The Account has a Tipping Account Action, all accounts have this by default break; case "UnknownAccountAction": // The Account has a custom Account Action break; } } ``` See some examples below. ```json filename="TippingAccountAction" { "__typename": "TippingAccountAction", "address": "0x5678…" // the address to tip } ``` ```json filename="UnknownAccountAction" { "__typename": "UnknownAccountAction", "address": "0x1234…", "metadata": { "__typename": "ActionMetadata", "id": "123e4567-e89b-12d3-a456-426614174000", "name": "SampleAction", "description": "This is a sample action description.", "authors": ["author1@example.com", "author2@example.com"], "source": "https://github.com/example/repo", "configureParams": [ { "__typename": "KeyValuePair", "key": "0x3e…", "name": "limit", "type": "uint256" } ], "executeParams": [ { "__typename": "KeyValuePair", "key": "0x4f…", "name": "sender", "type": "address" }, { "__typename": "KeyValuePair", "key": "0x5f…", "name": "amount", "type": "uint256" } ], "setDisabledParams": [] } } ``` ### Execute Account Action Next, execute the desired Account Action. Use the `executeAccountAction` action to execute an Account Action. ```ts filename="Tipping Native" import { bigDecimal, evmAddress } from "@lens-protocol/client"; import { executeAccountAction } from "@lens-protocol/client/actions"; const result = await executeAccountAction(sessionClient, { account: evmAddress("0x1234…"), action: { tipping: { native: bigDecimal("100"), }, }, }); if (result.isErr()) { return console.error(result.error); } ``` ```ts filename="Tipping in ERC20" import { bigDecimal, evmAddress } from "@lens-protocol/client"; import { executeAccountAction } from "@lens-protocol/client/actions"; const result = await executeAccountAction(sessionClient, { account: evmAddress("0x1234…"), action: { tipping: { erc20: { currency: evmAddress("0x5678…"), value: bigDecimal("100"), }, }, }, }); if (result.isErr()) { return console.error(result.error); } ``` ```ts filename="Tipping with Signer" import { bigDecimal, evmAddress } from "@lens-protocol/client"; import { executeAccountAction } from "@lens-protocol/client/actions"; const result = await executeAccountAction(sessionClient, { account: evmAddress("0x1234…"), action: { tipping: { native: bigDecimal(5), paymentSource: PaymentSource.Signer, }, }, }); if (result.isErr()) { return console.error(result.error); } ``` ```ts filename="Tipping with Referrals" import { bigDecimal, evmAddress } from "@lens-protocol/client"; import { executeAccountAction } from "@lens-protocol/client/actions"; const result = await executeAccountAction(sessionClient, { account: evmAddress("0x1234…"), action: { tipping: { erc20: { currency: evmAddress("0x5678…"), value: bigDecimal("100"), }, referrals: [ { address: evmAddress("0xc0ffee…"), percent: 10, }, { address: evmAddress("0xbeef…"), percent: 90, }, ], }, }, }); if (result.isErr()) { return console.error(result.error); } ``` ```ts filename="Custom Account Action" import { evmAddress, blockchainData } from "@lens-protocol/client"; import { executeAccountAction } from "@lens-protocol/client/actions"; const result = await executeAccountAction(sessionClient, { account: evmAddress("0x1234…"), action: { unknown: { address: evmAddress("0x5678…"), params: [ { raw: { // 32 bytes key (e.g., keccak(name)) key: blockchainData("0xac5f04…"), // an ABI encoded data data: blockchainData("0x00"), }, }, ], }, }, }); if (result.isErr()) { return console.error(result.error); } ``` Use the `executeAccountAction` mutation to execute an Account Action. ```graphql filename="Tipping" mutation { executeAccountAction( request: { account: "0x1234…" action: { tipping: { erc20: { currency: "0x5678…", value: "100" } # or # native: "100", # OPTIONALS # paymentSource: SIGNER # default is ACCOUNT # referrals: [ # { address: "0xc0ffee…", percent: 10 }, # { address: "0xbeef…", percent: 90 }, # ] # Custom Account Action # unknown: { # address: "0x5678…", # params: [ # { raw: { key: "0x4f…", data: "0x00" } }, # ], # }, } } } ) { ... on ExecuteAccountActionResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } ... on SignerErc20ApprovalRequired { reason amount { ...Erc20Amount } } ... on InsufficientFunds { reason } } } ``` ```json filename="ExecuteAccountActionResponse" { "data": { "executeAccountAction": { "hash": "0x…" } } } ``` Coming soon ### Handle Result Then, the result will indicate what steps to take next. ```ts filename="viem" highlight="1,17,21,27" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const operation = await executeAccountAction(sessionClient, { account: evmAddress("0x1234…"), action: { // … }, }); if (operation.isErr()) { return console.error(operation.error); } switch (operation.value.__typename) { case "InsufficientFunds": // handle insufficient funds scenario return console.log("Insufficient funds for tipping"); case "SignerErc20ApprovalRequired": // handle ERC20 approval required scenario leveraging operation.value.amount: Erc20Amount return console.log("Signer ERC20 approval required for tipping"); } const result = await operation .asyncAndThen(handleOperationWith(wallet)) .andThen(sessionClient.waitForTransaction); ``` ```ts filename="ethers" highlight="1,17,20,26" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const operation = await executeAccountAction(sessionClient, { account: evmAddress("0x1234…"), action: { // … }, }); if (operation.isErr()) { return console.error(operation.error); } switch (operation.value.__typename) { case "InsufficientFunds": // handle insufficient funds scenario return console.log("Insufficient funds for tipping"); case "SignerErc20ApprovalRequired": // handle ERC20 approval required scenario leveraging operation.value.amount: Erc20Amount return console.log("Signer ERC20 approval required for tipping"); } const result = await operation .asyncAndThen(handleOperationWith(signer)) .andThen(sessionClient.waitForTransaction); ``` See the [ERC20 token approval](../best-practices/erc20-approval) guide for more information on how to handle ERC20 token approval. Finally, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Coming soon That's it—you've successfully executed an Account Action. --- ## Building an Account Action The Account Actions are defined by the `IAccountAction` interface, which basically requires three functions, one to configure the action, one to execute it, and another to disable it. ```solidity interface IAccountAction { function configure(address originalMsgSender, address account, KeyValue[] calldata params) external returns (bytes memory); function execute(address originalMsgSender, address account, KeyValue[] calldata params) external returns (bytes memory); function setDisabled(address originalMsgSender, address account, bool isDisabled, KeyValue[] calldata params) external returns (bytes memory); } ``` ### The ActionHub Before we dive into each of the functions from the `IAccountAction` interface, we need to talk about the `ActionHub`. The `ActionHub` is a special contract that acts as the entry point for every Action in the Lens Protocol. The purpose of that is to help discovery of Actions, having a single point where all main Action-related events are emitted. So, each function of the `IAccountAction` interface must be only callable by `ActionHub`. For this, you can inherit [`BaseAccountAction` contract](https://github.com/lens-protocol/lens-v3/blob/development/contracts/actions/account/base/BaseAccountAction.sol), which acts as template for your custom Post Action, applying the restrictions needed, so you do not need to worry about it. With this context, you can now understand why the first param of every `IAccountAction` function is `originalMsgSender`: this is the address that called the `ActionHub` originally, either to configure, execute, or disable an Action, given that `msg.sender` will always have the `ActionHub` address in the context of your Action contract. ### Configuration The configuration of the Action is done through the `configure` function, which purpose is to initialize any required state that the Action might require to work properly. The function receives three parameters: - `originalMsgSender`: the address of the original msg.sender that invoked the ActionHub (as explained above). - `account`: the address of the Account for which the Action is configured for. - `params`: array of key-value pairs whose values can be decoded into any extra custom configuration parameters that the Action could require to work. Return of the `configure` function is bytes - some Actions might want to return custom information to the caller. This `configure` function could be called by anyone (through the ActionHub, as explained above), it will depend on the implementation of the Action and its purpose who is allowed to invoke it (based on originalMsgSender). For example, there might be Actions that do not require initialization at all, so the `configure` function implementation will be empty, while other Actions might require the caller to match the Account for which the actions is being configured for (i.e. being configured by the Account itself). Keep in mind that if no prior configuration is required, the `configure` function must still be implemented and must not revert. Every time the `configure` function is called, the ActionHub will emit a `Lens_ActionHub_AccountAction_Configured` event matching the parameters of the call (or `Lens_ActionHub_AccountAction_Reconfigured` if the configuration was updated). ### Execution The execution of the Action is done through the `execute` function, which purpose is to perform the actual action logic that the Action implements. The function receives three parameters: - `originalMsgSender`: the address of the original msg.sender that invoked the ActionHub (as explained above). - `account`: the address of the Account for which the Action is being executed. - `params`: array of key-value pairs whose values can be decoded into any extra custom execution parameters that the Action could require to work. Return of the `execute` function is bytes - some Actions might want to return custom information to the caller. This `execute` function could be called by anyone (through the ActionHub, as explained above), it will depend on the implementation of the Action and its purpose who is allowed to invoke it (based on originalMsgSender). For example, there might be Actions that do not require any permissions at all, so the `execute` function implementation will be open, while other Actions might require the caller to match the some specific address. Every time the `execute` function is called, the ActionHub will emit a `Lens_ActionHub_AccountAction_Executed` event matching the parameters of the call. The ActionHub will not allow to invoke the `execute` function on an Account if the Action is disabled for it. ### Disabling The disabling of the Action is done through the `setDisabled` function, which purpose is to stop an Action to be executable for a given Account. The same function can be used to enable the Action back. The function receives four parameters: - `originalMsgSender`: the address of the original msg.sender that invoked the ActionHub (as explained above). - `account`: the address of the Account for which the Action is being disabled/enabled. - `isDisabled`: boolean indicating if the Action is being disabled or enabled. - `params`: array of key-value pairs whose values can be decoded into any extra custom disabling/enabling parameters that the Action could require to work. Return of the `setDisabled` function is bytes - some Actions might want to return custom information to the caller. This `setDisabled` function could be called by anyone (through the ActionHub, as explained above), it will depend on the implementation of the Action and its purpose who is allowed to invoke it (based on originalMsgSender). Every time the `setDisabled` function is called, the ActionHub will emit a `Lens_ActionHub_AccountAction_Disabled` event matching the parameters of the call. ### Example Let us create a Pay-To-Repost Account Action that allows anyone to pay a fixed fee for the account to repost a given post. #### Define State Variables First, we define the necessary state variables. We need a mapping `_prices` to store the price configured by each account for a specific token. This same array can also act as an indicator if the action was configured (i.e. if the price is 0, it means the action was not configured for that account). ```solidity /** * @title SimplePayToReAccountAction * @notice A simple Account action allowing users to pay for the Account to repost a given post. */ contract SimplePayToReAccountAction is BaseAccountAction, LensPaymentHandler { // account => token => price mapping(address => mapping(address => uint256)) private _prices; ``` #### Implement the Configure Function Next, implement the `_configure` function. This function allows the `account` owner (`originalMsgSender`) to set the price for reposting using their account. It extracts the payment `token` and `price` from the `params` array. It includes checks to ensure: 1. Only the account owner can configure it (`originalMsgSender == account`). 2. This action contract is set as an Account Manager for the target `account`, as it needs permission to execute the repost later. 3. A valid `token` address and a `price > 0` are provided. ```solidity // ... inside SimplePayToReAccountAction ... function _configure( address originalMsgSender, address account, KeyValue[] calldata params ) internal override returns (bytes memory) { require(originalMsgSender == account, "Only account can configure"); require(IAccount(account).canExecuteTransactions(address(this)), "SimplePayToReAccountAction is not set as AccountManager"); address token; uint256 price = 0; for (uint256 i = 0; i < params.length; i++) { if (params[i].key == keccak256("lens.param.token")) { token = abi.decode(params[i].value, (address)); } else if (params[i].key == keccak256("lens.param.price")) { price = abi.decode(params[i].value, (uint256)); } } require(token != address(0), "Token not found in params"); IERC20(token).balanceOf(address(this)); // Just checking if the token is valid require(price > 0, "Valid price not found in params"); _prices[account][token] = price; // Emitting an event Lens_ActionHub_AccountAction_Configured happens automatically via ActionHub return ""; } ``` #### Implement the Execute Function Implement the `_execute` function. This allows any user (`originalMsgSender`) to pay the configured price to make the `account` repost a specific post. The function extracts the target `feed`, `postId`, payment `token`, and the `expectedPrice` from the `params`. It verifies that the `expectedPrice` matches the price configured by the account owner. It then uses the inherited `_handlePayment` function (from `LensPaymentHandler`) to process the payment from the `originalMsgSender` to the `account`. Finally, it executes the repost on behalf of the `account` by calling the `repost` function on the target `IFeed` interface. This requires the action contract to have been approved as an Account Manager during configuration. ```solidity // ... inside SimplePayToReAccountAction ... function _execute( address originalMsgSender, address account, KeyValue[] calldata params ) external override returns (bytes memory) { address feed; uint256 postId; address token; uint256 expectedPrice; for (uint256 i = 0; i < params.length; i++) { if (params[i].key == keccak256("lens.param.feed")) { feed = abi.decode(params[i].value, (address)); } else if (params[i].key == keccak256("lens.param.postId")) { postId = abi.decode(params[i].value, (uint256)); } else if (params[i].key == keccak256("lens.param.token")) { token = abi.decode(params[i].value, (address)); } else if (params[i].key == keccak256("lens.param.expectedPrice")) { expectedPrice = abi.decode(params[i].value, (uint256)); } } require(expectedPrice > 0, "Valid expected price not found in params"); require(_prices[account][token] == expectedPrice, "Not configured or wrong price expected"); // Function located at LensPaymentHandler contract: _handlePayment({ payer: originalMsgSender, token: token, amount: expectedPrice, recipient: account, referrals: new RecipientData[](0), referralFeeBps: 0 }); uint256 repostId = IFeed(feed).repost({ postParams: CreatePostParams({ author: account, contentURI: "", repostedPostId: postId, quotedPostId: 0, repliedPostId: 0, ruleChanges: new RuleChange[](0), extraData: new KeyValue[](0) }), customParams: new KeyValue[](0), feedRulesParams: new RuleProcessingParams[](0), rootPostRulesParams: new RuleProcessingParams[](0), quotedPostRulesParams: new RuleProcessingParams[](0), }); return abi.encode(repostId); } ``` After adding the corresponding imports, our final code should look something like this: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import { KeyValue } from "contracts/core/Types.sol"; import { BaseAccountAction } from "contracts/actions/account/base/BaseAccountAction.sol"; import { IFeed } from "contracts/core/interfaces/IFeed.sol"; import { LensPaymentHandler } from "contracts/extensions/fees/LensPaymentHandler.sol"; /** * @title SimplePayToReAccountAction * @notice A simple Account action allowing users to pay for the Account to repost a given post. */ contract SimplePayToReAccountAction is BaseAccountAction, LensPaymentHandler { mapping(address account => mapping(address token => uint256 price)) private _prices; /** * @notice Configures the SimplePayToReAccount Action for a given post. * @param originalMsgSender The address initiating the configuration via the ActionHub. Must be post the account. * @param account The address of the account that configures the Pay-To-Repost for itself. * @param params KeyValue array containing the token and price configuration parameters. * @return bytes abi.encoded(token, price). */ function _configure( address originalMsgSender, address account, KeyValue[] calldata params ) internal override returns (bytes memory) { require(originalMsgSender == account, "Only account can configure"); require(IAccount(account).canExecuteTransactions(address(this)), "SimplePayToReAccountAction is not set as AccountManager"); address token; uint256 price = 0; for (uint256 i = 0; i < params.length; i++) { if (params[i].key == keccak256("lens.param.token")) { token = abi.decode(params[i].value, (address)); } else if (params[i].key == keccak256("lens.param.price")) { price = abi.decode(params[i].value, (uint256)); } } require(token != address(0), "Token not found in params"); IERC20(token).balanceOf(address(this)); // Just checking if the token is valid require(price > 0, "Valid price not found in params"); _prices[account][token] = price; // Emitting an event Lens_ActionHub_AccountAction_Configured happens automatically via ActionHub and params // already contain token & price, so we don't have to encode them again for emitting. return ""; } /** * @notice Executes a repost of a given post by a configured Account. * @param originalMsgSender The address initiating the repost via the ActionHub. * @param account The address of the account where the Pay-To-Repost Action was configured. * @param params Array of key-value pairs. Expected to contain the feed and postId to repost. * @return bytes feed and post. * Requirements: * - The `originalMsgSender` must not have voted on this `postId` before. * - `params` must not be empty and the first element's value must be abi-decodable as a boolean. */ function _execute( address originalMsgSender, address account, KeyValue[] calldata params ) external override returns (bytes memory) { address feed; uint256 postId; address token; uint256 expectedPrice; for (uint256 i = 0; i < params.length; i++) { if (params[i].key == keccak256("lens.param.feed")) { feed = abi.decode(params[i].value, (address)); } else if (params[i].key == keccak256("lens.param.postId")) { postId = abi.decode(params[i].value, (uint256)); } else if (params[i].key == keccak256("lens.param.token")) { token = abi.decode(params[i].value, (address)); } else if (params[i].key == keccak256("lens.param.expectedPrice")) { expectedPrice = abi.decode(params[i].value, (uint256)); } } require(expectedPrice > 0, "Valid expected price not found in params"); require(_prices[account][token] == expectedPrice, "Not configured or wrong price expected"); // Function located at LensPaymentHandler contract: _handlePayment({ payer: originalMsgSender, token: token, amount: expectedPrice, recipient: account, referrals: new RecipientData[](0), referralFeeBps: 0 }); uint256 repostId = IFeed(feed).repost({ postParams: CreatePostParams({ author: account, contentURI: "", repostedPostId: postId, quotedPostId: 0, repliedPostId: 0, ruleChanges: new RuleChange[](0), extraData: new KeyValue[](0) }), customParams: new KeyValue[](0), feedRulesParams: new RuleProcessingParams[](0), rootPostRulesParams: new RuleProcessingParams[](0), quotedPostRulesParams: new RuleProcessingParams[](0), }); return abi.encode(repostId); } } ``` ================ File: src/pages/protocol/accounts/block.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Block Accounts This guide explains how to block and unblock an Account on Lens. --- A Lens Account can block other Lens accounts from interacting with them, directly or indirectly. This is achieved by rejecting on-chain transactions that involve the blocking Account. The Lens API also honors the block status in features that are currently off-chain, such as reactions. ## Fetch Blocked Accounts Use the paginated `fetchAccountsBlocked` action to get a list of accounts that are blocked by the current account. ```ts filename="Example" import { fetchAccountsBlocked } from "@lens-protocol/client/actions"; const result = await fetchAccountsBlocked(client); if (result.isErr()) { return console.error(result.error); } // items: Array: [{blockedAt: DateTime, account: Account}, …] const { items, pageInfo } = result.value; ``` Use the paginated `accountsBlocked` query to get a list of accounts that are blocked by the current account. ```graphql filename="Query" query { accountsBlocked(request: {}) { items { blockedAt account { address metadata { name picture } } } pageInfo { prev next } } } ``` ```json filename="Response" { "data": { "accountsBlocked": { "items": [ { "blockedAt": "2022-01-01T00:00:00Z", "account": { "address": "0x1234…", "username": { "value": "lens/bob" }, "metadata": { "name": "Bob", "picture": "https://example.com/bob.jpg" } } } ], "pageInfo": { "prev": null, "next": "U29mdHdhcmU=" } } } } ``` Coming soon Continue with the [Pagination](../best-practices/pagination) guide for more information on how to handle paginated results. ## Block Account ### Block Specific Account You MUST be authenticated as Account Owner or Account Manager to make this request. Use the `blockAccount` action to block an account. ```ts filename="Block Account" import { evmAddress } from "@lens-protocol/client"; import { blockAccount } from "@lens-protocol/client/actions"; const result = await blockAccount(sessionClient, { account: evmAddress("0x1234…"), }); if (result.isErr()) { return console.error(result.error); } ``` Use the `block` mutation to block an account. ```graphql filename="Block Mutation" mutation { block(request: { account: "0x5071DeEcD24EBFA6161107e9a875855bF79f7b21" }) { ... on BlockResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on BlockError { error } ... on TransactionWillFail { reason } } } ``` Coming soon ### Handle Result Then, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,7" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await blockAccount(sessionClient, { account: evmAddress("0x1234…"), }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,7" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await blockAccount(sessionClient, { account: evmAddress("0x1234…"), }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Then, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Coming soon That's it—the account is now blocked. ## Unblock Account ### Unblock Specific Account You MUST be authenticated as Account Owner or Account Manager to make this request. Use the `unblockAccount` action to unblock an account. ```ts filename="Unblock Account" import { evmAddress } from "@lens-protocol/client"; import { unblockAccount } from "@lens-protocol/client/actions"; const result = await unblockAccount(sessionClient, { account: evmAddress("0x1234…"), }); if (result.isErr()) { return console.error(result.error); } ``` Use the `unblock` mutation to unblock an account. ```graphql filename="Unblock Mutation" mutation { unblock(request: { account: "0x5071DeEcD24EBFA6161107e9a875855bF79f7b21" }) { ... on UnblockResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on UnblockError { error } ... on TransactionWillFail { reason } } } ``` Coming soon ### Handle Result Then, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,7" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await unblockAccount(sessionClient, { account: evmAddress("0x1234…"), }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,7" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await unblockAccount(sessionClient, { account: evmAddress("0x1234…"), }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Then, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Coming soon That's it—the account is now unblocked. ================ File: src/pages/protocol/accounts/create.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Create an Account This guide will help you create your first Lens account. --- The process of creating a Lens Account differs slightly depending on whether you want to create a free username (such as one in the `lens/` Global Namespace) or a username within a [Custom Namespace](../usernames/custom-namespaces) that may be token-gated or require fees. ## Account With Free Username To create a new Account with a free Username, follow these steps ### Log In to Lens First, authenticate as an [Onboarding User](../authentication). Use the `@lens-protocol/client` package to authenticate with the user's wallet. ```ts filename="viem" import { evmAddress } from "@lens-protocol/client"; import { signMessageWith } from "@lens-protocol/client/viem"; import { client } from "./client"; const authenticated = await client.login({ onboardingUser: { app: evmAddress(""), wallet: signer.address, }, signMessage: signMessageWith(walletClient), }); if (authenticated.isErr()) { return console.error(authenticated.error); } // SessionClient: { ... } const sessionClient = authenticated.value; ``` ```ts filename="ethers" import { evmAddress } from "@lens-protocol/client"; import { signMessageWith } from "@lens-protocol/client/ethers"; import { client } from "./client"; const authenticated = await client.login({ onboardingUser: { app: evmAddress(""), wallet: signer.address, }, signMessage: signMessageWith(signer), }); if (authenticated.isErr()) { return console.error(authenticated.error); } // SessionClient: { ... } const sessionClient = authenticated.value; ``` Generate an authentication challenge: ```graphql filename="Onboarding User Challenge" mutation { challenge( request: { app: "" account: "" signedBy: "" } ) { id text } } ``` ```json filename="Response" { "data": { "challenge": { "id": "", "text": " wants you to sign in with your Ethereum account…" } } } ``` Sign it and acquire the authentication tokens: ```graphql filename="Mutation" mutation { authenticate(request: { id: "", signature: "" }) { ... on AuthenticationTokens { accessToken refreshToken idToken } ... on WrongSignerError { reason } ... on ExpiredChallengeError { reason } ... on ForbiddenError { reason } } } ``` ```json filename="AuthenticationTokens" { "data": { "authenticate": { "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6…", "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Ij…", "idToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Ij…" } } } ``` Coming soon The process is explained in detail in the [Authentication](../authentication) guide, so we will keep it brief here. ### Verify Username Then, verify if the desired username is available. Use the `canCreateUsername` action as follows: ```ts filename="Global Lens Namespace" import { canCreateUsername } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await canCreateUsername(sessionClient, { localName: "wagmi", }); if (result.isErr()) { return console.error(result.error); } result.value; // CanCreateUsernameResult ``` ```ts filename="Custom Namespace" import { evmAddress } from "@lens-protocol/client"; import { canCreateUsername } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await canCreateUsername(sessionClient, { localName: "wagmi", namespace: evmAddress("0x1234…"), }); if (result.isErr()) { return console.error(result.error); } result.value; // CanCreateUsernameResult ``` Use the `canCreateUsername` query as follows: ```graphql filename="Global Lens Namespace" query { canCreateUsername(request: { localName: "wagmi" }) { ...CanCreateUsernameResult } } ``` ```graphql filename="Custom Namespace" query { canCreateUsername(request: { localName: "wagmi", namespace: "0x1234…" }) { ...CanCreateUsernameResult } } ``` ```graphql filename="CanCreateUsernameResult" fragment CanCreateUsernameResult on CanCreateUsernameResult { __typename ... on NamespaceOperationValidationPassed { passed } ... on NamespaceOperationValidationUnknown { extraChecksRequired { __typename id type address executesOn config { ...AnyKeyValue } } } ... on NamespaceOperationValidationFailed { reason unsatisfiedRules { required { __typename rule reason message config { ...AnyKeyValue } } anyOf { __typename rule reason message config { ...AnyKeyValue } } } } ... on UsernameTaken { reason } } ``` Coming soon The `CanCreateUsernameResult` tells you if the logged-in user satisfy the Namespace Rules for creating a username, and if the desired username is available. ```ts filename="Check CanCreateUsernameResult" switch (data.__typename) { case "NamespaceOperationValidationPassed": // Creating a username is allowed break; case "NamespaceOperationValidationFailed": // Creating a username is not allowed console.log(data.reason); break; case "NamespaceOperationValidationUnknown": // Validation outcome is unknown break; case "UsernameTaken": // The desired username is not available break; } ``` Where: - `NamespaceOperationValidationPassed`: The logged-in user can create a username under the desired Namespace. - `NamespaceOperationValidationFailed`: Reposting is not allowed. The `reason` field explains why, and `unsatisfiedRules` lists the unmet requirements. - `NamespaceOperationValidationUnknown`: The Namespace has one or more _unknown rules_ requiring ad-hoc verification. The `extraChecksRequired` field provides the addresses and configurations of these rules. - `UsernameTaken`: The desired username is not available. Treat the `NamespaceOperationValidationUnknown` as _failed_ unless you intend to support the specific rules. See [Namespace Rules](./rules) for more information. ### Create Account Metadata Then, create an Account Metadata object. The following example demonstrates the most efficient way to create Account Metadata using [Grove storage](../../storage). Let's assume there is a form where the user can enter their name, bio, and a profile picture. See [other Account metadata fields](#advanced-options-account-metadata-fields) at the bottom of this page. ```tsx filename="Example Form"
``` Use an instance of the `StorageClient` to create a Grove folder containing your media files (such as a profile picture), and set the Account Metadata object as the index file. ```ts filename="index.ts" import type { Resource } from "@lens-chain/storage-client"; import { account } from "@lens-protocol/metadata"; import { storageClient } from "./storage"; // … async function onSubmit(event: SubmitEvent) { event.preventDefault(); const form = event.target as HTMLFormElement; const name = form.elements.namedItem("name")?.value || ""; const bio = form.elements.namedItem("bio")?.value || ""; const input = form.elements.namedItem("picture") as HTMLInputElement; // … const { folder, files } = await storage.uploadFolder(input.files, { index: (resources: Resource[]) => account({ name, picture: resources[0].uri, // this is a resolved lens://… URI bio, }), }); // folder.uri will be something like `lens://4f91ca…` } ``` ```ts filename="storage.ts" import { StorageClient } from "@lens-chain/storage-client"; export const storageClient = StorageClient.create(); ``` ### Deploy Account Contract Then, use the `createAccountWithUsername` action to deploy Lens Account smart contract and contextually create a username for it. ```ts filename="Simple Example" import { uri } from "@lens-protocol/client"; import { createAccountWithUsername } from "@lens-protocol/client/actions"; const result = await createAccountWithUsername(sessionClient, { username: { localName: "wagmi" }, metadataUri: uri(""), }); ``` ```ts filename="With Managers" import { evmAddress, uri } from "@lens-protocol/client"; import { createAccountWithUsername } from "@lens-protocol/client/actions"; const result = await createAccountWithUsername(sessionClient, { managers: [evmAddress("0x5071DeEcD24EBFA6161107e9a875855bF79f7b21")], username: { localName: "wagmi" }, metadataUri: uri(""), // lens://4f91ca… }); ``` ```ts filename="Free Custom Namespace" import { evmAddress, uri } from "@lens-protocol/client"; import { createAccountWithUsername } from "@lens-protocol/client/actions"; const result = await createAccountWithUsername(sessionClient, { username: { localName: "wagmi", namespace: evmAddress("0x1234…"), }, metadataUri: uri(""), // lens://4f91ca… }); ``` And, handle the result using the adapter for the library of your choice and wait for it to be indexed. ```ts filename="viem" highlight="1,9,10" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await createAccountWithUsername(sessionClient, { username: { localName: "wagmi" }, metadataUri: uri("lens://4f91ca…"), }) .andThen(handleOperationWith(walletClient)) .andThen(sessionClient.waitForTransaction); ``` ```ts filename="ethers" highlight="1,9,10" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await createAccountWithUsername(sessionClient, { username: { localName: "wagmi" }, metadataUri: uri("lens://4f91ca…"), }) .andThen(handleOperationWith(signer)) .andThen(sessionClient.waitForTransaction); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Then, use the `createAccountWithUsername` mutation to deploy Lens Account smart contract and contextually create a username for it. ```graphql filename="Mutation" mutation { createAccountWithUsername( request: { username: { localName: "jane" # lens/jane in this example # Optional. Defaults to lens/* namespace. # namespace: EvmAddress } metadataUri: "" # lens://4f91ca… # Optional. Specify managers to be set for the Account. # managers: ["0x5071DeEcD24EBFA6161107e9a875855bF79f7b21"] } ) { ... on CreateAccountResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on UsernameTaken { reason } ... on NamespaceOperationValidationFailed { reason } ... on TransactionWillFail { reason } } } ``` ```json filename="Response" { "data": { "createAccountWithUsername": { "hash": "0x5e9d8f8a4b8e4b8e4b8e4b8e4b8e4b8e4b8e4b" } } } ``` And, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Coming soon ### Switch to Account Owner Finally, switch to the [Account Owner](../authentication) authentication role to access the newly created Account. Use the `fetchAccount` action to retrieve the newly created Account, then call the `SessionClient.switchAccount` method to switch the authentication role to Account Owner for that Account. ```ts filename="Switch to Account Owner" highlight="1,2,12-17" import { nonNullable } from "@lens-protocol/client"; import { fetchAccount } from "@lens-protocol/client/actions"; // … const result = await createAccountWithUsername(sessionClient, { username: { localName: "wagmi" }, metadataUri: uri("lens://4f91ca…"), }) .andThen(handleOperationWith(walletClientOrSigner)) .andThen(sessionClient.waitForTransaction) .andThen((txHash) => fetchAccount(sessionClient, { txHash }).map(nonNullable)) .andThen((account) => sessionClient.switchAccount({ account: account.address, }), ); ``` Use the `account` query to retrieve the newly created Account's address: ```graphql filename="Query" query { account(request: { txHash: "0x5e9d8f8a4b8e4b8e4b8e4b8e4b8e4b8e4b8e4b" }) { address username metadata { name picture } } } ``` ```json filename="Response" { "data": { "account": { "address": "0x1234…", "username": "lens/jane", "metadata": { "name": "Jane Doe", "picture": "lens://4f91cab87ab5e4f5066f878b72…" } } } } ``` And, use it with the `switchAccount` mutation to switch to the Account Owner role. ```graphql filename="Mutation" mutation { switchAccount(request: { account: "0x1234…" }) { ... on AuthenticationTokens { accessToken refreshToken idToken } ... on ForbiddenError { reason } } } ``` ```json filename="AuthenticationTokens" { "data": { "switchAccount": { "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6…", "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Ij…", "idToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Ij…" } } } ``` Coming soon That's it—you are now authenticated with the newly created Account.
## Account With Restricted Username If you’re creating an Account with a username in a Namespace that enforces token gating or fees, follow these steps. ### Log In to Lens First, authenticate as an [Onboarding User](../authentication). Use the `@lens-protocol/client` package to authenticate with the user's wallet. ```ts filename="viem" import { evmAddress } from "@lens-protocol/client"; import { signMessageWith } from "@lens-protocol/client/viem"; import { client } from "./client"; const authenticated = await client.login({ onboardingUser: { app: evmAddress(""), wallet: signer.address, }, signMessage: signMessageWith(walletClient), }); if (authenticated.isErr()) { return console.error(authenticated.error); } // SessionClient: { ... } const sessionClient = authenticated.value; ``` ```ts filename="ethers" import { evmAddress } from "@lens-protocol/client"; import { signMessageWith } from "@lens-protocol/client/ethers"; import { client } from "./client"; const authenticated = await client.login({ onboardingUser: { app: evmAddress(""), wallet: signer.address, }, signMessage: signMessageWith(signer), }); if (authenticated.isErr()) { return console.error(authenticated.error); } // SessionClient: { ... } const sessionClient = authenticated.value; ``` Generate an authentication challenge: ```graphql filename="Onboarding User Challenge" mutation { challenge( request: { app: "" account: "" signedBy: "" } ) { id text } } ``` ```json filename="Response" { "data": { "challenge": { "id": "", "text": " wants you to sign in with your Ethereum account…" } } } ``` Sign it and acquire the authentication tokens: ```graphql filename="Mutation" mutation { authenticate(request: { id: "", signature: "" }) { ... on AuthenticationTokens { accessToken refreshToken idToken } ... on WrongSignerError { reason } ... on ExpiredChallengeError { reason } ... on ForbiddenError { reason } } } ``` ```json filename="AuthenticationTokens" { "data": { "authenticate": { "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6…", "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Ij…", "idToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Ij…" } } } ``` Coming soon The process is explained in detail in the [Authentication](../authentication) guide, so we will keep it brief here. ### Verify Username Then, verify if the desired username is available and the user's wallet satisfies the Namespace Rules. Use the `canCreateUsername` action as follows: ```ts filename="Verify Username Availability" import { evmAddress, RulesSubject } from "@lens-protocol/client"; import { canCreateUsername } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await canCreateUsername(sessionClient, { localName: "wagmi", namespace: evmAddress("0x1234…"), rulesSubject: RulesSubject.Signer, }); if (result.isErr()) { return console.error(result.error); } result.value; // CanCreateUsernameResult ``` Use the `canCreateUsername` query as follows: ```graphql filename="Verify Username Availability" query { canCreateUsername( request: { localName: "wagmi", namespace: "0x1234…", rulesSubject: SIGNER } ) { ...CanCreateUsernameResult } } ``` ```graphql filename="CanCreateUsernameResult" fragment CanCreateUsernameResult on CanCreateUsernameResult { __typename ... on NamespaceOperationValidationPassed { passed } ... on NamespaceOperationValidationUnknown { extraChecksRequired { __typename id type address executesOn config { ...AnyKeyValue } } } ... on NamespaceOperationValidationFailed { reason unsatisfiedRules { required { __typename rule reason message config { ...AnyKeyValue } } anyOf { __typename rule reason message config { ...AnyKeyValue } } } } ... on UsernameTaken { reason } } ``` Coming soon Setting `rulesSubject` to _signer_ means that the Namespace Rules will be checked against the wallet address of the currently logged-in user. The `CanCreateUsernameResult` tells you if the logged-in user satisfy the Namespace Rules for creating a username, and if the desired username is available. ```ts filename="Check CanCreateUsernameResult" switch (data.__typename) { case "NamespaceOperationValidationPassed": // Creating a username is allowed break; case "NamespaceOperationValidationFailed": // Creating a username is not allowed console.log(data.reason); break; case "NamespaceOperationValidationUnknown": // Validation outcome is unknown break; case "UsernameTaken": // The desired username is not available break; } ``` Where: - `NamespaceOperationValidationPassed`: The logged-in user can create a username under the desired Namespace. - `NamespaceOperationValidationFailed`: Reposting is not allowed. The `reason` field explains why, and `unsatisfiedRules` lists the unmet requirements. - `NamespaceOperationValidationUnknown`: The Namespace has one or more _unknown rules_ requiring ad-hoc verification. The `extraChecksRequired` field provides the addresses and configurations of these rules. - `UsernameTaken`: The desired username is not available. Treat the `NamespaceOperationValidationUnknown` as _failed_ unless you intend to support the specific rules. See [Namespace Rules](./rules) for more information. ### Create Account Metadata Then, create an Account Metadata object. The following example demonstrates the most efficient way to create Account Metadata using [Grove storage](../../storage). Let's assume there is a form where the user can enter their name, bio, and a profile picture. See [other Account metadata fields](#advanced-options-account-metadata-field) at the bottom of this page. ```tsx filename="Example Form"
``` Use an instance of the `StorageClient` to create a Grove folder containing your media files (such as a profile picture), and set the Account Metadata object as the index file. ```ts filename="index.ts" import type { Resource } from "@lens-chain/storage-client"; import { account } from "@lens-protocol/metadata"; import { storageClient } from "./storage"; // … async function onSubmit(event: SubmitEvent) { event.preventDefault(); const form = event.target as HTMLFormElement; const name = form.elements.namedItem("name")?.value || ""; const bio = form.elements.namedItem("bio")?.value || ""; const input = form.elements.namedItem("picture") as HTMLInputElement; // … const { folder, files } = await storage.uploadFolder(input.files, { index: (resources: Resource[]) => account({ name, picture: resources[0].uri, // this is a resolved lens://… URI bio, }), }); // folder.uri will be something like `lens://4f91ca…` } ``` ```ts filename="storage.ts" import { StorageClient } from "@lens-chain/storage-client"; export const storageClient = StorageClient.create(); ``` ### Deploy Account Contract Then, use the `createAccount` action to deploy Lens Account smart contract without a username (yet). ```ts filename="Simple Example" import { uri } from "@lens-protocol/client"; import { createAccount } from "@lens-protocol/client/actions"; const result = await createAccount(sessionClient, { metadataUri: uri(""), }); ``` ```ts filename="With Managers" import { evmAddress, uri } from "@lens-protocol/client"; import { createAccount } from "@lens-protocol/client/actions"; const result = await createAccount(sessionClient, { managers: [evmAddress("0x5071DeEcD24EBFA6161107e9a875855bF79f7b21")], metadataUri: uri(""), // lens://4f91ca… }); ``` And, handle the result using the adapter for the library of your choice and wait for it to be indexed. ```ts filename="viem" highlight="1,8-9" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await createAccount(sessionClient, { metadataUri: uri("lens://4f91ca…"), }) .andThen(handleOperationWith(walletClient)) .andThen(sessionClient.waitForTransaction); ``` ```ts filename="ethers" highlight="1,8-9" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await createAccount(sessionClient, { metadataUri: uri("lens://4f91ca…"), }) .andThen(handleOperationWith(signer)) .andThen(sessionClient.waitForTransaction); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Then, use the `createAccount` mutation to deploy Lens Account smart contract without a username (yet). ```graphql filename="Mutation" mutation { createAccount( request: { metadataUri: "" # lens://4f91ca… # Optional. Specify managers to be set for the Account. # managers: ["0x5071DeEcD24EBFA6161107e9a875855bF79f7b21"] } ) { ... on CreateAccountResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on UsernameTaken { reason } ... on NamespaceOperationValidationFailed { reason } ... on TransactionWillFail { reason } } } ``` ```json filename="Response" { "data": { "createAccountWithUsername": { "hash": "0x5e9d8f8a4b8e4b8e4b8e4b8e4b8e4b8e4b8e4b" } } } ``` And, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Coming soon ### Switch to Account Owner Then, switch to the [Account Owner](../authentication) authentication role to access the newly created Account. Use the `fetchAccount` action to retrieve the newly created Account, then call the `SessionClient.switchAccount` method to switch the authentication role to Account Owner for that Account. ```ts filename="Switch to Account Owner" highlight="1,2,11-16" import { nonNullable } from "@lens-protocol/client"; import { fetchAccount } from "@lens-protocol/client/actions"; // … const result = await createAccount(sessionClient, { metadataUri: uri("lens://4f91ca…"), }) .andThen(handleOperationWith(walletClientOrSigner)) .andThen(sessionClient.waitForTransaction) .andThen((txHash) => fetchAccount(sessionClient, { txHash }).map(nonNullable)) .andThen((account) => sessionClient.switchAccount({ account: account.address, }), ); ``` Use the `account` query to retrieve the newly created Account's address: ```graphql filename="Query" query { account(request: { txHash: "0x5e9d8f8a4b8e4b8e4b8e4b8e4b8e4b8e4b8e4b" }) { address username metadata { name picture } } } ``` ```json filename="Response" { "data": { "account": { "address": "0x1234…", "username": "lens/jane", "metadata": { "name": "Jane Doe", "picture": "lens://4f91cab87ab5e4f5066f878b72…" } } } } ``` And, use it with the `switchAccount` mutation to switch to the Account Owner role. ```graphql filename="Mutation" mutation { switchAccount(request: { account: "0x1234…" }) { ... on AuthenticationTokens { accessToken refreshToken idToken } ... on ForbiddenError { reason } } } ``` ```json filename="AuthenticationTokens" { "data": { "switchAccount": { "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6…", "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Ij…", "idToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Ij…" } } } ``` Coming soon ### Create Username Finally, use the `createUsername` action to assign a username to the newly created Account within the desired restricted Namespace. Use the `createUsername` action to create the desired username. ```ts filename="Restricted Namespace" import { evmAddress, RulesSubject } from "@lens-protocol/client"; import { createUsername } from "@lens-protocol/client/actions"; const result = await createUsername(sessionClient, { username: { localName: "wagmi", namespace: evmAddress("0x1234…"), }, rulesSubject: RulesSubject.Signer, }); ``` And, handle the result using the adapter for the library of your choice and wait for it to be indexed. ```ts filename="viem" highlight="1,12,13" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await createUsername(sessionClient, { username: { localName: "wagmi", namespace: evmAddress("0x1234…"), }, rulesSubject: RulesSubject.Signer, }) .andThen(handleOperationWith(walletClient)) .andThen(sessionClient.waitForTransaction); ``` ```ts filename="ethers" highlight="1,12,13" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await createUsername(sessionClient, { username: { localName: "wagmi", namespace: evmAddress("0x1234…"), }, rulesSubject: RulesSubject.Signer, }) .andThen(handleOperationWith(signer)) .andThen(sessionClient.waitForTransaction); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Use the `createUsername` mutation to create the desired username. ```graphql filename="Restricted Namespace" mutation { createUsername( request: { username: { localName: "wagmi", namespace: "0x1234…" } rulesSubject: SIGNER } ) { ... on CreateUsernameResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on NamespaceOperationValidationFailed { reason } ... on TransactionWillFail { reason } } } ``` ```json filename="CreateUsernameResponse" { "data": { "createUsername": { "hash": "0x…" } } } ``` And, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Coming soon Setting `rulesSubject` to _signer_ means that the Namespace Rules will be checked against the wallet address of the currently logged-in user. That's it—you are now authenticated with the newly created Account and have a username in a restricted Namespace.
--- ## Advanced Options ### Account Metadata Fields Account Metadata object is a JSON object that contains information about the Account, such as its name, bio, profile picture, and other attributes. It is used to display the Account's information in the Lens ecosystem. Use the `@lens-protocol/metadata` package to construct a valid `AccountMetadata` object: ```ts filename="Example" import { MetadataAttributeType, account } from "@lens-protocol/metadata"; const metadata = account({ name: "Jane Doe", bio: "I am a photographer based in New York City.", picture: "lens://4f91cab87ab5e4f5066f878b72…", coverPicture: "lens://4f91cab87ab5e4f5066f8…", attributes: [ { key: "twitter", type: MetadataAttributeType.STRING, value: "https://twitter.com/janedoexyz", }, { key: "dob", type: MetadataAttributeType.DATE, value: "1990-01-01T00:00:00Z", }, { key: "enabled", type: MetadataAttributeType.BOOLEAN, value: "true", }, { key: "height", type: MetadataAttributeType.NUMBER, value: "1.65", }, { key: "settings", type: MetadataAttributeType.JSON, value: '{"theme": "dark"}', }, ], }); ``` If you opted for manually create Metadata objects, make sure it conform to the [Account Metadata JSON Schema](https://json-schemas.lens.dev/account/1.0.0.json). ```json filename="Example" { "$schema": "https://json-schemas.lens.dev/account/1.0.0.json", "lens": { "id": "6418053f-89ae-4f9e-8362-8d1b6c0cdafb", "name": "Jane Doe", "bio": "I am a photographer based in New York City.", "picture": "lens://4f91cab87ab5e4f5066f878b72…", "coverPicture": "lens://4f91cab87ab5e4f5066f8…", "attributes": [ { "key": "twitter", "type": "String", "value": "https://twitter.com/janedoexyz" }, { "key": "dob", "type": "Date", "value": "1990-01-01T00:00:00Z" }, { "key": "enabled", "type": "Boolean", "value": "true" }, { "key": "height", "type": "Number", "value": "1.65" }, { "key": "settings", "type": "JSON", "value": "{\"theme\": \"dark\"}" } ] } } ``` See the [Lens Metadata Standards](../best-practices/metadata-standards) guide for more information on creating and hosting Metadata objects. ================ File: src/pages/protocol/accounts/feedback.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Provide Feedback This guide will show you tools for users to provide account related feedback on Lens. --- ## Report an Account Users can report an Account if they find it inappropriate or offensive. Reporting an Account will help Lens ML algorithms to improve over time. Users can chose between the following reasons: ```graphql filename="AccountReportReason" enum AccountReportReason { IMPERSONATION REPETITIVE_SPAM OTHER } ``` You MUST be authenticated as Account Owner or Account Manager to make this request. Use the `reportAccount` action to report an account on Lens. ```ts filename="Report Account" import { evmAddress, AccountReportReason, postId } from "@lens-protocol/client"; import { reportAccount } from "@lens-protocol/client/actions"; const result = await reportAccount(sessionClient, { report: AccountReportReason.REPETITIVE_SPAM, account: evmAddress("0x123"), additionalComment: "This account is a spammer!" // optional referencedPosts: [postId("42"), postId("43")] // optional }); if (result.isErr()) { return console.error(result.error); } ``` Use the `reportAccount` mutation to report an account on Lens. ```graphql filename="Mutation" mutation { reportAccount( request: { account: "0x123" reason: REPETITIVE_SPAM # optional, free-form text # additionalComment: String # optional, list of post ids # referencedPosts: [String] } ) } ``` ```json filename="Response" { "data": { "reportAccount": null } } ``` Coming soon That's it—you've successfully reported an account on Lens! ## Recommend an Account Users have the ability to recommend an account if they think it's a good fit for the Lens community. The Lens ML algorithms will use this feedback to improve the user's feed. You MUST be authenticated as Account Owner or Account Manager to make these requests. Use the `recommendAccount` action to recommend an account. ```ts filename="Recommend Account" import { evmAddress } from "@lens-protocol/client"; import { recommendAccount } from "@lens-protocol/client/actions"; const result = await recommendAccount(sessionClient, { account: evmAddress("0x123"), }); if (result.isErr()) { return console.error(result.error); } ``` Use the `undoRecommendAccount` action to dismiss a recommended account. ```ts filename="Undo Recommended Account" import { evmAddress } from "@lens-protocol/client"; import { undoRecommendAccount } from "@lens-protocol/client/actions"; const result = await undoRecommendAccount(sessionClient, { account: evmAddress("0x123"), }); if (result.isErr()) { return console.error(result.error); } ``` Use the `recommendAccount` mutation to recommend an account. ```graphql filename="Mutation" mutation { recommendAccount(request: { account: "0x123" }) } ``` ```json filename="Response" { "data": { "recommendAccount": null } } ``` Use the `undoRecommendAccount` mutation to undo the "Recommend" action. ```graphql filename="Mutation" mutation { undoRecommendAccount(request: { account: "0x123" }) } ``` ```json filename="Response" { "data": { "undoRecommendAccount": null } } ``` Coming soon ================ File: src/pages/protocol/accounts/manager.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Account Managers This guide explains how to delegate social activities to an Account Manager. --- An Account Manager is an EVM address authorized to sign social operations on behalf of an Account. This allows the Account owner to maintain control while delegating the execution of social operations to one or more Account Managers. ## Security Considerations An Account Manager can sign most Account operations, except for those that, for security reasons, require the Account owner's signature. The Tiered Transaction Model described in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide specifies that Social Operations will fall back to a signed execution mode if the operation specifics require it. For example, _free collects_ can be signless, while _paid collects_ will require a user signature. Updating Account Managers is considered a sensitive operation and thus always requires the Account owner's signature. For this reason, all mutations involving Account Managers are [Restricted Operations](../best-practices/transaction-lifecycle#tiered-transaction-model-restricted-operations). ## Add Account Managers ### Add Manager To Owned Account You MUST be authenticated as Account Owner to make this request. Use the `addAccountManager` action to add an Account Manager to the logged-in Account. ```ts filename="Add Manager With All Permissions" import { evmAddress } from "@lens-protocol/client"; import { addAccountManager } from "@lens-protocol/client/actions"; const result = await addAccountManager(sessionClient, { address: evmAddress("0x1234…"), }); ``` ```ts filename="Add Manager With Tailored Permissions" import { evmAddress } from "@lens-protocol/client"; import { addAccountManager } from "@lens-protocol/client/actions"; const result = await addAccountManager(sessionClient, { address: evmAddress("0x1234…"), permissions: { canExecuteTransactions: true, canTransferTokens: false, canTransferNative: false, canSetMetadataUri: true, }, }); ``` Use the `addAccountManager` mutation to add an Account Manager to the logged-in Account. ```graphql filename="AddAccountManager.graphql" mutation { addAccountManager( request: { address: "0x5071DeEcD24EBFA6161107e9a875855bF79f7b21" permissions: { canExecuteTransactions: true canTransferTokens: false canTransferNative: false canSetMetadataUri: true } } ) { ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` where: - `canExecuteTransactions`: Indicates whether the new Account Manager can execute transactions. - `canTransferTokens`: Indicates whether the new Account Manager can transfer tokens. - `canTransferNative`: Indicates whether the new Account Manager can transfer native tokens. - `canSetMetadataUri`: Indicates whether the new Account Manager can set the Account's Metadata URI. Coming soon ### Handle Result Then, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await addAccountManager(sessionClient, { address: evmAddress("0x1234…"), }) .andThen(handleOperationWith(walletClient)) .andThen(client.waitForTransaction); ``` ```ts filename="ethers" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await addAccountManager(sessionClient, { address: evmAddress("0x1234…"), }) .andThen(handleOperationWith(signer)) .andThen(client.waitForTransaction); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Then, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Coming soon ## Remove Account Managers ### Remove Manager From Owned Account You MUST be authenticated as Account Owner to make this request. Use the `removeAccountManager` action to remove an Account Manager from the logged-in Account. ```ts filename="Remove Account Manager" import { evmAddress } from "@lens-protocol/client"; import { removeAccountManager } from "@lens-protocol/client/actions"; const result = await removeAccountManager(sessionClient, { manager: evmAddress("0x1234…"), }); ``` Use the `removeAccountManager` mutation to remove an Account Manager from the logged-in Account. ```graphql filename="RemoveAccountManager.graphql" mutation { removeAccountManager( request: { manager: "0x5071DeEcD24EBFA6161107e9a875855bF79f7b21" } ) { ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` Coming soon ### Handle Result Then, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await removeAccountManager(sessionClient, { manager: evmAddress("0x1234…"), }) .andThen(handleOperationWith(walletClient)) .andThen(client.waitForTransaction); ``` ```ts filename="ethers" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await removeAccountManager(sessionClient, { manager: evmAddress("0x1234…"), }) .andThen(handleOperationWith(signer)) .andThen(client.waitForTransaction); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Then, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Coming soon ## Update Account Manager Permissions ### Update Manager Permissions You MUST be authenticated as Account Owner to make this request. Use the `updateAccountManager` action to update an Account Manager's permissions. ```ts filename="Example" import { evmAddress } from "@lens-protocol/client"; import { updateAccountManager } from "@lens-protocol/client/actions"; const result = await updateAccountManager(sessionClient, { manager: evmAddress("0x1234…"), permissions: { canExecuteTransactions: true, canTransferTokens: false, canTransferNative: false, canSetMetadataUri: true, }, }); ``` Use the `updateAccountManager` mutation to update an Account Manager's permissions. ```graphql filename="UpdateAccountManager.graphql" mutation { updateAccountManager( request: { manager: "0x5071DeEcD24EBFA6161107e9a875855bF79f7b21" permissions: { canExecuteTransactions: true canTransferTokens: false canTransferNative: false canSetMetadataUri: true } } ) { ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` Now, you can submit and monitor the transaction as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle#transaction-requests) guide. Coming soon ### Handle Result Then, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,14" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await updateAccountManager(sessionClient, { manager: evmAddress("0x1234…"), permissions: { canExecuteTransactions: true, canTransferTokens: false, canTransferNative: false, canSetMetadataUri: true, }, }) .andThen(handleOperationWith(walletClient)) .andThen(client.waitForTransaction); ``` ```ts filename="ethers" highlight="1,14" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await updateAccountManager(sessionClient, { manager: evmAddress("0x1234…"), permissions: { canExecuteTransactions: true, canTransferTokens: false, canTransferNative: false, canSetMetadataUri: true, }, }) .andThen(handleOperationWith(walletClient)) .andThen(client.waitForTransaction); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Then, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Coming soon ## Signless Experience By leveraging the Account Manager feature, you can enable a _signless experience_ for social interactions through the Lens API. ### Enable/Remove Signless Experience You MUST be authenticated as Account Owner to make this request. Use the `enableSignless` action to set up the Lens API as Account Manager for the logged-in Account, or the `removeSignless` action to remove it. ```ts filename="Enable Signless" import { enableSignless } from "@lens-protocol/client/actions"; const result = await enableSignless(sessionClient); if (result.isErr()) { return console.error(result.error); } ``` ```ts filename="Remove Signless" import { removeSignless } from "@lens-protocol/client/actions"; const result = await removeSignless(sessionClient); if (result.isErr()) { return console.error(result.error); } ``` Use the `enableSignless` mutation to set up the Lens API as Account Manager for your Account, or the `removeSignless` mutation to disable it. ```graphql filename="EnableSignless.graphql" mutation { enableSignless { ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ```graphql filename="RemoveSignless.graphql" mutation { removeSignless { ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` Coming soon ### Handle Result Then, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,6" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await enableSignless(sessionClient) .andThen(handleOperationWith(walletClient)) .andThen(client.waitForTransaction); ``` ```ts filename="ethers" highlight="1,6" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await enableSignless(sessionClient) .andThen(handleOperationWith(signer)) .andThen(client.waitForTransaction); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Then, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Coming soon ## Account Managers Visibility ### List Account Managers Use the paginated `fetchAccountManagers` action to list the Account Managers for the logged-in Account. You MUST be authenticated as Account Owner or Account Manager to make this request. ```ts filename="Example" import { fetchAccountManagers } from "@lens-protocol/client/actions"; const result = await fetchAccountManagers(sessionClient); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; export const client = PublicClient.create({ environment: mainnet, }); ``` You can use the paginated `accountManagers` query to list the Account Managers for the logged-in Account. You MUST be authenticated as Account Owner or Account Manager to make this request. ```graphql filename="ListAccountManagers.graphql" query { accountManagers { items { # The address of the Account Manager. manager # Whether the Account Manager is a Lens manager. isLensManager # The permissions the Account Manager has. permissions { canExecuteTransactions canTransferTokens canTransferNative canSetMetadataUri } # The date the Account Manager was added. addedAt } pageInfo { prev next } } } ``` ```json filename="Response" { "data": { "accountManagers": { "items": [ { "manager": "0x5071DeEcD24EBFA6161107e9a875855bF79f7b21", "isLensManager": false, "permissions": { "canExecuteTransactions": true, "canTransferTokens": false, "canTransferNative": false, "canSetMetadataUri": true }, "addedAt": "2021-09-01T00:00:00Z" } ], "pageInfo": { "prev": null, "next": null } } } } ``` where: - `manager`: The address of the Account Manager. - `isLensManager`: Indicates whether the Account Manager is a Lens API Manager for [Signless Experience](#signless-experience). - `permissions`: The permissions the Account Manager has. - `addedAt`: The date time the Account Manager was added to the Account. Coming soon See the [Pagination](../best-practices/pagination) guide for more information on how to handle paginated results. ### Hide/Unhide an Account Managed When a wallet manages multiple Accounts, users can hide some of them from the Available Accounts list to simplify the login experience on Lens Apps. You MUST be authenticated as [Account Manager or Account Owner](../authentication) to perform this operation. Use the `hideManagedAccount` action to hide an Account managed by your account. To undo this action, use the `unhideManagedAccount` action. ```ts filename="Hide Managed Account" import { evmAddress } from "@lens-protocol/client"; import { hideManagedAccount } from "@lens-protocol/client/actions"; const result = await hideManagedAccount(sessionClient, { account: evmAddress("0x1234…"), }); if (result.isErr()) { return console.error(result.error); } ``` ```ts filename="Unhide Managed Account" import { evmAddress } from "@lens-protocol/client"; import { unhideManagedAccount } from "@lens-protocol/client/actions"; const result = await unhideManagedAccount(sessionClient, { account: evmAddress("0x1234…"), }); if (result.isErr()) { return console.error(result.error); } ``` Use the `hideManagedAccount` mutation to hide an Account managed by your account. To undo this action, use the `unhideManagedAccount` mutation. ```graphql filename="Hide Mutation" mutation { hideManagedAccount( request: { account: "0x5071DeEcD24EBFA6161107e9a875855bF79f7b21" } ) } ``` ```graphql filename="Undo Mutation" mutation { unhideManagedAccount( request: { account: "0x5071DeEcD24EBFA6161107e9a875855bF79f7b21" } ) } ``` Coming soon That's it—the Account Managed is now hidden from your list of Accounts Managed. ================ File: src/pages/protocol/accounts/metadata.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Update Account Metadata This guide will help you update Account details like profile picture, name, and bio. --- To update Account Metadata, you need to: 1. Create a new Account Metadata object. 2. Upload the Account Metadata object onto a public URI. 3. Set the URI of the Account Metadata on your Lens Account. The first two steps are similar to the ones in the [Create an Account](./create) guide so we'll keep them brief. See the [Lens Metadata Standards](../best-practices/metadata-standards) guide for more information on creating and hosting Metadata objects. ## Create Account Metadata First, create a new Account Metadata object with the updated details. It's developer responsability to copy over any existing data that should be retained. ```ts filename="TS/JS" import { MetadataAttributeType, account } from "@lens-protocol/metadata"; const metadata = account({ name: "Jane Doe", bio: "I am a photographer based in New York City.", picture: "lens://4f91cab87ab5e4f5066f878b72…", coverPicture: "lens://4f91cab87ab5e4f5066f878b78…", attributes: [ { key: "twitter", type: MetadataAttributeType.STRING, value: "https://twitter.com/janedoexyz", }, { key: "dob", type: MetadataAttributeType.DATE, value: "1990-01-01T00:00:00Z", }, { key: "enabled", type: MetadataAttributeType.BOOLEAN, value: "true", }, { key: "height", type: MetadataAttributeType.NUMBER, value: "1.65", }, { key: "settings", type: MetadataAttributeType.JSON, value: '{"theme": "dark"}', }, ], }); ``` ```json filename="JSON Schema" { "$schema": "https://json-schemas.lens.dev/account/1.0.0.json", "lens": { "id": "6418053f-89ae-4f9e-8362-8d1b6c0cdafb", "name": "Jane Doe", "bio": "I am a photographer based in New York City.", "picture": "lens://4f91cab87ab5e4f5066f878b72…", "coverPicture": "lens://4f91cab87ab5e4f5066f8…", "attributes": [ { "key": "twitter", "type": "String", "value": "https://twitter.com/janedoexyz" }, { "key": "dob", "type": "Date", "value": "1990-01-01T00:00:00Z" }, { "key": "enabled", "type": "Boolean", "value": "true" }, { "key": "height", "type": "Number", "value": "1.65" }, { "key": "settings", "type": "JSON", "value": "{\"theme\": \"dark\"}" } ] } } ``` ## Upload Account Metadata Then, upload the Account Metadata object to a public URI. ```ts import { account } from "@lens-protocol/metadata"; import { storageClient } from "./storage-client"; const metadata = account({ name: "Jane Doe", }); const { uri } = await storageClient.uploadAsJson(metadata); console.log(uri); // e.g., lens://4f91ca… ``` This example uses [Grove storage](../../storage) to host the Metadata object. See the [Lens Metadata Standards](../best-practices/metadata-standards#host-metadata-objects) guide for more information on hosting Metadata objects. ## Set Account Metadata URI You MUST be authenticated as Account Owner or Account Manager to make this request. Then, you can use the `setAccountMetadata` action to update the Account Metadata URI. ```ts filename="Set Account Metadata" import { uri } from "@lens-protocol/client"; import { setAccountMetadata } from "@lens-protocol/client/actions"; const result = await setAccountMetadata(sessionClient, { metadataUri: uri("lens://4f91ca…"), }); ``` Then, you can use the `setAccountMetadata` mutation to update the Account Metadata URI. ```graphql filename="Mutation" mutation { setAccountMetadata(request: { metadataUri: "lens://4f91ca…" }) { ... on SetAccountMetadataResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ```json filename="SetAccountMetadataResponse" { "data": { "setAccountMetadata": { "hash": "0x…" } } } ``` Coming soon ## Handle Result Finally, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,7" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await setAccountMetadata(sessionClient, { metadataUri: uri("lens://4f91ca…"), }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,7" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await setAccountMetadata(sessionClient, { username: { localName: "wagmi" }, metadataUri: uri("lens://4f91ca…"), }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Finally, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Coming soon That's it—you now know how to update metadata for a Lens Account. ================ File: src/pages/protocol/accounts/mute.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Mute Accounts This guide will show you how to mute accounts in Lens API. --- The Lens API allows you to mute an account, so you won’t receive notifications from the muted account’s activities. This feature only affects the data returned to the account performing the mute; the muted account will still be able to interact with its content. To prevent an account from interacting with your content entirely, see [Block Accounts](./block). You MUST be authenticated as Account Owner or Account Manager to make this request. Use the `mute` action to mute an account. To unmute the account, use the `unmute` action. ```ts filename="Mute" import { evmAddress } from "@lens-protocol/client"; import { muteAccount } from "@lens-protocol/client/actions"; const result = await muteAccount(sessionClient, { account: evmAddress("01234…"), }); if (result.isErr()) { return console.error(result.error); } ``` ```ts filename="Unmute" import { evmAddress } from "@lens-protocol/client"; import { unmuteAccount } from "@lens-protocol/client/actions"; const result = await unmuteAccount(sessionClient, { account: evmAddress("01234…"), }); if (result.isErr()) { return console.error(result.error); } ``` Use the `mute` mutation to mute an account. To unmute the account, use the `unmute` mutation. ```graphql filename="Mute Mutation" mutation { mute(request: { account: "0x1234…" }) } ``` ```graphql filename="Unmute Mutation" mutation { unmute(request: { account: "0x1234…" }) } ``` Coming soon That's it—the account is now muted. ================ File: src/pages/protocol/accounts/notifications.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Notifications This guide will help you retrieve Account related notifications. --- When an Account interacts with you or your content, the activity is recorded and notification detailing the interaction is stored in the Lens API. ## Notification types There are various types of notifications, each with a slightly different structure, requiring individual handling. The available notification types include: - **CommentNotification** - when someone comments on your post - **FollowNotification** - when someone follows you - **MentionNotification** - when someone mentions you in a post - **RepostNotification** - when someone reposts your post - **QuoteNotification** - when someone quotes your post - **ReactionNotification** - when someone adds reaction to your post At any given time new notification types may be added to the Lens API. Make sure you handle gracefully unknown notification types to avoid breaking your app. ## Notifications Query You MUST be authenticated as Account Owner or Account Manager to make this request. Use the paginated `fetchNotifications` action to retrieve notifications for an Account. ```ts filename="Any Graph/Feed" import { evmAddress } from "@lens-protocol/client"; import { fetchNotifications } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchNotifications(client, {}); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="Global Graph/Feed" import { evmAddress } from "@lens-protocol/client"; import { fetchNotifications } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchNotifications(client, { filter: { graphs:[{ globalGraph: true }] feeds:[{ globalFeed: true }] } }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="Custom Graph/Feed" import { evmAddress } from "@lens-protocol/client"; import { fetchNotifications } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchNotifications(client, { filter: { graphs:[{ graph: evmAddress("0x1234…") }] feeds:[{ feed: evmAddress("0x5678…") }] } }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="App Graph/Feed" import { evmAddress } from "@lens-protocol/client"; import { fetchNotifications } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchNotifications(client, { filter: { graphs:[{ app: evmAddress("0x1234…") }] feeds:[{ app: evmAddress("0x5678…") }] } }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="Others" import { NotificationType } from "@lens-protocol/client"; import { fetchNotifications } from "@lens-protocol/client/actions"; const result = await fetchNotifications(sessionClient, { filter: { timeBasedAggregation: true, includeLowScore: false, notificationTypes: [NotificationType.Commented, NotificationType.Followed] apps: [evmAddress("0x1234…"), evmAddress("0x5678…")] } }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` The Lens API offers a `notifications` query to retrieve notifications for an Account. ```graphql filename="Query" query Notifications( $request: NotificationRequest! ) { notifications(request: { filter: { # optional, filter by graphs (by default, all graphs are included) graphs: [ # optional, filter by global graph { globalGraph: true } # and/or, filter by graph address # { # graph: EvmAddress # } # and/or, filter by graph associated w/ an app address # { # app: EvmAddress # } ], # optional, filter by feeds (by default, all feeds are included) feeds: [ # optional, filter by global feed { globalFeed: true } # and/or, filter by feed address # { # feed: EvmAddress # } # and/or, filter by ALL feeds associated w/ an app address # { # app: EvmAddress # } ], # optional, default to any apps # apps: [EvmAddress!] # optional, defaults to all notifications # notificationTypes: [NotificationType!] # optional, include notification from low score accounts, defaults to false includeLowScore: false # optional, defaults to true timeBasedAggregation: true } # optional order # orderBy: NotificationOrderBy }) { items { ... on ReactionNotification { __typename id reactions { account { address } reactions { reaction reactedAt } } post { id } } ... on CommentNotification { __typename id comment { id } } ... on RepostNotification { __typename id reposts { repostId repostedAt account { address } } post { id } } ... on QuoteNotification { __typename id quote { id } } ... on FollowNotification { __typename id followers { followedAt account { address } } } ... on MentionNotification { __typename id post { id } } } pageInfo { next prev } } } ``` ```json filename="Response" { "data": { "notifications": { "items": [ { "__typename": "CommentNotification", "id": "notification-id", "comment": { "id": "300" } }, { "__typename": "RepostNotification", "id": "notification-id", "reposts": [ { "repostId": "200", "repostedAt": "2024-01-20T09:00:00Z", "account": { "address": "0x3234567890123456789012345678901234567890" } } ], "post": { "id": "100" } }, ], "pageInfo": { "prev": null, "next": null } } } } ``` Coming soon Continue with the [Pagination](../best-practices/pagination) guide for more information on how to handle paginated results. ================ File: src/pages/protocol/apps/authorization-workflows.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; export default ({ children }) => {children}; {/* Start of the page content */} # Authorization Workflows This guide will show you how you can control how user interact with your app. --- The Lens API empowers builders with enhanced control over user interactions within their app. Specifically, builders can: - Determine who is allowed to log in to the Lens API as a user of their Lens App - Revoke user credentials at any time - Decide whether the app, through the associated [Sponsorship](../sponsorships/sponsoring-transactions), should sponsor user activities - Protect your Lens App from being impersonated by an unauthorized actor (e.g., a spam bot) To help you get started, we have provided an [example implementation](https://github.com/lens-protocol/lens-authorization-app-workflow-example) using Express.js. ## Overview The Lens Authentication flow is at the heart of this mechanism. It allows you to control who can acquire credentials for the Lens API as user of your Lens App, serving as the starting point to manage user activity sponsorship and App Verification. Below is a high-level overview of the Lens Authentication flow working with the Operation Approval mechanism: >API: Login Request API->>Authorization: Authorization Request Authorization-->>API: { "allow": true, "signingKey": "0x…", } API-->>Client: Issues Auth Tokens note over Client,Protocol: App Verification Client->>API: Social Operation (e.g., post) activate API API->>API: Sign Operation API->>Protocol: Send Operation Protocol->>Protocol: Verify Signature Protocol->>Protocol: Execute Operation Protocol-)API: Indexed for downstream access Client->>API: Operation Result API-->>Client: Verified Operation Outcome deactivate API note over Client,Authorization: Credentials Refresh Client->>API: Refresh Tokens API->>Authorization: Authorization Request Authorization-->>API: { "allow": true, "signingKey": "0x…", } API-->>Client: Issues New Auth Tokens `} /> ### Initial Authentication During the initial authentication, the Lens API makes a server-to-server call to a custom Authorization Endpoint that you define. This call containing information about the user’s Lens Account and the address that is signing the log-in request (Account Owner or Account Manager for the given Account). Based on this information, the endpoint determines whether the user is allowed acquire credentials for the Lens API as a user of your Lens App. The endpoint response can also control: - Whether to sponsor the user's activities - The signing key to use for the App Verification process ### App Verification When App Verification is enabled for your Lens App, the Lens API signs each social operation (e.g., post, comment, follow) using the signing key you provided via the Authorization Endpoint. This signature is included in the operation request sent to the Lens Protocol. The Lens Protocol validates the signature and executes the operation only if the signature is valid. This process ensures that only operations authorized by your app are executed, providing a secure and reliable way to associate each operation with your app. ### Credentials Refresh When the user's credentials are about to expire and the client requests a refresh, the Lens API makes a server-to-server call to the Authorization Endpoint to determine if the user is still allowed to act as a user of your Lens App. ## App Authorization By default, any Lens account can log in to your app. To control access, sponsorship, or enable app verification, follow the steps below to implement a custom authorization workflow. ### Authorization Endpoint First, create an Authorization Endpoint as a publicly accessible HTTPS URL. It must accept POST requests with a JSON body and use token authentication via the standard `Authorization` header (Bearer token authentication). Ensure the endpoint responds within 500 ms, as exceeding this limit will result in the user’s authentication request being denied. To ensure reliability, focus on lightweight checks and avoid resource-intensive operations. For more complex validations, consider asynchronously populating a cache with the required data (e.g., through a separate job) to meet the timing constraints. If using serverless infrastructure, address cold start issues to ensure quick responses. **Request** The Lens API will send a POST request to the Authorization Endpoint according to the following format: ```http POST /path/to/endpoint HTTP/1.1 Host: myserver.com Authorization: Bearer Content-Type: application/json { "account": "0x4F10f685B6BF165e86f41CDf4a906B17F295C235", "signedBy": "0x00004747f7a56EE7Af7237220c960a7D06232626" } ``` | **Header** | **Description** | | ---------- | --------------------------------------------------------------------------------------- | | `` | A secret used to authenticate the request. See the **Generate a Secret** section below. | | **Body Parameter** | **Description** | | ------------------ | ------------------------------------------------------------------------ | | `account` | The Lens Account that wants to log-in to the Lens API for your Lens App. | | `signedBy` | The Lens Account owner or an Account Manager for it. | **Response** The Authorization Endpoint must respond with a JSON object according to the following format: Any non-200 response or invalid response will end up in denying the user access to the Lens API for your Lens App. The user is allowed to log in to the Lens API as a user of your Lens App. ```http HTTP/1.1 200 OK Content-Type: application/json { "allowed": true, "sponsored": true } ``` | **Response Property** | **Description** | | --------------------- | --------------------------------------------------------------------------------------------------------------------------- | | `allowed` | `true` - allowed | | `sponsored` | Boolean indication whether the Lens API can use the App Sponsorship to cover transaction fees for this Account-Signer pair. | {/* | `signingKey` | Optional, the App Verification signing key from the first step. | */} The user is not allowed to log in to the Lens API as a user of your Lens App. ```http HTTP/1.1 200 OK Content-Type: application/json { "allowed": false, "reason": "Account is not allowed to access application X" } ``` | **Response Property** | **Description** | | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | `allowed` | `false` - not allowed | | `reason` | A human-readable reason why the user is not allowed to access the Lens API. This will be forwarded to the end-user as part of the response to the client. | If the Authorization header is missing or the bearer token is invalid, respond with a `401 Unauthorized` status. ```http HTTP/1.1 401 Unauthorized Content-Type: application/json { "error": "Invalid or missing authorization token" } ``` ### Generate a Secret Create a secret to be used as a **Bearer token** for authenticating requests to your Authorization Endpoint. The secret must be between **64 and 4096 characters** and use only **type-safe characters**, such as: ```text A–Z a–z 0–9 - _ . ~ + / = ``` Avoid whitespace, control characters, or symbols that require escaping in HTTP headers. A long-lived JWT can also be used as the secret, as long as it meets the character and length requirements. ### Configure App Once you have your Authorization Endpoint ready, you can configure it for your Lens App. {/* You MUST be either the owner or an admin of the App you intend to configure. - Go to the [Apps section in the Lens Developer Dashboard](https://developer.lens.xyz/apps). - Select your app → **Authentication** section (in the right sidebar) → click **Change**. In the dialog that appears: - Select **Custom**. - Enter your **Endpoint URL**. - Provide the **Secret** from the previous step. - Test the connection by clicking **Test Connection**. Click **Update Authentication** to save. */} You MUST be authenticated as [Builder](../authentication) and be either the owner or an admin of the App you intend to configure. Use the `addAppAuthorizationEndpoint` action to configure the Authorization Endpoint for your Lens App. ```ts filename="Add Authorization Endpoint" import { evmAddress, uri } from "@lens-protocol/client"; import { addAppAuthorizationEndpoint } from "@lens-protocol/client/actions"; const result = await addAppAuthorizationEndpoint(sessionClient, { endpoint: uri("https://myserver.com/path/to/endpoint"), app: evmAddress("0xa0182D914845ec1C3EF61a23C50D56370E23d94e"), bearerToken: "", }); if (result.isErr()) { return console.error(result.error); } ``` Use the `removeAppAuthorizationEndpoint` action to remove the Authorization Endpoint configuration for your Lens App. ```ts filename="Remove Authorization Endpoint" import { evmAddress } from "@lens-protocol/client"; import { removeAppAuthorizationEndpoint } from "@lens-protocol/client/actions"; const result = await removeAppAuthorizationEndpoint(sessionClient, { app: evmAddress("0xa0182D914845ec1C3EF61a23C50D56370E23d94e"), }); if (result.isErr()) { return console.error(result.error); } ``` You MUST be authenticated as [Builder](../authentication) and be either the owner or an admin of the App you intend to configure. Use the `addAppAuthorizationEndpoint` mutation to configure the Authorization Endpoint for your Lens App. ```graphql filename="Mutation" mutation { addAppAuthorizationEndpoint( request: { app: "0xa0182D914845ec1C3EF61a23C50D56370E23d94e" endpoint: "https://myserver.com/path/to/endpoint" bearerToken: "" } ) } ``` ```json filename="Response" { "data": { "addAppAuthorizationEndpoint": null } } ``` Use the `removeAppAuthorizationEndpoint` mutation to remove the Authorization Endpoint configuration for your Lens App. ```graphql filename="Mutation" mutation { removeAppAuthorizationEndpoint( request: { app: "0xa0182D914845ec1C3EF61a23C50D56370E23d94e" } ) } ``` ```json filename="Response" { "data": { "removeAppAuthorizationEndpoint": null } } ``` That's it—you now have full control over who can log in into your Lens App and how your sponsorship funds are used. During the initial phase, all Lens transactions are sponsored by the Lens team. ## App Verification With your authorization flow configured, you can now set up App Verification to securely sign operations on behalf of your app so to avoid impersonation by unauthorized actors (e.g., spam bots). ### Generate Signing Key First, generate a new signing key for the address that will be responsible for signing operations. This key will serve as an authorized App Signer for your Lens App's operations. ```shell filename="Foundry (cast)" cast wallet new Successfully created new keypair. Address: 0x8711d4d6B7536D… Private key: 0x72433488d76ffec7a16b… ``` ```ts filename="viem" #!/usr/bin/env tsx import { privateKeyToAccount, generatePrivateKey } from "viem/accounts"; const privateKey = generatePrivateKey(); const account = privateKeyToAccount(privateKey); console.log("Private Key:", account.privateKey); console.log("Address:", account.address); ``` ```ts filename="ethers" #!/usr/bin/env tsx import { Wallet } from "ethers"; const wallet = Wallet.createRandom(); console.log("Private Key:", wallet.privateKey); console.log("Address:", wallet.address); ``` **DO NOT** use an existing private key or reuse the generated key for any other purpose. This key should be exclusively used for signing operations on behalf of your Lens App. ### Issue Signing Key Update the Authorization Endpoint to include the signing key in the response. ```http HTTP/1.1 200 OK Content-Type: application/json { "allowed": true, "sponsored": true, "signingKey": "0x72433488d76ffec7a16b…" } ``` | **Response Property** | **Description** | | --------------------- | --------------------------------------------------------------------------------------------------------------------------- | | `allowed` | `true` - allowed | | `sponsored` | Boolean indication whether the Lens API can use the App Sponsorship to cover transaction fees for this Account-Signer pair. | | `signingKey` | The App Verification signing key from the first step. | ### Configure App Signers Then, add address from the previous step to the list of App Signers associated with your Lens App. {/* You MUST be either the owner or an admin of the App you intend to configure. - Go to the [Apps section in the Lens Developer Dashboard](https://developer.lens.xyz/apps). - Select your app → under the **Authentication** section (in the right sidebar) → click **Enable App Verification**. - Add the signer address to the **App Signers** list. */} Use the `addAppSigners` action to add the approver addresses to the list of App Signers associated with your Lens App. ```ts filename="viem" import { evmAddress } from "@lens-protocol/client"; import { addAppSigners } from "@lens-protocol/client/actions"; import { handleOperationWith } from "@lens-protocol/client/viem"; const result = await addAppSigners(sessionClient, { app: evmAddress("0x75bb5fBdb559Fb2A8e078EC2ee74aad791e37DCc"), signers: [evmAddress("0xe2f2a5C287993345a840db3B0845fbc70f5935a5")], }) .andThen(handleOperationWith(walletClient)) .andThen(sessionClient.waitForTransaction); ``` ```ts filename="ethers" import { evmAddress } from "@lens-protocol/client"; import { addAppSigners } from "@lens-protocol/client/actions"; import { handleOperationWith } from "@lens-protocol/client/ethers"; const result = await addAppSigners(sessionClient, { app: evmAddress("0x75bb5fBdb559Fb2A8e078EC2ee74aad791e37DCc"), signers: [evmAddress("0xe2f2a5C287993345a840db3B0845fbc70f5935a5")], }) .andThen(handleOperationWith(signer)) .andThen(sessionClient.waitForTransaction); ``` Use the `addAppSigners` mutation to remove the Authorization Endpoint configuration for your Lens App. ```graphql filename="Mutation" mutation { addAppSigners( request: { app: "0xa0182D914845ec1C3EF61a23C50D56370E23d94e" signers: ["0xe2f2a5C287993345a840db3B0845fbc70f5935a5"] } ) { ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` And, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. ### Enable App Verification Finally, enable the App Verification for your Lens App. {/* Within the same dialog where you added the App Signers, check the **Enable App Verification** checkbox and save. */} You MUST be authenticated as [Builder](../authentication) and be either the owner or an admin of the App you intend to configure. Use the `setAppVerification` action to enable the App Verification for your Lens App. ```ts filename="viem" import { evmAddress } from "@lens-protocol/client"; import { handleOperationWith } from "@lens-protocol/client/viem"; const result = await setAppVerification(sessionClient, { app: evmAddress("0x75bb5fBdb559Fb2A8e078EC2ee74aad791e37DCc"), enabled: true, }) .andThen(handleOperationWith(walletClient)) .andThen(sessionClient.waitForTransaction); ``` ```ts filename="ethers" import { addAppSigners } from "@lens-protocol/client/actions"; import { handleOperationWith } from "@lens-protocol/client/ethers"; const result = await setAppVerification(sessionClient, { app: evmAddress("0x75bb5fBdb559Fb2A8e078EC2ee74aad791e37DCc"), enabled: true, }) .andThen(handleOperationWith(signer)) .andThen(sessionClient.waitForTransaction); ``` You MUST be authenticated as [Builder](../authentication) and be either the owner or an admin of the App you intend to configure. Use the `setAppVerification` mutation to enable the App Verification for your Lens App. ```graphql filename="Mutation" mutation { setAppVerification( request: { app: "0xa0182D914845ec1C3EF61a23C50D56370E23d94e" enabled: true } ) { ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` And, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. That's it—all operations performed by the Lens API on behalf of your Lens App will now be signed using the signing key you provided. ## Advanced Topics ### Revoking Credentials The Lens Authentication flow allows you to implement a credentials revocation mechanism. This is useful when you want to invalidate a user's session or revoke access to the Lens API for interactions involving your app. To revoke a user's credentials, you should include the relevant Account address in a _denylist_ that is accessible to your Authorization Endpoint. On the subsequent request to refresh the credentials you can then deny access to the Lens API. >API: Refresh Tokens API->>Authorization: Authorization Request note over Authorization: Checks DenyList Authorization-->>API: { "allowed": false, "reason": "Banned" } API-->>App: Denied `} /> ================ File: src/pages/protocol/apps/create.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Create an App This guide will walk you through the process of creating a Lens App. --- To create an App, follow these steps. You MUST be authenticated as [Builder](../authentication) create an App. ## Create App Metadata First, construct an App Metadata object with the necessary content. Use the `@lens-protocol/metadata` package to construct a valid `AppMetadata` object: ```ts filename="Example" import { MetadataAttributeType, app } from "@lens-protocol/metadata"; const metadata = app({ name: "XYZ", tagline: "The next big thing", description: "An app to rule them all", logo: "lens://4f91cab87ab5e4f5066f878b72…", developer: "John Doe ", url: "https://example.com", termsOfService: "https://example.com/terms", privacyPolicy: "https://example.com/privacy", platforms: ["web", "ios", "android"], }); ``` If you opted for manually create Metadata objects, make sure it conform to the [App Metadata JSON Schema](https://json-schemas.lens.dev/app/1.0.0.json). ```json filename="Example" { "$schema": "https://json-schemas.lens.dev/app/1.0.0.json", "lens": { "id": "6418053f-89ae-4f9e-8362-8d1b6c0cdafb", "name": "XYZ", "tagline": "The next big thing", "description": "An app to rule them all", "logo": "lens://4f91cab87ab5e4f5066f878b72…", "url": "https://example.com", "platforms": ["web", "ios", "android"] } } ``` ## Upload App Metadata Next, upload the App Metadata object to a public URI. ```ts import { storageClient } from "./storage-client"; const { uri } = await storageClient.uploadAsJson(metadata); console.log(uri); // e.g., lens://4f91ca… ``` This example uses [Grove storage](../../storage) to host the Metadata object. See the [Lens Metadata Standards](../best-practices/metadata-standards#host-metadata-objects) guide for more information on hosting Metadata objects. ## Deploy App Contract Next, deploy the Lens App smart contract. Use the `createApp` action to deploy the Lens App smart contract. ```ts filename="Lens Globals" import { uri } from "@lens-protocol/client"; import { createApp } from "@lens-protocol/client/actions"; // … const result = await createApp(sessionClient, { metadataUri: uri("lens://4f91…"), // the URI from the previous step defaultFeed: { globalFeed: true, }, graph: { globalGraph: true, }, namespace: { globalNamespace: true, }, }); ``` ```ts filename="App w/ Admins" import { evmAddress, uri } from "@lens-protocol/client"; import { createApp } from "@lens-protocol/client/actions"; // … const result = await createApp(sessionClient, { admins: [evmAddress("0x1234…"), evmAddress("0x5678…")], metadataUri: uri("lens://4f91…"), defaultFeed: { globalFeed: true, }, graph: { globalGraph: true, }, namespace: { globalNamespace: true, }, }); ``` ```ts filename="Custom Feeds" import { evmAddress, uri } from "@lens-protocol/client"; import { createApp } from "@lens-protocol/client/actions"; // … const result = await createApp(sessionClient, { metadataUri: uri("lens://4f91…"), // the URI from the previous step feeds: [ evmAddress("0x1234…"), evmAddress("0x5678…"), evmAddress("0x9abc…"), evmAddress("0xdef0…"), ], defaultFeed: { custom: evmAddress("0x1234…"), }, graph: { none: true, }, namespace: { none: true, }, }); ``` ```ts filename="Custom Graph" import { evmAddress, uri } from "@lens-protocol/client"; import { createApp } from "@lens-protocol/client/actions"; // … const result = await createApp(sessionClient, { metadataUri: uri("lens://4f91…"), // the URI from the previous step defaultFeed: { globalFeed: true, }, graph: { custom: evmAddress("0x1234…"), }, namespace: { globaNamespace: true, }, }); ``` ```ts filename="Custom Namespace" import { evmAddress, uri } from "@lens-protocol/client"; import { createApp } from "@lens-protocol/client/actions"; // … const result = await createApp(sessionClient, { metadataUri: uri("lens://4f91…"), // the URI from the previous step defaultFeed: { globalFeed: true, }, graph: { globalGraph: true, }, namespace: { custom: evmAddress("0x1234…"), }, }); ``` Use the `createApp` mutation to deploy the Lens App smart contract. ```graphql filename="Mutation" mutation { createApp( request: { metadataUri: "lens://4f91cab87ab5e4f5066f878b72…" # Any admins who need to manage this app also # admins: [EvmAddress!] # The app graph defaults to use the global graph # graph: GraphChoiceOneOf! # Any custom feeds # feeds: [EvmAddress!]! = [] # The default feed # defaultFeed: FeedChoiceOneOf! # The app username namespace # namespace: UsernameNamespaceChoiceOneOf! # The app groups, omit if none # groups: [EvmAddress!] # The app signers, omit if none # signers: [EvmAddress!] # The app sponsorship, omit if none # sponsorship: EvmAddress # The app treasury, omit if none # treasury: EvmAddress } ) { ... on CreateAppResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ```json filename="CreateAppResponse" { "data": { "createApp": { "hash": "0x…" } } } ``` ## Handle Result Next, handle the result using the adapter for the library of your choice and wait for it to be indexed. ```ts filename="viem" highlight="1,8,9" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await createApp(sessionClient, { metadataUri: uri("lens://4f91…"), // the URI from the previous step }) .andThen(handleOperationWith(walletClient)) .andThen(sessionClient.waitForTransaction); ``` ```ts filename="ethers" highlight="1,8,9" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await createApp(sessionClient, { metadataUri: uri("lens://4f91…"), // the URI from the previous step }) .andThen(handleOperationWith(signer)) .andThen(sessionClient.waitForTransaction); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Next, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. ## Fetch New App Finally, fetch the newly created App using the `fetchApp` action. ```ts filename="viem" highlight="1,10" import { fetchApp } from "@lens-protocol/client/actions"; // … const result = await createApp(sessionClient, { metadataUri: uri("lens://4f91…"), // the URI from the previous step }) .andThen(handleOperationWith(walletClientOrSigner)) .andThen(sessionClient.waitForTransaction) .andThen((txHash) => fetchApp(sessionClient, { txHash })); if (result.isErr()) { return console.error(result.error); } // app: App | null const app = result.value; ``` Finally, fetch the newly created App using the `app` query. ```graphql filename="Query" query { app(request: { txHash: "0x1234…" }) { address graphAddress sponsorshipAddress defaultFeedAddress namespaceAddress createdAt metadata { description name } owner } } ``` ```json filename="Response" { "data": { "app": { "address": "0x1234…", "graphAddress": "0x1234…", "sponsorshipAddress": "0x1234…", "defaultFeedAddress": "0x1234…", "namespaceAddress": "0x1234…", "createdAt": "2024-12-22T21:14:53+00:00", "metadata": { "description": "An app to rule them all", "name": "XYZ" }, "owner": "0x1234…" } } } ``` That's it—you now can start using your Lens App! ================ File: src/pages/protocol/apps/fetch.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Fetch Apps This guide will show you how to fetch App data in different ways. --- Lens App data has a rich structure that includes the following information: - Addresses of the primitives contracts - App Metadata content - Time of creation - Owner of the App To illustrate how to fetch apps, we will use the following fragments: ```graphql filename="App" fragment App on App { address graphAddress sponsorshipAddress defaultFeedAddress namespaceAddress treasuryAddress verificationEnabled createdAt metadata { ...AppMetadata } owner } ``` ```graphql filename="AppMetadata" fragment AppMetadata on AppMetadata { description developer logo name platforms privacyPolicy termsOfService url } ``` ## Get an App Use the `fetchApp` action to fetch a single App by address or by transaction hash. ```ts filename="By Address" import { evmAddress } from "@lens-protocol/client"; import { fetchApp } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchApp(client, { app: evmAddress("0x1234…"), }); if (result.isErr()) { return console.error(result.error); } const app = result.value; ``` ```ts filename="By Tx Hash" import { txHash } from "@lens-protocol/client"; import { fetchApp } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchApp(client, { txHash: txHash("0x1234…"), }); if (result.isErr()) { return console.error(result.error); } const app = result.value; ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; export const client = PublicClient.create({ environment: mainnet, }); ``` Use the `app` query to fetch a single App by address or by transaction hash. ```graphql filename="Query" query { app( request: { app: "0x1234…" # OR # txHash: "TxHash!" } ) { address graphAddress sponsorshipAddress defaultFeedAddress namespaceAddress treasuryAddress verificationEnabled createdAt metadata { description developer logo name tagline platforms privacyPolicy termsOfService url } owner } } ``` ```json filename="Response" { "data": { "app": { "address": "0x1234…", "graphAddress": null, "sponsorshipAddress": null, "defaultFeedAddress": null, "namespaceAddress": null, "treasuryAddress": null, "verificationEnabled": false, "createdAt": "2024-12-22T21:14:53+00:00", "metadata": null, "owner": "0x1234…" } } } ``` ## List Apps Use the paginated `fetchApps` action to fetch a list of Apps based on the provided filters. ```ts filename="Search By App Name" import { fetchApps } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchApps(client, { filter: { searchQuery: "Lens", }, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="Managed By Address" import { evmAddress } from "@lens-protocol/client"; import { fetchApps } from "@lens-protocol/client/actions"; import { client } from "./client"; const posts = await fetchApps(client, { filter: { managedBy: { address: evmAddress("0x1234…"), }, }, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="Linked To Graph" import { evmAddress } from "@lens-protocol/client"; import { fetchApps } from "@lens-protocol/client/actions"; import { client } from "./client"; const posts = await fetchApps(client, { filter: { linkedToGraph: evmAddress("0x1234…"), }, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="Linked To Feed" import { evmAddress } from "@lens-protocol/client"; import { fetchApps } from "@lens-protocol/client/actions"; import { client } from "./client"; const posts = await fetchApps(client, { filter: { linkedToFeed: evmAddress("0x1234…"), }, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="Linked To Sponsorship" import { evmAddress } from "@lens-protocol/client"; import { fetchApps } from "@lens-protocol/client/actions"; import { client } from "./client"; const posts = await fetchApps(client, { filter: { linkedToSponsorship: evmAddress("0x1234…"), }, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; export const client = PublicClient.create({ environment: mainnet, }); ``` Use the paginated `apps` query to fetch a list of Apps based on the provided filters. ```graphql filename="Query" query { apps( request: { filter: { # optional, search query in app name content searchQuery: String # optional, filter by the account owner # managedBy: { # address: "0x1234…" # } # optional, filter by linked Graphs # linkedToGraph: "0x1234…" # optional, filter by linked Feeds # linkedToFeed: "0x1234…" # optional, filter by linked Sponsorships # linkedToSponsorship: "0x1234…" } # optional, order of the results (default: ALPHABETICAL) orderBy: ALPHABETICAL # other options: LATEST_FIRST, OLDEST_FIRST # optional, number of items per page (default: FIFTY) pageSize: TEN # other option is FIFTY } ) { items { address graphAddress sponsorshipAddress defaultFeedAddress namespaceAddress treasuryAddress verificationEnabled createdAt metadata { description developer logo name tagline platforms privacyPolicy termsOfService url } owner } pageInfo { prev next } } } ``` ```json filename="Response" { "data": { "apps": { "items": [ { "address": "0x1234…", "graphAddress": null, "sponsorshipAddress": null, "defaultFeedAddress": null, "namespaceAddress": null, "treasuryAddress": null, "verificationEnabled": false, "createdAt": "2024-11-22T21:15:44+00:00", "metadata": null, "owner": "0x1234…" }, { "address": "0x1234…", "graphAddress": null, "sponsorshipAddress": null, "defaultFeedAddress": null, "namespaceAddress": null, "treasuryAddress": null, "verificationEnabled": false, "createdAt": "2024-11-22T21:14:53+00:00", "metadata": null, "owner": "0x1234…" } ], "pageInfo": { "prev": null, "next": null } } } } ``` See the [Pagination](../best-practices/pagination) guide for more information on how to handle paginated results. ## List App Users Use the paginated `fetchAppUsers` action to fetch a list of users using an App based on the provided filters. ```ts filename="All Users" import { evmAddress } from "@lens-protocol/client"; import { fetchAppUsers } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchAppUsers(client, { app: evmAddress("0x1234…"), }); if (result.isErr()) { return console.error(result.error); } // items: Array: [{account: Account, lastActiveOn: DateTime, firstLoginOn: DateTime}, …] const { items, pageInfo } = result.value; ``` ```ts filename="Search By Username" import { evmAddress } from "@lens-protocol/client"; import { fetchAppUsers } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchAppUsers(client, { filter:{ searchBy: { localNameQuery: "John", } } app: evmAddress("0x1234…"), }); if (result.isErr()) { return console.error(result.error); } // items: Array: [{account: Account, lastActiveOn: DateTime, firstLoginOn: DateTime}, …] const { items, pageInfo } = result.value; ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; export const client = PublicClient.create({ environment: mainnet, }); ``` Use the paginated `appUsers` query to fetch a list of users using an App based on the provided filters. ```graphql filename="Query" query { appUsers( request: { filter: { # optional, search query in user name content searchBy: { localNameQuery: "John" # optional filter by custom namespace # namespaceQuery: "0x1234…" } } # required, app address app: "0x1234…" # optional, if not provided by default it will be ordered by ACCOUNT_SCORE orderBy: 'ACCOUNT_SCORE' # other options: 'LAST_ACTIVE', 'FIRST_LOGIN' # optional, number of items per page (default: FIFTY) pageSize: TEN # other option is FIFTY } ) { items { account { address username { value } metadata { name picture } } lastActiveOn firstLoginOn } pageInfo { prev next } } } ``` ```json filename="Response" { "data": { "appUsers": { "items": [ { "address": "0x1234…", "username": { "value": "lens/bob" }, "metadata": { "name": "Bob", "picture": "https://example.com/bob.jpg" }, "lastActiveOn": "2024-12-22T21:15:44+00:00", "firstLoginOn": "2024-11-22T21:14:53+00:00" } ], "pageInfo": { "prev": null, "next": null } } } } ``` See the [Pagination](../best-practices/pagination) guide for more information on how to handle paginated results. ## List App Feeds Use the paginated `fetchAppFeeds` action to fetch a list of feeds using an App. ```ts filename="Example" import { evmAddress } from "@lens-protocol/client"; import { fetchAppFeeds } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchAppFeeds(client, { app: evmAddress("0x1234…"), }); if (result.isErr()) { return console.error(result.error); } // items: Array: [{feed: evmAddress, timestamp: DateTime}, …] const { items, pageInfo } = result.value; ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; export const client = PublicClient.create({ environment: mainnet, }); ``` Use the paginated `fetchAppFeeds` query to fetch a list of feeds using an App. ```graphql filename="Query" query { appFeeds( request: { # required, app address app: "0x1234…" # optional, number of items per page (default: FIFTY) pageSize: TEN # other option is FIFTY } ) { items { feed timestamp } pageInfo { prev next } } } ``` ```json filename="Response" { "data": { "appFeeds": { "items": [ { "feed": "0x1234…", "timestamp": "2024-12-22T21:15:44+00:00" } ], "pageInfo": { "prev": null, "next": null } } } } ``` See the [Pagination](../best-practices/pagination) guide for more information on how to handle paginated results. ## List App Groups Use the paginated `fetchAppGroups` action to fetch a list of groups using an App. ```ts filename="Example" import { evmAddress } from "@lens-protocol/client"; import { fetchAppGroups } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchAppGroups(client, { app: evmAddress("0x1234…"), }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; export const client = PublicClient.create({ environment: mainnet, }); ``` Use the paginated `appGroups` query to fetch a list of groups using an App based on the provided filters. ```graphql filename="Query" query { appGroups( request: { # required, app address app: "0x1234…" # optional, number of items per page (default: FIFTY) pageSize: TEN # other option is FIFTY } ) { items { address owner timestamp metadata { id name icon coverPicture description } } pageInfo { prev next } } } ``` ```json filename="Response" { "data": { "appGroups": { "items": [ { "address": "0x1234…", "owner": "0x5678…", "timestamp": "2024-12-22T21:15:44+00:00", "metadata": { "id": "0x1234…", "name": "Group Name", "icon": "https://example.com/icon.jpg", "coverPicture": "https://example.com/cover.jpg", "description": "Group Description" } } ], "pageInfo": { "prev": null, "next": null } } } } ``` See the [Pagination](../best-practices/pagination) guide for more information on how to handle paginated results. ## List App Signers Use the paginated `fetchAppSigners` action to fetch list of signers assigned to an App. ```ts filename="All Signers" import { evmAddress } from "@lens-protocol/client"; import { fetchAppSigners } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchAppSigners(client, { app: evmAddress("0x1234…"), }); if (result.isErr()) { return console.error(result.error); } // items: Array: [{signer: evmAddress, timestamp: DateTime}, …] const { items, pageInfo } = result.value; ``` ```ts filename="Search By Username" import { evmAddress } from "@lens-protocol/client"; import { fetchAppSigners } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchAppSigners(client, { filter:{ searchQuery: "admin", } app: evmAddress("0x1234…"), }); if (result.isErr()) { return console.error(result.error); } // items: Array: [{signer: evmAddress, timestamp: DateTime}, …] const { items, pageInfo } = result.value; ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; export const client = PublicClient.create({ environment: mainnet, }); ``` Use the paginated `appSigners` query to fetch a list of signers assigned to an App. ```graphql filename="Query" query { appSigners( request: { filter: { # optional, search query searchQuery: "John" } # required, app address app: "0x1234…" # optional, if not provided by default it will be ordered by LATEST_FIRST orderBy: 'LATEST_FIRST' # other option: 'OLDEST_FIRST' # optional, number of items per page (default: FIFTY) pageSize: TEN # other option is FIFTY } ) { items { signer timestamp } pageInfo { prev next } } } ``` ```json filename="Response" { "data": { "appSigners": { "items": [ { "signer": "0x1234…", "timestamp": "2024-12-22T21:15:44+00:00" } ], "pageInfo": { "prev": null, "next": null } } } } ``` See the [Pagination](../best-practices/pagination) guide for more information on how to handle paginated results. ================ File: src/pages/protocol/apps/index.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Apps Lens Apps let you manage various aspects of your application on-chain. --- A **Lens App** is a smart contract instance that consolidates various Lens primitives under a single entity. At a high level, a Lens App consists of: - **Metadata**: Information about the app, including its name, description, and icon. - **Admins**: A list of EVM addresses authorized to modify the app’s configuration. - **Graph**: An optional Lens Graph that defines the relationships between accounts within your app. - **Feeds**: An optional list of Lens Feeds for storing users' posts. - **Username**: An optional Lens Username that establishes the username namespace for accounts within your app. - **Groups**: An optional list of Lens Groups for organizing initiatives or projects within your app. - **Sponsorship**: A Lens Sponsorship instance that enables transaction cost sponsorship for end-users. ================ File: src/pages/protocol/best-practices/content-licensing.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; export default ({ children }) => {children}; # Content Licensing Learn how to specify content licensing for Lens posts. --- Content licensing is the process where the owner of creative work grants permission to others to use, share, or reproduce the content under specified conditions. Licenses can be customised to suit particular purposes and may include restrictions on usage, distribution, or modification of the work. In Lens, content licensing can be included directly in the Post Metadata. This means the terms of how the content can be used are embedded within the Post itself, providing blockchain-native licensing information that is linked to the Post and the ownership and transfer of any token minted from it (e.g., Collect NFTs). ## Licensing The Lens [Post Metadata Standard](./metadata-standards) provides the capability to specify the license for a Post's content. Lens Post Metadata supports 36 different license choices, ranging from strict to more permissive licenses that allow commercial remixability. Additionally, it's possible to apply different licenses to each media item within the Post Metadata object. Not all posts on Lens have a license. If a post does not specify a license, the post for copyright purposes is unlicensed. The following examples demonstrate how to specify licensing for various types of content. ```ts filename="Single Song" highlight="14" import { audio, MediaAudioMimeType, MetadataLicenseType, } from "@lens-protocol/metadata"; const metadata = audio({ title: "Great song!", audio: { item: "ar://3jQsSRK9X2w5u2S9rtZkZEW1ceWBrxXQhcW3qS4PmVg", type: MediaAudioMimeType.MP3, artist: "Jane Doe", cover: "ar://bNb2ER7LPE9IvmHKJPiddV3Yi3sZIbRFhPE2fpc7X1U", license: MetadataLicenseType.TBNL_C_ND_NPL_Legal, }, }); ``` ```ts filename="Album with Sample" highlight="15,23,29,35" import { audio, MediaAudioMimeType, MetadataLicenseType, } from "@lens-protocol/metadata"; const metadata = audio({ title: "My first collaboration!", // sample audio: { item: "ar://3jQsSRK9X2w5u2S9rtZkZEW1ceWBrxXQhcW3qS4PmVg", type: MediaAudioMimeType.MP3, cover: "ar://bNb2ER7LPE9IvmHKJPiddV3Yi3sZIbRFhPE2fpc7X1U", license: MetadataLicenseType.CC0, }, attachments: [ { item: "ar://mTc6sV9vPKhMb5ix7Yck4bUjFvAyXY87JmEf2r3zSmc", type: MediaAudioMimeType.MP3, artist: "Ezra Calloway", license: MetadataLicenseType.TBNL_C_ND_NPL_Legal, }, { item: "ar://nTc7sV0vPKhMb5ix7Yck4bUjFvAyXY87JmEf2r3zSmc", type: MediaAudioMimeType.MP3, artist: "Lila Rhodes", license: MetadataLicenseType.TBNL_C_ND_NPL_Legal, }, { item: "ar://oTc8sV1vPKhMb5ix7Yck4bUjFvAyXY87JmEf2r3zSmc", type: MediaAudioMimeType.MP3, artist: "Felix Hart", license: MetadataLicenseType.TBNL_C_ND_NPL_Legal, }, ], }); ``` ```ts filename="Single Image" highlight="13" import { image, MediaImageMimeType, MetadataLicenseType, } from "@lens-protocol/metadata"; const metadata = image({ title: "Touch grass", image: { item: "ar://fTc9sV6vPKhMb5ix7Yck4bUjFvAyXY87JmEf2r3zSmc", type: MediaImageMimeType.PNG, altTag: "Me touching grass", license: MetadataLicenseType.TBNL_C_ND_NPL_Legal, }, }); ``` ```ts filename="Gallery" highlight="13,21,27,33" import { image, MediaImageMimeType, MetadataLicenseType, } from "@lens-protocol/metadata"; const metadata = image({ title: "My last trip to the mountains", image: { item: "ar://fTc9sV6vPKhMb5ix7Yck4bUjFvAyXY87JmEf2r3zSmc", type: MediaImageMimeType.PNG, altTag: "A cover image", license: MetadataLicenseType.CC0, }, attachments: [ { item: "ar://gTc0sV2vPKhMb5ix7Yck4bUjFvAyXY87JmEf2r3zSmc", type: MediaImageMimeType.PNG, altTag: "A mountain", license: MetadataLicenseType.TBNL_C_ND_NPL_Legal, }, { item: "ar://hTc1sV3vPKhMb5ix7Yck4bUjFvAyXY87JmEf2r3zSmc", type: MediaImageMimeType.PNG, altTag: "A river", license: MetadataLicenseType.TBNL_C_ND_NPL_Legal, }, { item: "ar://iTc2sV4vPKhMb5ix7Yck4bUjFvAyXY87JmEf2r3zSmc", type: MediaImageMimeType.PNG, altTag: "A forest", license: MetadataLicenseType.TBNL_C_ND_NPL_Legal, }, ], }); ``` ```ts filename="Video" highlight="15" import { MetadataLicenseType, MediaVideoMimeType, video, } from "@lens-protocol/metadata"; const metadata = video({ title: "Great video!", video: { item: "ar://kTc4sV7vPKhMb5ix7Yck4bUjFvAyXY87JmEf2r3zSmc", type: MediaVideoMimeType.MP4, cover: "ar://lTc5sV8vPKhMb5ix7Yck4bUjFvAyXY87JmEf2r3zSmc", duration: 123, altTag: "The video of my life", license: MetadataLicenseType.TBNL_C_ND_NPL_Legal, }, }); ``` ```ts filename="Video with Screenshots" highlight="16,24,30" import { MetadataLicenseType, MediaImageMimeType, MediaVideoMimeType, video, } from "@lens-protocol/metadata"; const metadata = video({ title: "Great video!", video: { item: "ar://kTc4sV7vPKhMb5ix7Yck4bUjFvAyXY87JmEf2r3zSmc", type: MediaVideoMimeType.MP4, cover: "ar://lTc5sV8vPKhMb5ix7Yck4bUjFvAyXY87JmEf2r3zSmc", duration: 123, altTag: "The video of my life", license: MetadataLicenseType.TBNL_C_ND_NPL_Legal, }, attachments: [ { item: "ar://iTc2sV4vPKhMb5ix7Yck4bUjFvAyXY87JmEf2r3zSmc", type: MediaImageMimeType.PNG, altTag: "The moment I touched grass", license: MetadataLicenseType.CC0, }, { item: "ar://jTc3sV5vPKhMb5ix7Yck4bUjFvAyXY87JmEf2r3zSmc", type: MediaImageMimeType.PNG, altTag: "The moment grass touched me", license: MetadataLicenseType.CC0, }, ] }); ``` ## Supported Licenses The Lens Post Metadata supports both [Creative Commons licenses](#appendix-creative-commons-licenses) and [Token Bound NFT licenses](#appendix-token-bound-nft-licenses), a licensing standard native to NFTs. While the metadata offers a range of licenses to foster innovation and flexibility, the most commonly chosen and comprehensive licenses include: - **CC0**: This license allows anyone to use the content without any restrictions. - **TBNL-C-D-NPL-Legal**: This license grants the NFT owner commercial rights. - **TBNL-NC-D-NPL-Legal**: This license grants the NFT owner personal rights. --- ## Appendix ### Creative Commons Licenses Creative Commons licenses provide a robust set of public licenses, allowing creators to share their work globally. These standardized and [publicly accessible licenses](https://creativecommons.org/licenses/) enable creators to distribute their creative works while maintaining specific rights. They offer a flexible framework for granting permissions for various usage types, including commercial or non-commercial use, creation of derivative works, and sharing with appropriate attribution. The breakdown of Creative Commons licenses is as follows: - **CC0 (Public Domain Dedication):** This license permits both commercial use and the creation of derivative works, without the need for attribution. - **CC BY (Attribution):** This license allows both commercial use and the creation of derivative works, but requires attribution to the original creator. - **CC BY-ND (Attribution-NoDerivs):** This license permits commercial use but does not allow the creation of derivative works. Attribution to the original creator is required. - **CC BY-NC (Attribution-NonCommercial):** This license does not permit commercial use but allows for the creation of derivative works. Attribution to the original creator is required. Use the table below to understand the permissions and requirements of each Creative Commons license. | Identifier | Commercial Use | Derivatives | Attribution | | ---------- | -------------- | ----------- | ----------- | | `CC0` | Yes | Yes | - | | `CC BY` | Yes | Yes | Required | | `CC BY-ND` | Yes | No | Required | | `CC BY-NC` | No | Yes | Required | ### Token Bound NFT Licenses Token Bound NFT Licenses provide a native and modular standard licensing system for NFTs, offering a selection of 32 license options. See the [full license standard](https://eips.ethereum.org/assets/eip-5218/ic3license/ic3license.pdf). These are the key components of the Token Bound NFT Licenses: - **Commercial (C) vs. Non-Commercial (NC):** Specifies whether the license allows for commercial use. Commercial licenses permit making money from merchandise and other uses of the work, whereas Non-Commercial licenses do not. - **Derivatives (D), Derivatives-NFT (DT), Derivatives-NFT-Share-Alike (DTSA), No-Derivatives (ND):** Determines the extent to which derivative works are allowed. Derivatives allow for unrestricted derivative works, Derivatives-NFT requires derivatives to be NFTs, Derivatives-NFT-Share-Alike requires derivative NFTs to carry the same license, and No-Derivatives prohibits derivative works entirely. - **Public-License (PL) vs. No-Public-License (NPL):** A Public-License grants the public broad rights to reproduce the work, whereas a No-Public-License restricts rights to the licensee only. - **Ledger-Authoritative (Ledger) vs. Legal-Authoritative (Legal):** Ledger-Authoritative means blockchain ledger status is always authoritative for ownership and rights under the license, even in cases of theft or mistake. Legal-Authoritative allows for legal intervention to correct ownership in cases of theft or fraud. Use the table below to understand the permissions and requirements of each Token Bound NFT license. | Identifier | Commercial Use | Derivatives | Public License | Authoritative | | ------------------------- | -------------- | ----------- | -------------- | ------------- | | `TBNL-C-D-PL-Legal` | Yes | Yes | Yes | Legal | | `TBNL-C-DT-PL-Legal` | Yes | NFT | Yes | Legal | | `TBNL-C-ND-PL-Legal` | Yes | No | Yes | Legal | | `TBNL-C-D-NPL-Legal` | Yes | Yes | No | Legal | | `TBNL-C-DT-NPL-Legal` | Yes | NFT | No | Legal | | `TBNL-C-DTSA-PL-Legal` | Yes | Share-Alike | Yes | Legal | | `TBNL-C-DTSA-NPL-Legal` | Yes | Share-Alike | No | Legal | | `TBNL-C-ND-NPL-Legal` | Yes | No | No | Legal | | `TBNL-C-D-PL-Ledger` | Yes | Yes | Yes | Ledger | | `TBNL-C-DT-PL-Ledger` | Yes | NFT | Yes | Ledger | | `TBNL-C-ND-PL-Ledger` | Yes | No | Yes | Ledger | | `TBNL-C-D-NPL-Ledger` | Yes | Yes | No | Ledger | | `TBNL-C-DT-NPL-Ledger` | Yes | NFT | No | Ledger | | `TBNL-C-DTSA-PL-Ledger` | Yes | Share-Alike | Yes | Ledger | | `TBNL-C-DTSA-NPL-Ledger` | Yes | Share-Alike | No | Ledger | | `TBNL-C-ND-NPL-Ledger` | Yes | No | No | Ledger | | `TBNL-NC-D-PL-Legal` | No | Yes | Yes | Legal | | `TBNL-NC-DT-PL-Legal` | No | NFT | Yes | Legal | | `TBNL-NC-ND-PL-Legal` | No | No | Yes | Legal | | `TBNL-NC-D-NPL-Legal` | No | Yes | No | Legal | | `TBNL-NC-DT-NPL-Legal` | No | NFT | No | Legal | | `TBNL-NC-DTSA-PL-Legal` | No | Share-Alike | Yes | Legal | | `TBNL-NC-DTSA-NPL-Legal` | No | Share-Alike | No | Legal | | `TBNL-NC-ND-NPL-Legal` | No | No | No | Legal | | `TBNL-NC-D-PL-Ledger` | No | Yes | Yes | Ledger | | `TBNL-NC-DT-PL-Ledger` | No | NFT | Yes | Ledger | | `TBNL-NC-ND-PL-Ledger` | No | No | Yes | Ledger | | `TBNL-NC-D-NPL-Ledger` | No | Yes | No | Ledger | | `TBNL-NC-DT-NPL-Ledger` | No | NFT | No | Ledger | | `TBNL-NC-DTSA-PL-Ledger` | No | Share-Alike | Yes | Ledger | | `TBNL-NC-DTSA-NPL-Ledger` | No | Share-Alike | No | Ledger | | `TBNL-NC-ND-NPL-Ledger` | No | No | No | Ledger | ================ File: src/pages/protocol/best-practices/custom-fragments.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Custom Fragments This guide expands on the how to customize the Lens SDK responses by using custom fragments. --- In the [Getting Started Guide](/protocol/getting-started/typescript), we introduced the concept of custom GraphQL fragments. This guide delves into common use cases and potential pitfalls to consider when defining custom fragments. ## Fragment Definitions A GraphQL [fragment](https://graphql.org/learn/queries/#fragments), a Fragment Definition to be precise, is a reusable piece of a GraphQL query that defines a subset of fields to be retrieved from the server. ```graphql fragment FragmentName on TypeName { field1 field2 field3 } ``` where: - `FragmentName` is the name of the fragment - `TypeName` is the type of the node the fragment represents - `field1`, `field2`, `field3` are the fields to be retrieved Fragments can include other fragments, known as _fragment spreads_. This allows you to build complex queries by combining smaller, reusable components. ```graphql fragment FragmentName on TypeName { field1 field2 field3 { ...OtherFragment } } ``` ## Common Use Cases Use the `graphql` function from the Lens SDK to define custom fragments by passing a query string and an array of imported fragments. ```ts filename="Example" import { UsernameFragment, graphql } from "@lens-protocol/client"; export const AccountFragment = graphql( ` fragment Account on Account { __typename username { ...Username } address } `, [UsernameFragment] ); ``` The `graphql` function is a bespoke instance of the [gql.tada](https://gql-tada.0no.co/) library that provides a more ergonomic way to define GraphQL fragments. If a Lens SDK fragment meets your requirements, it is recommended to reuse it to prevent data duplication. In most cases, Lens SDK fragments are named after the node they represent (e.g., `Account`, `Post`, `Group`, etc.). Any deviations from this naming convention will be explicitly noted in the documentation. It is essential to name your fragment consistently with the existing Lens SDK fragments. This allows the Lens SDK to seamlessly merge your custom fragment with the predefined ones. ### Post Fields The `PostFields` fragment is used to retrieve post fields for the `Post` and the `ReferencedPost` fragments. ```ts filename="fragments/posts.ts" import { graphql, PostMetadataFragment } from "@lens-protocol/client"; export const PostFieldsFragment = graphql( ` fragment PostFields on Post { slug timestamp metadata { ...PostMetadata } } `, [PostMetadataFragment] ); ``` The `ReferencedPost` fragment selects specific fields from a Post, excluding links to other posts. This prevents circular references when retrieving posts. ### Post Metadata The `PostMetadata` fragment represents a union of all standard Post Metadata types. Use it to retrieve metadata for the specific post types you plan to support in your application (e.g., video, image, text-only, audio, story, embed) while excluding unnecessary types. ```ts filename="fragments/posts.ts" import { ArticleMetadataFragment, AudioMetadataFragment, TextOnlyMetadataFragment, ImageMetadataFragment, VideoMetadataFragment, graphql, } from "@lens-protocol/client"; export const PostMetadataFragment = graphql( ` fragment PostMetadata on PostMetadata { __typename ... on ArticleMetadata { ...ArticleMetadata } ... on AudioMetadata { ...AudioMetadata } ... on TextOnlyMetadata { ...TextOnlyMetadata } ... on ImageMetadata { ...ImageMetadata } ... on VideoMetadata { ...VideoMetadata } } `, [ ArticleMetadataFragment, AudioMetadataFragment, TextOnlyMetadataFragment, ImageMetadataFragment, VideoMetadataFragment, ] ); ``` ### Media Images Use custom `MediaImage` fragment to optimize the retrieval of Post images. Use the parametrized `item` field to request images of different sizes and formats. ```ts filename="fragments/images.ts" import { graphql } from "@lens-protocol/client"; export const MediaImageFragment = graphql( ` fragment MediaImage on MediaImage { __typename full: item large: item(request: { preferTransform: { widthBased: { width: 2048 } } }) thumbnail: item( request: { preferTransform: { fixedSize: { height: 128, width: 128 } } } ) altTag license type } ` ); ``` See the [Querying Metadata Media](./metadata-standards#querying-metadata-media) section for more information on this. ### Account Metadata The `AccountMetadata` fragment is used to retrieve metadata for the `Account` type. Use the `picture` and `coverPicture` fields to retrieve profile and cover images. ```ts filename="fragments/accounts.ts" import { graphql, MediaImageFragment } from "@lens-protocol/client"; export const AccountMetadataFragment = graphql( ` fragment AccountMetadata on AccountMetadata { name bio thumbnail: picture( request: { preferTransform: { fixedSize: { height: 128, width: 128 } } } ) picture coverPicture wideBackground: coverPicture( request: { preferTransform: { widthBased: { width: 2048 } } } ) } `, [MediaImageFragment] ); ``` See the [Querying Metadata Media](./metadata-standards#querying-metadata-media) section for more information on this. ## Troubleshooting ### Duplicated Fragments When instantiating the `PublicClient` with fragments, you might encounter the following error: ```text InvariantError: Duplicate fragment detected. A fragment named "" has already been provided, either directly or as part of another fragment document. ``` This means that there are duplicated fragments as part of the fragment documents you provided. Typically, this occurs when you define a fragment that includes a sub-fragment and then include both the parent fragment and the sub-fragment in the list of fragments passed to the `PublicClient` factory function. For example, if you defined `PostFieldsFragment` with a bespoke `PostMetadataFragment` like this: ```ts filename="PostFieldsFragment" const PostFieldsFragment = graphql( ` fragment PostFields on Post { metadata { ...PostMetadata } } `, [PostMetadataFragment] ); ``` ```ts filename="PostMetadataFragment" const PostMetadataFragment = graphql( ` fragment PostMetadata on PostMetadata { __typename ... on TextOnlyMetadata { content } } `, [TextOnlyMetadataFragment] ); ``` Then, omit the `PostMetadataFragment` from the list of fragments passed to the `PublicClient` factory function: ```diff const client = PublicClient.create({ environment: mainnet, - fragments: [PostFieldsFragment, PostMetadataFragment], + fragments: [PostFieldsFragment], }); ``` ================ File: src/pages/protocol/best-practices/erc20-approval.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # ERC20 Approval on Actions When an action needs to spend ERC20 tokens from `Signer`, the Signer must first approve the ERC20 token transfer. If the required approval is missing, the system returns a `SignerErc20ApprovalRequired` error that indicates which token needs approval before the action can proceed. ## Prepare Approval Transaction Approval for a given `Signer`/`Token` pair for an authenticated `Account` can be prepared in two ways: - `Infinite Approval` - approve unlimited amount of tokens (only once per token). - `Exact Approval` - approve a specific amount of tokens to spend. Use the `prepareSignerErc20Approval` action to prepare an approval transaction. ```ts filename="Prepare Infinite Approval" import { evmAddress } from "@lens-protocol/client"; import { prepareSignerErc20Approval } from "@lens-protocol/client/actions"; const result = await prepareSignerErc20Approval(sessionClient, { approval: { infinite: evmAddress("0x1234…"), } }); if (result.isErr()) { return console.error(result.error); } ``` ```ts filename="Prepare Exact Approval" import { evmAddress, bigDecimal } from "@lens-protocol/client"; import { prepareSignerErc20Approval } from "@lens-protocol/client/actions"; const result = await prepareSignerErc20Approval(sessionClient, { approval: { exact: { value: bigDecimal("1"), currency: evmAddress("0x1234…"), } } }); if (result.isErr()) { return console.error(result.error); } ``` Use the `prepareSignerErc20Approval` mutation to prepare an approval transaction. ```graphql mutation { prepareSignerErc20Approval( request: { approval: { infinite: "0x1234…", } # or # approval: { # exact: { # value: '1', # currency: "0x1234…", # } # } } ) { ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` Coming soon ## Handle Result Then, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,9" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await prepareSignerErc20Approval(sessionClient, { approval: { infinite: evmAddress("0x1234…"), }, }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,9" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await prepareSignerErc20Approval(sessionClient, { approval: { infinite: evmAddress("0x1234…"), }, }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Then, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Coming soon ## Retry the operation Once the approval transaction has been successfully completed, re-execute the operation that required ERC20 approval. ================ File: src/pages/protocol/best-practices/error-handling.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Error Handling This guide will show you how to handle errors on Lens. --- Lens tools categorize errors as either **failures** or **exceptions**: - **Failure**: A known issue directly related to business logic prevents the task from completing successfully. These scenarios are anticipated and managed within the application. - **Exception**: An unexpected issue arises, such as incorrect usage, invariant errors, or external/system malfunctions, which are not tied to business logic. The Lens SDK adopts a functional approach to error handling. At its core, it uses a `Result` object as the return value for many of its functions. This object can represent one of two states: - `Ok`: A **successful** result containing a value of type `T`. - `Err`: A **failure** containing an error of type `E`. Any **error thrown** should be considered an **exception**—a malfunction not contemplated by the code. This approach avoids reliance on `try/catch` blocks and promotes predictable, type-safe code by ensuring errors are handled explicitly. Specifically, the SDK uses the [NeverThrow](https://www.npmjs.com/package/neverthrow) library, which is re-exported for convenience. ```ts filename="@lens-protocol/react" import { Result, Ok, Err, ok, err } from "@lens-protocol/react"; ``` ```ts filename="@lens-protocol/client" import { Result, Ok, Err, ok, err } from "@lens-protocol/client"; ``` Let’s explore the concept with a simple example function: ```ts function parseNumber(input: string): Result { return isNaN(Number(input)) ? err("Invalid number") : ok(Number(input)); } ``` Use the convenient `isOk()` or `isErr()` methods to narrow down the result type: ```ts filename="Type Narrowing" const result = parseNumber("42"); if (result.isOk()) { console.log("Number:", result.value); } else { console.error("Error:", result.error); } ``` You can chain multiple operations: ```ts filename="Chaining" function divide(a: number, b: number): Result { return b === 0 ? err("Division by zero") : ok(a / b); } const result = parseNumber("42").andThen((num) => divide(num, 2)); if (result.isOk()) { console.log("Result:", result.value); } else { console.error("Error:", result.error); } ``` You can also provide a default value: ```ts filename="Default Value" const value = parseNumber("invalid").unwrapOr(0); // 0 ``` NeverThrow also provides a `ResultAsync` type for handling asynchronous operations. This is a _thenable object_ that can be awaited, and/or chained with other operations: ```ts filename="Async" const result = await ResultAsync.fromPromise( fetch("https://api.example.com/data") ) .map((response) => response.json()) .mapErr((error) => `Failed to fetch data: ${error}`); if (result.isOk()) { console.log("Data:", result.value); } else { console.error("Error:", result.error); } ``` See the [NeverThrow documentation](https://github.com/supermacro/neverthrow) for more information. The Lens API embraces this distinction by modeling failures as part of the response payload. For instance, when a mutation fails, the response provides a structured outcome that specifies the nature of the failure: ```graphql filename="Mutation" mutation { authenticate(request: { id: "", signature: "" }) { ... on AuthenticationTokens { accessToken refreshToken idToken } ... on WrongSignerError { reason } ... on ExpiredChallengeError { reason } ... on ForbiddenError { reason } } } ``` In contrast, exceptions are represented as [GraphQL Errors](https://spec.graphql.org/October2021/#sec-Errors) and occur in cases where an unexpected issue, such as a validation error, disrupts the query or mutation execution. ```json filename="Error Response" { "errors": [ { "message": "Validation error", "locations": [ { "line": 2, "column": 3 } ], "path": ["authenticate"], "extensions": { "code": "INTERNAL_SERVER_ERROR" } } ], "data": { "authenticate": null } } ``` ================ File: src/pages/protocol/best-practices/mentions.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Mentions This guide will show you how mentions work on Lens. --- Mentions are tracked by the Lens API when included in the `content` field of the [Post](../feeds/post) metadata. ```ts filename="Example" import { textOnly } from "@lens-protocol/metadata"; const metadata = textOnly({ content: `Hey @lens/stani, how are you?`, }); ``` Lens supports two types of mentions: Account and Group mentions. ## Account Mentions In Lens, an Account can have multiple usernames, but only one username per [Username Namespace](../usernames/custom-namespaces). ### Global Lens Namespace A special case is the global Lens Username namespace (i.e., `lens/`). In this case, account mentions take the familiar form: ```text @lens/ ``` where `` is the Lens Account's name under the global Lens Username namespace. For example: ```text Hey @lens/stani, how are you? ``` ### Custom Namespaces The general format for a mention is: ```text @/ ``` where: - ``: The address of the Username namespace contract associated with the account. - ``: The address of the Lens Account being mentioned. For example: ```text Hey @0x123abc456…/0x789def123…, how are you? ``` - `0x123abc456…` is the Username namespace contract address. - `0x789def123…` is the mentioned Lens Account address. ## Group Mentions Group mentions are similar to Account mentions, but with a different format: ```text # ``` where `` is the address of the Group being mentioned. For example: ```text To all #0x123abc456… members, please check the latest update. ``` ## Rendering Mentions Use the `post.mentions` field to replace raw mentions in the Post content with a more user-friendly format. ```ts filename="Example" const content = "Hey @0x123abc456…/0x789def123…, how are you?"; const processed = post.mentions.reduce( (updated, mention) => updated.replace(mention.replace.from, mention.replace.to), content ); // Hey @lens/wagmi, how are you? ``` ```json filename="mentions" [ { "__typename": "AccountMention", "account": "0x789def123…", "replace": { "from": "@0x123abc456…/0x789def123…", "to": "@lens/wagmi" } } ] ``` Below a more detailed example with React. #### Mention Components Define components for group and account mentions. ```tsx filename="mentions.tsx" import React from "react"; export function AccountMention({ children, address }: MentionProps) { return {children}; } export function GroupMention({ children, address }: MentionProps) { return {children}; } type MentionProps = { children: React.ReactNode; account: string; }; ``` #### Render Post Content Replace mentions in the Post content with the corresponding mention component. This example use [markdown-to-jsx](https://markdown-to-jsx.quantizor.dev/) to render the Post content, but you can use any other library or custom renderer. ```tsx filename="PostContent.tsx" import React from "react"; import Markdown from "markdown-to-jsx"; import { AccountMention } from "./AccountMention"; import { GroupMention } from "./GroupMention"; import { PostContentProps } from "./PostContentProps"; export function PostContent({ content, mentions }: PostContentProps) { // Replace mentions in content with custom tags const processed = mentions.reduce((updated, mention) => { switch (mention.__typename) { case "AccountMention": return updated.replace( mention.replace.from, `${mention.replace.to}` ); case "GroupMention": return updated.replace( mention.replace.from, `${mention.replace.to}` ); default: return updated; } }, content); // Render the processed content with Markdown return ( {processed} ); } ``` ```tsx filename="PostContentProps.ts" export type PostContentProps = { content: string; mentions: PostMention[]; }; // Discriminated Union for PostMention generated from GraphQL schema type PostMention = | { __typename: "AccountMention"; account: string; replace: { from: string; to: string; }; } | { __typename: "GroupMention"; group: string; replace: { from: string; to: string; }; }; ``` ## Adding Mentions It's app responsibility to aid users in selecting the correct Account and format mentions correctly in the Post's metadata `content`. ### Account Mentions When the user types `@` followed by a character, the app should open a lookup interface to help the user select the desired Account. ```text Hey @stan ``` #### Account Lookup Use the `accounts` query to search for Accounts that have a Username matching the search term under the specified namespace. ```graphql filename="Global Lens Namespace" query { accounts( request: { filter: { searchBy: { localNameQuery: "stan" } } orderBy: BEST_MATCH } ) { items { address username { value namespace { address } } metadata { name picture } } pageInfo { prev next } } } ``` ```graphql filename="Multiple Namespaces" query { accounts( request: { filter: { searchBy: { localNameQuery: "stan" namespaces: ["0x123abc456…", "0x789def123…"] } } orderBy: BEST_MATCH } ) { items { address username1: username(request: { namespace: "0x123abc456…" }) { value namespace { address } } username2: username(request: { namespace: "0x789def123…" }) { value namespace { address } } metadata { name picture } } pageInfo { prev next } } } ``` Make sure to use the `BEST_MATCH` order for the `orderBy` field to ensure the most relevant results are returned. In case of multiple namespaces, you can alias the `username` field to get an account's username under a specific namespace. It's your app's responsibility to determine which username to display to the user. Coming soon Coming soon #### Display Account Selection Let's say the previous step returned the following accounts: ```text filename="Example" lens/stani lens/stanley lens/stanford ``` ```json filename="Response" { "data": { "accounts": { "items": [ { "address": "0x789def123…", "username": { "value": "lens/stani", "namespace": { "address": "0x123abc456…" } }, "metadata": { "name": "Stani" } }, { "address": "0x456ghi789…", "username": { "value": "lens/stanley", "namespace": { "address": "0x123abc456…" } }, "metadata": { "name": "Stanley" } }, { "address": "0x123abc456…", "username": { "value": "lens/stanford", "namespace": { "address": "0x123abc456…" } }, "metadata": { "name": "Stanford" } } ], "pageInfo": { "prev": null, "next": null } } } } ``` On user's selection, use the provided data to render a user-friendly mention. For example you can use the `username.value` like so: ```text Hey @ ``` #### Populate Mention in Content Finally, when submitting the post, include the mention in the Post's metadata `content` field according to the specification above. For the global Lens Username namespace, the mention format remains unchanged: ```ts filename="Example" import { textOnly } from "@lens-protocol/metadata"; const metadata = textOnly({ content: `Hey @${selected.username.value}, how are you?`, }); ``` For custom Username namespaces, include the namespace and account addresses as specified: ```ts filename="Example" import { textOnly } from "@lens-protocol/metadata"; const metadata = textOnly({ content: `Hey @${selected.username1.namespace.address}/${selected.address}, how are you?`, }); ``` That's it—the Lens indexer will track the mention and notify the mentioned Account. ### Group Mentions When the user types `#` followed by a character, the app should open a lookup interface to help the user select the desired Group. ```text To all #build ``` #### Group Lookup Use the `groups` query to search for Groups that have a name matching the search term. ```graphql filename="Query" query { groups(request: { filter: { searchQuery: "build" } }) { items { address metadata { description icon name } } pageInfo { prev next } } } ``` Coming soon Coming soon #### Display Group Selection Let's say the previous step returned the following groups: ```json filename="Response" { "data": { "groups": { "items": [ { "address": "0x1234…", "metadata": { "description": "A group for builders", "icon": "https://example.com/icon.png", "name": "Builders" } }, { "address": "0x5678…", "metadata": { "description": "A group for building", "icon": "https://example.com/icon.png", "name": "Building" } } ], "pageInfo": { "prev": null, "next": null } } } } ``` On user's selection, use the provided data to render a user-friendly mention. ```text To all # ``` #### Populate Mention in Content Finally, when submitting the post, include the mention in the Post's metadata `content` field according to the specification above. ```ts filename="Example" import { textOnly } from "@lens-protocol/metadata"; const metadata = textOnly({ content: `To all #${selected.address} members, please check the latest update.`, }); ``` That's it—the Lens indexer will track the mention and populate the `post.mentions` field with the necessary data. ================ File: src/pages/protocol/best-practices/metadata-standards.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Metadata Standards This guide explain how Metadata objects are created and managed in Lens. --- Lens Metadata Standards, introduced in [LIP-2](https://github.com/lens-protocol/LIPs/pull/5), are a set of **self-describing object specifications**. These standards ensure that the data includes all the necessary information for validation within itself. ## Create Metadata Object You can construct Metadata objects in two ways: - By utilizing the `@lens-protocol/metadata` package - Manually, with the help of a dedicated JSON Schema Install the `@lens-protocol/metadata` package with its required peer dependencies. ```bash filename="npm" npm install zod @lens-protocol/metadata@latest ``` ```bash filename="yarn" yarn add zod @lens-protocol/metadata@latest ``` ```bash filename="pnpm" pnpm add zod @lens-protocol/metadata@latest ``` Below, we provide few practical examples for creating Metadata objects. Throughout this documentation, we will detail the specific Metadata objects required for various use cases. ```ts filename="Text-only Post Metadata" import { textOnly } from "@lens-protocol/metadata"; const metadata = textOnly({ content: `GM! GM!`, }); ``` ```ts filename="Account Metadata" import { account } from "@lens-protocol/metadata"; const metadata = account({ name: "Jane Doe", bio: "I am a photographer based in New York City.", picture: "lens://4f91cab87ab5e4f5066f878b78…", }); ``` ```ts filename="App Metadata" import { app } from "@lens-protocol/metadata"; const metadata = app({ name: "XYZ", description: "The next big thing", logo: "lens://4f91cab87ab5e4f5066f878b72…", developer: "John Doe ", url: "https://example.com", termsOfService: "https://example.com/terms", privacyPolicy: "https://example.com/privacy", platforms: ["web", "ios", "android"], }); ``` The preferred method for creating Metadata objects is by using the `@lens-protocol/metadata` package. Alternatively, you can manually create a Metadata object, provided it adheres to the corresponding JSON Schema. ```json filename="Text-only Example" { "$schema": "https://json-schemas.lens.dev/posts/text-only/3.0.0.json", "lens": { "id": "6418053f-89ae-4f9e-8362-8d1b6c0cdafb", "content": "GM!", "locale": "en", "mainContentFocus": "TEXT_ONLY" } } ``` The JSON Schema for each Metadata specification is conveniently hosted in the same exact `$shema` URL mentioned in the spec. You can also use the `@lens-protocol/metadata` package to have a local copy of the JSON Schemas. Here's how you can find the Post Metadata JSON Schemas: ```ts filename="Example" import audio from "@lens-protocol/metadata/jsonschemas/posts/audio/3.0.0.json" assert { type: "json" }; import article from "@lens-protocol/metadata/jsonschemas/posts/article/3.0.0.json" assert { type: "json" }; ``` In this case, you don't need to install `zod`. You only need to install the `@lens-protocol/metadata` package. ## Localize Post Metadata You can specify the language of a Post's content using the `locale` field in the metadata. The `locale` values must follow the `-` format, where: - `` is a lowercase [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) language code - `` is an optional uppercase [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) country code You can provide either just the language code, or both the language and country codes. Here are some examples: - `en` represents English in any region - `en-US` represents English as used in the United States - `en-GB` represents English as used in the United Kingdom If not specified, the `locale` field in all `@lens-protocol/metadata` helpers will default to `en`. ```ts filename="Example" highlight="5" import { textOnly } from "@lens-protocol/metadata"; const metadata = textOnly({ content: `Ciao mondo!`, locale: "it", }); ``` While this example uses the `textOnly` helper, the same principle applies to all other metadata types. ```json filename="Text-only Example" { "$schema": "https://json-schemas.lens.dev/posts/text-only/3.0.0.json", "lens": { "id": "6418053f-89ae-4f9e-8362-8d1b6c0cdafb", "content": "GM!", "locale": "en", "mainContentFocus": "TEXT_ONLY" } } ``` ## Host Metadata Objects We recommend using [Grove](../../storage) to host your Metadata objects as a cheap and secure solution. However, developers are free to store Metadata anywhere, such as IPFS, Arweave, or AWS S3, as long as the data is publicly accessible via a URI and served with the `Content-Type: application/json` header. In this documentation, examples will often use an instance of Grove's `StorageClient` to upload Metadata objects. ```ts filename="storage-client.ts" import { StorageClient, testnet } from "@lens-chain/storage-client"; export const storageClient = StorageClient.create(testnet); ``` You can also upload media files to the same hosting solution, then reference their URIs in the Metadata prior to uploading it. ## Query Metadata Media Many metadata fields reference media objects such as images, audio, and video files. The content at those URIs is fetched and snapshotted by the Lens API as part of the indexing process. By default, when you query those fields, the Lens API returns the snapshot URLs. However, you can also request the original URIs. ```graphql filename="MediaImage" highlight="4,5" fragment MediaImage on MediaImage { __typename altTag item # Snapshot URL original: item(request: { useOriginal: true }) license type width height } ``` ```graphql filename="MediaAudio" highlight="4,5,9,10" fragment MediaAudio on MediaAudio { __typename artist cover # Snapshot URL originalCover: cover(request: { useOriginal: true }) credits duration genre item # Snapshot URL originalAudio: item(request: { useOriginal: true }) kind license lyrics recordLabel type } ``` ```graphql filename="MediaVideo" highlight="4,5,7,8" fragment MediaVideo on MediaVideo { __typename altTag cover # Snapshot URL originalCover: cover(request: { useOriginal: true }) duration item # Snapshot URL originalVideo: item(request: { useOriginal: true }) license type } ``` ```graphql filename="AccountMetadata" highlight="5,6,8,9" fragment AccountMetadata on AccountMetadata { name bio picture # Snapshot URL originalPicture: picture(request: { useOriginal: true }) coverPicture # Snapshot URL originalCoverPicture: coverPicture(request: { useOriginal: true }) } ``` ```graphql filename="GroupMetadata" highlight="5,6,8,9" fragment GroupMetadata on GroupMetadata { name description icon # Snapshot URL originalIcon: icon(request: { useOriginal: true }) coverPicture # Snapshot URL originalCoverPicture: coverPicture(request: { useOriginal: true }) } ``` ```graphql filename="AppMetadata" highlight="6,7" fragment AppMetadata on AppMetadata { name # … logo # Snapshot URL originalLogo: logo(request: { useOriginal: true }) } ``` Additionally, when you get snapshot URLs of images, you can request different sizes of the image through an `ImageTransform` object. ```graphql filename="ImageTransform" input ImageTransform @oneOf { fixedSize: FixedSizeTransform widthBased: WidthBasedTransform heightBased: HeightBasedTransform } # Resize image to a fixed size, cropping if necessary input FixedSizeTransform { width: Int! # px height: Int! # px } # Maintain aspect ratio by adjusting height based on width input WidthBasedTransform { width: Int! # px } # Maintain aspect ratio by adjusting width based on height input HeightBasedTransform { height: Int! # px } ``` See the following example: ```graphql filename="MediaImage" fragment MediaImage on MediaImage { # … tall: item(request: { preferTransform: { heightBased: { height: 600 } } }) large: item(request: { preferTransform: { widthBased: { width: 2048 } } }) thumbnail: item( request: { preferTransform: { fixedSize: { height: 128, width: 128 } } } ) } ``` ```graphql filename="AccountMetadata" fragment AccountMetadata on AccountMetadata { # … picture # Snapshot URL thumbnail: picture( request: { preferTransform: { fixedSize: { height: 128, width: 128 } } } ) hero: coverPicture( request: { preferTransform: { widthBased: { width: 2048 } } } ) } ``` ## Refresh Metadata Objects In some cases, you may need to refresh the cached content of a Metadata object in the Lens API. Let's go through an example. Suppose you have a Post object with a Metadata object hosted on [Grove](../../storage) that you want to update without submitting a transaction, as described in the [edit Post guide](../feeds/edit-post). ```ts filename="Post" import { Post } from "@lens-protocol/client"; const post: Post = { id: "42", contentUri: "lens://323c0e1cceb…", metadata: { content: "Good morning!", }, // … }; ``` Assuming you have the [necessary permissions](../../storage/usage/upload#permission-models) to update the content of the Post, you can update the Metadata object hosted on Grove as follows. ```ts filename="Example" import { textOnly } from "@lens-protocol/metadata"; import { acl } from "./acl"; import { storageClient } from "./storage"; import { signer } from "./viem"; const updates = textOnly({ content: `Good morning!`, }); const response = await storageClient.updateJson( post.contentUri newData, signer, { acl } ); ``` ```ts filename="acl.ts" import { chains } from "@lens-chain/sdk/viem"; import { walletOnly } from "@lens-chain/storage-client"; export const acl = walletOnly(signer.address, chains.testnet.id); ``` ```ts filename="storage.ts" import { StorageClient } from "@lens-chain/storage-client"; export const storageClient = StorageClient.create(); ``` ```ts filename="viem.ts" import { privateKeyToAccount } from "viem/accounts"; export const signer = privateKeyToAccount(import.meta.env.PRIVATE_KEY); ``` The process described here works with any hosting solution that allows you to update the content at a given URI. ### Initiate a Metadata Refresh First, use the `refreshMetadata` action to initiate the refresh process. ```ts filename="Refresh Metadata" import { refreshMetadata } from "@lens-protocol/client"; import { client } from "./client"; const result = await refreshMetadata(client, { entity: { post: post.id } }); ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; export const client = PublicClient.create({ environment: mainnet, }); ``` This process is asynchronous and may take a few seconds to complete. ### Wait for the Refresh to Complete Then, if necessary, use the for the Lens API to update the Metadata object. ```ts filename="Refresh Metadata" highlight="1,7" import { waitForMetadata } from "@lens-protocol/client"; // … const result = await refreshMetadata(client, { entity: { post: post.id }, }).andThen(({ id }) => waitForMetadata(client, id)); ``` That's it—any Lens API request involving the given Post will now reflect the updated Metadata object. ================ File: src/pages/protocol/best-practices/pagination.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Paginated Results This guide explains hot to handle paginated results in Lens. --- Lens API uses cursor-based pagination to navigate through lists of results. This is crucial for both performance and user experience, as it allows the API to return only the data required at a given time. ## Paginated Responses With `T` representing the item type being queried, all paginated queries yield a `Paginated` object: ```ts filename="Paginated" type Paginated = { items: readonly T[]; pageInfo: PaginatedResultInfo; }; ``` With `Type` representing the item type being queried, all paginated queries return a `PaginatedResult` object: ```graphql filename="PaginatedTypeResult" type PaginatedTypeResult { items: [Type!]! pageInfo: PaginatedResultInfo! } ``` where `items` contains the current page of results and `pageInfo` contains information about the pagination state: ```ts filename="PaginatedResultInfo" type PaginatedResultInfo = { prev: Cursor | null; next: Cursor | null; }; ``` ```graphql filename="PaginatedResultInfo" type PaginatedResultInfo { prev: Cursor next: Cursor } ``` The `Cursor` type is an _opaque_ scalar that can be used to fetch a different page of results. ## Paginated Requests All paginated queries accepts two optional parameters: `pageSize` and `cursor`. ```ts filename="Request" type Request = { cursor?: Cursor | null; pageSize?: PageSize | null; }; ``` ```ts filename="PageSize" export enum PageSize { Ten = "TEN", Fifty = "FIFTY", } ``` ```graphql filename="Request" input Request { pageSize: PageSize cursor: Cursor } ``` ```graphql filename="PageSize" enum PageSize { TEN FIFTY } ``` where - `pageSize` determines the number of items to return per page. - `cursor` is the value of the `next` or `prev` field from a previous response. The meaning of _prev_ and _next_ depends on the query and the sorting order. For example, when sorting by creation date with most recent first, _prev_ will return newer items and _next_ will return older items. ## Examples Let's use an example to illustrate how to handle paginated results. ### First Page Let's say you want to fetch the paginated posts by a specific author: ```ts filename="posts.ts" import { evmAddress } from "@lens-protocol/client"; import { fetchPosts } from "@lens-protocol/client/actions"; const page1 = await fetchPosts(sessionClient, { filter: { authors: [evmAddress("0x1234…")], }, pageSize: PageSize.FIFTY, }); if (page1.isErr()) { return console.error(page1.error); } const { items, pageInfo } = page1.value; ``` The result value will contain the first page of posts and a `pageInfo.next` cursor: Let's say you want to fetch paginated posts by a specific author: ```graphql filename="Query" query { posts(request: { filter: { authors: ["0x1234…"] }, pageSize: FIFTY }) { items { ... on Post { ...Post } ... on Repost { ...Repost } } pageInfo { prev next } } } ``` The response will contain the first page of posts and a `pageInfo.next` cursor: ```json filename="Page 1" { "data": { "posts": { "items": [ /* ... */ ], "pageInfo": { "prev": null, "next": "SGVsbG8hIQ==" } } } } ``` Coming soon ### Next Pages Use the `pageInfo.next` cursor to fetch the next page of posts: ```ts filename="posts.ts" const page2 = await fetchPosts(sessionClient, { filter: { authors: [evmAddress("0x1234…")], }, pageSize: PageSize.FIFTY, cursor: page1.pageInfo.next, }); ``` Remember to retain the same search criteria and `pageSize` of the initial query when fetching subsequent pages. Use the `pageInfo.next` cursor to fetch the next page of posts: ```graphql filename="Query" query { posts( request: { filter: { authors: ["0x1234…"] } pageSize: FIFTY cursor: "SGVsbG8hIQ==" } ) { items { ... on Post { ...Post } ... on Repost { ...Repost } } pageInfo { prev next } } } ``` The response will contain the second page of posts and updated `pageInfo.next` and `pageInfo.prev` cursors: ```json filename="Page 2" { "data": { "posts": { "items": [ /* ... */ ], "pageInfo": { "prev": "U3RyaW5nIQ==", "next": "V2VsY29tZSE=" } } } } ``` Remember to retain the same search criteria and `pageSize` of the initial query when fetching subsequent pages. Coming soon That's it—you can apply the same approach to any paginated query in Lens. ================ File: src/pages/protocol/best-practices/team-management.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Team Management This guide explains how to manage your team's access to your Lens primitives. --- Lens uses a unified approach to access control for its primitives (apps, graphs, feeds, etc.). There are two types of roles: - **Owner**: The owner has full control over a primitive, including adding and removing admins and transferring ownership. The initial owner is the address that creates the primitive. - **Admin**: An admin can perform most actions except transferring ownership. See the individual primitive documentation for more details. This document identifies as primitives the following Lens entities: - [Apps](../apps) - [Graphs](../graphs/custom-graphs) - [Feeds](../feeds/custom-feeds) - [Groups](../groups/create) - [Username Namespaces](../usernames/custom-namespaces) - [Sponsorships](../sponsorships/sponsoring-transactions) The steps are the same for all primitives, so we will just refer to them their primitive address. ## Add Admins You MUST be authenticated as [Builder](../authentication) to make this request. ### Prepare the Request Use the `addAdmins` action to add Admins to an owned primitive. ```ts filename="Example" import { evmAddress } from "@lens-protocol/client"; import { addAdmins } from "@lens-protocol/client/actions"; const result = await addAdmins(sessionClient, { admins: [evmAddress("0x1234…"), evmAddress("0x5678…")], address: evmAddress("0x90ab…"), // address of the primitive (app/graph/feed/etc) }); ``` Use the `addAdmins` mutation to add Admins to an owned primitive ```graphql filename="Mutation" mutation { addAdmins( request: { admins: ["0x1234…", "0x5678…"] address: "0x90ab…" # address of the primitive (app/graph/feed/etc) } ) { ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ### Handle Result Finally, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await addAdmins(sessionClient, { admins: [evmAddress("0x1234…"), evmAddress("0x5678…")], address: evmAddress("0x3243…"), }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await addAdmins(sessionClient, { admins: [evmAddress("0x1234…"), evmAddress("0x5678…")], address: evmAddress("0x3243…"), }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Finally, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. ## Remove Admins ### Prepare the Request You MUST be authenticated as [Builder](../authentication) to make this request. Use the `removeAdmins` action to remove Admins from an owned primitive. ```ts filename="Example" import { evmAddress } from "@lens-protocol/client"; import { removeAdmins } from "@lens-protocol/client/actions"; const result = await removeAdmins(sessionClient, { admins: [evmAddress("0x1234…"), evmAddress("0x5678…")], address: evmAddress("0x90ab…"), // address of the primitive (app/graph/feed/etc) }); ``` Use the `removeAdmins` mutation to remove Admins from an owned primitive ```graphql filename="Mutation" mutation { removeAdmins( request: { admins: ["0x1234…", "0x5678…"] address: "0x90ab…" # address of the primitive (app/graph/feed/etc) } ) { ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ### Handle Result Finally, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await removeAdmins(sessionClient, { admins: [evmAddress("0x1234…"), evmAddress("0x5678…")], address: evmAddress("0x3243…"), }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await removeAdmins(sessionClient, { admins: [evmAddress("0x1234…"), evmAddress("0x5678…")], address: evmAddress("0x3243…"), }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Finally, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. ## Fetch Admins In some cases, you may need to fetch the list of admins for a primitive. Since a Lens Account, by being a smart wallet, can potentially be a primitive's admin, you can also search admins by their username. Use the paginated `fetchAdminsFor` action to fetch a list of admins for a primitive. ```ts filename="All Admins of a Primitive" import { evmAddress } from "@lens-protocol/client"; import { fetchAdminsFor } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchAdminsFor(client, { address: evmAddress("0x1234…"), }); if (result.isErr()) { return console.error(result.error); } // items: Array: [{ account: Account, addedAt: DateTime }, …] const { items, pageInfo } = result.value; ``` ```ts filename="Search by Username" import { evmAddress } from "@lens-protocol/client"; import { fetchAdminsFor } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchAdminsFor(client, { address: evmAddress("0x1234…"), filter: { searchBy: { localNameQuery: "admin", // namespace: evmAddress("0x5678…"), - Optional in case of a custom namespace }, }, }); if (result.isErr()) { return console.error(result.error); } // items: Array: [{ account: Account, addedAt: DateTime }, …] const { items, pageInfo } = result.value; ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; export const client = PublicClient.create({ environment: mainnet, }); ``` Use the paginated `adminsFor` query to fetch a list of admins for a primitive. ```graphql filename="Query" query { adminsFor( request: { address; "0x1234…" # address of the primitive (app/graph/feed/etc) # Optional filter: { searchBy: { localNameQuery: "bob" # Optional. Defaults to lens/* namespace. # namespace: EvmAddress } } orderBy: LATEST_FIRST # other option: OLDER_FIRST (optional) } ) { items { account { address username { value } metadata { name picture } } addedAt } pageInfo { prev next } } } ``` ```json filename="Response" { "data": { "adminsFor": { "items": [ { "account": { "address": "0x1234…", "username": { "value": "lens/bob" }, "metadata": { "name": "Bob", "picture": "https://example.com/bob.jpg" } }, "addedAt": "2021-09-01T00:00:00Z" } ], "pageInfo": { "prev": null, "next": "U29mdHdhcmU=" } } } } ``` See the [Pagination](../best-practices/pagination) guide for more information on how to handle paginated results. ## Transfer Ownership The owner of a primitive can transfer ownership to another address. ### Prepare the Request You MUST be authenticated as [Builder](../authentication) to make this request. Use the `transferPrimitiveOwnership` action to prepare the transfer of ownership of a primitive. ```ts filename="Example" import { evmAddress } from "@lens-protocol/client"; import { transferPrimitiveOwnership } from "@lens-protocol/client/actions"; const result = await transferPrimitiveOwnership(sessionClient, { address: evmAddress("0x5678…"), newOwner: evmAddress("0x1234…"), }); ``` Use the `transferPrimitiveOwnership` mutation to prepare the transfer of ownership of a primitive. ```graphql filename="Mutation" mutation { transferPrimitiveOwnership( request: { address: "0x90ab…" # address of the primitive (app/graph/feed/etc) newOwner: ["0x1234…", "0x5678…"] } ) { ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ### Handle Result Finally, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await transferPrimitiveOwnership(sessionClient, { address: evmAddress("0x5678…"), newOwner: evmAddress("0x1234…"), }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await transferPrimitiveOwnership(sessionClient, { address: evmAddress("0x5678…"), newOwner: evmAddress("0x1234…"), }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Finally, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. ================ File: src/pages/protocol/best-practices/transaction-lifecycle.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Transaction Lifecycle This guide will help you manage the lifecycle of Lens transactions. --- ## Tiered Transaction Model The Lens API’s approach to write operations prioritizes user convenience through a _tiered transaction model_. This model spans from signless transactions to options requiring user signatures and gas fees, offering the best possible experience based on the individual user's circumstances. There are three classes of operations in this model: - **Social Operations**: These operations can be done by a [Manager](../accounts/manager) on behalf of the Account owner. - **Restricted Operations**: These operations require the Account owner's signature due to their nature. - **Management Operations**: These operations could be funded by the server if the user is eligible. These are used to facilitate management operations such as creating new Apps, creating Custom Graphs, etc. The Lens API adapts its operation results based on user eligibility, ensuring users can proceed with the best available options, from the smoothest experience to necessary fallbacks. ### Social Operations The tiered transaction model for Social Operations is as follows: #### Signless Sponsored Transaction - Best UX - **Description:** Automatically signed and sent by the Lens API, with gas fees sponsored. - **Requirements:** Available when the user enabled the [signless experience](../accounts/manager#signless-experience), the user is eligible for sponsorship, and the operation is deemed _secure_ for signless execution. #### Sponsored Transaction Request - **Description:** Requires the user to sign and send a _gasless_ transaction request, powered by [ZKsync EIP-712](https://docs.zksync.io/zk-stack/concepts/transaction-lifecycle#eip-712-0x71). - **Requirements:** Available if the user is eligible for sponsorship but lacks signless support. #### Self-Funded Transaction Request - Fallback - **Description:** Requires user signature and gas payment, following a standard [EIP-1559](https://docs.zksync.io/zk-stack/concepts/transaction-lifecycle#eip-1559-0x2) transaction request. - **Requirements:** Used when neither signless nor sponsored transactions are available. ### Restricted Operations The tiered transaction model for Restricted Operations is as follows: #### Sponsored Transaction Request - Best UX - **Description:** Requires the user to sign and send a _gasless_ transaction request, powered by [ZKsync EIP-712](https://docs.zksync.io/zk-stack/concepts/transaction-lifecycle#eip-712-0x71). - **Requirements:** Available when the user is eligible for sponsorship. #### Self-Funded Transaction Request - Fallback - **Description:** Requires user signature and gas payment, following a standard [EIP-1559](https://docs.zksync.io/zk-stack/concepts/transaction-lifecycle#eip-1559-0x2) transaction request. - **Requirements:** Used when the user is not eligible for sponsorship. ### Management Operations The tiered transaction model for Management Operations is as follows: #### Signless Sponsored Transaction - Best UX - **Description:** Automatically signed and sent by the Lens API, with gas fees sponsored. - **Requirements:** Available when the user is eligible for sponsorship and the operation is deemed _secure_ for signless execution. #### Sponsored Transaction Request - **Description:** Requires the user to sign and send a _gasless_ transaction request, powered by [ZKsync EIP-712](https://docs.zksync.io/zk-stack/concepts/transaction-lifecycle#eip-712-0x71). - **Requirements:** Available when the user is eligible for sponsorship but the operation requires an explicit signature of the primitive's owner or an admin. #### Self-Funded Transaction Request - Fallback - **Description:** Requires user signature and gas payment, following a standard [EIP-1559](https://docs.zksync.io/zk-stack/concepts/transaction-lifecycle#eip-1559-0x2) transaction request. - **Requirements:** Used when the server deems the user as not eligible for sponsorship. ## Operation Results The tiered transaction model results is modelled as a union type with the possible outcomes for each operation type. In the examples below the terms _Operation Result_ and _Operation Response_ are used as placeholders for the actual operation at hand (e.g., `PostResult` and `PostResponse`, `FollowResult` and `FollowResponse`). ```ts filename="Social Operation" type OperationResult = | OperationResponse | SponsoredTransactionRequest | SelfFundedTransactionRequest | TransactionWillFail; ``` ```ts filename="Restricted Operation" type OperationResult = | SponsoredTransactionRequest | SelfFundedTransactionRequest | TransactionWillFail; ``` ```ts filename="Management Operation" type OperationResult = | OperationResponse | SelfFundedTransactionRequest | TransactionWillFail; ``` ```graphql filename="Social Operation" fragment OperationResult on OperationResult { ... on OperationResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } ``` ```graphql filename="Restricted Operation" fragment OperationResult on OperationResult { ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } ``` ```graphql filename="Management Operation" fragment OperationResult on OperationResult { ... on OperationResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } ``` The union could include additional failure scenarios specific to the operation. Where: - `OperationResponse`: Indicates that the transaction was successfully sent and returns the transaction hash for further [monitoring](#transaction-monitoring). - `SponsoredTransactionRequest`: Requests the user to sign and send the transaction, with gas fees covered. - `SelfFundedTransactionRequest`: Requests the user to sign and send the transaction, with the user covering gas fees. - `TransactionWillFail`: This is an omnipresent entry that, if returned, indicates that the transaction will fail with a specific reason. ```ts filename="SponsoredTransactionRequest" type SponsoredTransactionRequest = { reason: string; sponsoredReason: string; raw: Eip712TransactionRequest; }; type Eip712TransactionRequest = { type: string; to: string; from: string; nonce: number; gasLimit: string; maxPriorityFeePerGas: string; maxFeePerGas: string; data: string; value: string; chainId: number; customData?: { gasPerPubdata?: string; factoryDeps?: string[]; customSignature?: string; paymasterParams?: { paymaster: string; paymasterInput: string; }; }; }; ``` ```ts filename="SelfFundedTransactionRequest" type SelfFundedTransactionRequest = { reason: string; selfFundedReason: string; raw: Eip1559TransactionRequest; }; type Eip1559TransactionRequest = { type: string; to: string; from: string; nonce: number; gasLimit: string; maxPriorityFeePerGas: string; maxFeePerGas: string; data: string; value: string; chainId: number; }; ``` ```graphql filename="SponsoredTransactionRequest" fragment SponsoredTransactionRequest on SponsoredTransactionRequest { reason sponsoredReason raw { ...Eip712TransactionRequest } } fragment Eip712TransactionRequest on Eip712TransactionRequest { type to from nonce gasLimit maxPriorityFeePerGas maxFeePerGas data value chainId customData { gasPerPubdata factoryDeps customSignature paymasterParams { paymaster paymasterInput } } } ``` ```graphql filename="SelfFundedTransactionRequest" fragment SelfFundedTransactionRequest on SelfFundedTransactionRequest { reason selfFundedReason raw { ...Eip1559TransactionRequest } } fragment Eip1559TransactionRequest on Eip1559TransactionRequest { type to from nonce gasLimit maxPriorityFeePerGas maxFeePerGas data value chainId } ``` Both `SponsoredTransactionRequest` and `SelfFundedTransactionRequest` types include: - `reason` - a user-friendly message indicating the reason for the fallback, if any. - an enum reason field - a dev-friendly enum indicating the reason for the fallback, if any: - `sponsoredReason`: `SIGNLESS_DISABLED`, `SIGNLESS_FAILED` - `selfFundedReason`: `NOT_SPONSORED`, `CANNOT_SPONSOR` - `raw` - the transaction request details to be signed and sent by the user. Given a signer from the Ethereum wallet library of your choice: ```ts filename="viem.ts" import "viem/window"; import { chains } from "@lens-chain/sdk/viem"; import { type Address, createWalletClient, custom } from "viem"; // hoist account const [address] = (await window.ethereum!.request({ method: "eth_requestAccounts", })) as [Address]; export const walletClient = createWalletClient({ account: address, chain: chains.testnet, transport: custom(window.ethereum!), }); ``` ```ts filename="ethers.ts" import "@lens-chain/sdk/globals"; import { BrowserProvider, chains } from "@lens-chain/sdk/ethers"; import { Eip1193Provider } from "ethers"; const provider = new BrowserProvider(window.ethereum as Eip1193Provider); const network = await provider.getNetwork(); // { chainId: 37111, name: "Lens Chain Testnet" } export const signer = Signer.from( await provider.getSigner(), Number(network.chainId), ); ``` Use one of the provided adapters to handle the operation results with the signer. ```ts filename="viem" import { uri } from "@lens-protocol/client"; import { post } from "@lens-protocol/client/actions"; import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await post(sessionClient, { contentUri: uri("lens://…"), }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" import { uri } from "@lens-protocol/client"; import { post } from "@lens-protocol/client/actions"; import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await post(sessionClient, { contentUri: uri("lens://…"), }).andThen(handleOperationWith(signer)); ``` Alternatively, use the `raw` field to construct the transaction object for the user's wallet. ```ts filename="viem/zksync" import { sendEip712Transaction, sendTransaction } from "viem/zksync"; import { walletClient } from "./viem"; if (operationResult.__typename === "SponsoredTransactionRequest") { await sendEip712Transaction(walletClient, { data: transaction.raw.data, gas: BigInt(transaction.raw.gasLimit), maxFeePerGas: BigInt(transaction.raw.maxFeePerGas), maxPriorityFeePerGas: BigInt(transaction.raw.maxPriorityFeePerGas), nonce: transaction.raw.nonce, paymaster: transaction.raw.customData.paymasterParams?.paymaster, paymasterInput: transaction.raw.customData.paymasterParams?.paymasterInput, to: transaction.raw.to, value: BigInt(transaction.raw.value), }); } if (operationResult.__typename === "SelfFundedTransactionRequest") { await sendTransaction(walletClient, { data: transaction.raw.data, gas: BigInt(transaction.raw.gasLimit), maxFeePerGas: BigInt(transaction.raw.maxFeePerGas), maxPriorityFeePerGas: BigInt(transaction.raw.maxPriorityFeePerGas), nonce: transaction.raw.nonce, to: transaction.raw.to, type: "eip1559", value: BigInt(transaction.raw.value), }); } ``` ```ts filename="zksync-ethers" import { types } from "zksync-ethers"; if ( result.__typename === "SponsoredTransactionRequest" || result.__typename === "SelfFundedTransactionRequest" ) { const { __typename, from, ...transactionLike } = result.raw as any; const tx: types.TransactionLike = types.Transaction.from(transactionLike); await wallet.sendTransaction(tx); } ``` ## Transaction Monitoring At this point, whether you received an _Operation Response_ or you sent a transaction request via the user's wallet (`SponsoredTransactionRequest` or `SelfFundedTransactionRequest`), you should have a **transaction hash**. Chain the `sessionClient.waitForTransaction` method to monitor the transaction's until it's fully mined and indexed. ```ts filename="viem" highlight="3" const result = await post(sessionClient, { contentUri: uri("lens://…") }) .andThen(handleOperationWith(walletClient)) .andThen(sessionClient.waitForTransaction); if (result.isOk()) { console.log("Transaction indexed:", result.value); // "0x1234…" } else { console.error("Transaction failed:", result.error); } ``` ```ts filename="ethers" // coming soon ``` If you are more familiar with a Promise-based approach, you can use the `waitForTransaction` method directly: ```ts filename="viem" const result = await post(sessionClient, { contentUri: uri("lens://…") }); if (result.isErr()) { return console.error("Transaction failed:", result.error); } switch (result.value.__typename) { case "PostResponse": await sessionClient.waitForTransaction(result.value.hash); break; case "SponsoredTransactionRequest": const hash = await sendEip712Transaction(walletClient, { data: result.value.raw.data, gas: BigInt(result.value.raw.gasLimit), maxFeePerGas: BigInt(result.value.raw.maxFeePerGas), maxPriorityFeePerGas: BigInt(result.value.raw.maxPriorityFeePerGas), nonce: result.value.raw.nonce, paymaster: result.value.raw.customData.paymasterParams?.paymaster, paymasterInput: result.value.raw.customData.paymasterParams?.paymasterInput, to: result.value.raw.to, value: BigInt(result.value.raw.value), }); break; case "SelfFundedTransactionRequest": const hash = await sendTransaction(walletClient, { data: result.value.raw.data, gas: BigInt(result.value.raw.gasLimit), maxFeePerGas: BigInt(result.value.raw.maxFeePerGas), maxPriorityFeePerGas: BigInt(result.value.raw.maxPriorityFeePerGas), nonce: result.value.raw.nonce, to: result.value.raw.to, type: "eip1559", value: BigInt(result.value.raw.value), }); break; default: console.error("Failed to post due to:", result.value.reason); } ``` ```ts filename="ethers" // coming soon ``` You can poll the `transactionStatus` query with the transaction hash to monitor the transaction's lifecycle. It's recommended to poll this query no more frequently than once per second. ```graphql filename="Query" query { transactionStatus(request: { txHash: "0x1234…" }) { ... on NotIndexedYetStatus { reason txHasMined } ... on PendingTransactionStatus { blockTimestamp } ... on FinishedTransactionStatus { blockTimestamp } ... on FailedTransactionStatus { reason blockTimestamp } } } ``` ```json filename="NotIndexedYetStatus" { "data": { "transactionStatus": { "reason": "Transaction not indexed yet, keep trying", "txHasMined": true } } } ``` ```json filename="PendingTransactionStatus" { "data": { "transactionStatus": { "blockTimestamp": 1630000000 } } } ``` ```json filename="FinishedTransactionStatus" { "data": { "transactionStatus": { "blockTimestamp": 1630000000 } } } ``` ```json filename="FailedTransactionStatus" { "data": { "transactionStatus": { "reason": "Transaction failed due to …", "blockTimestamp": 1630000000 } } } ``` where: - `NotIndexedYetStatus`: The transaction has not been indexed yet. This could be due to delays in broadcasting to all nodes, not being mined, or events not being picked up by the Lens Indexer. - `PendingTransactionStatus`: The transaction is being processed by the Lens Indexer. This status can take longer for transactions involving metadata URIs, which need to be fetched and parsed. - `FinishedTransactionStatus`: The transaction has been successfully mined and indexed. You can go ahead with the desired user experience. - `FailedTransactionStatus`: The transaction has failed, with the response providing a reason for the failure. Coming soon ================ File: src/pages/protocol/bigquery/costs.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Understanding Query Costs BigQuery operates on a pay-as-you-go pricing model. Here's what you need to know: --- ## How Costs Are Calculated 1. **Query Costs**: - Charged based on data processed, not data returned - Only scanned columns count towards processing - For current pricing, check the [official BigQuery pricing page](https://cloud.google.com/bigquery/pricing) 2. **Cost Estimation**: - Estimated data to be processed - Estimated cost - Whether it fits in your free tier ## Cost-Saving Best Practices 1. **Write Efficient Queries**: ```sql -- ❌ Expensive: Scans all columns SELECT * FROM `table_name` -- ✅ Better: Select only needed columns SELECT specific_column1, specific_column2 FROM `table_name` ``` 2. **Use LIMIT While Testing**: ```sql -- Always add LIMIT when testing new queries SELECT column1, column2 FROM `table_name` LIMIT 100 ``` 3. **Leverage Table Design**: - Use partitioned tables to filter by date ranges - Use clustered columns for frequently filtered fields - Keep frequently joined columns in the same table when possible 4. **Monitor Usage**: - Set up billing alerts in Google Cloud Console - Review "Query History" for cost patterns - Set project-level quotas to prevent overages **Pro Tip**: Always use the "Query Validation" button before running large queries to check processing costs and avoid unexpected charges. ================ File: src/pages/protocol/bigquery/examples.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # BigQuery Usage Examples Learn how to interact with Lens Chain datasets using different programming languages and tools. --- ## Python Using the official Google Cloud client library: ```python filename="Mainnet" from google.cloud import bigquery # Initialize the client client = bigquery.Client() # Query recent posts app, account, post_types query = """ SELECT app, `lens-protocol-mainnet.post.FORMAT_HEX`(account) as account, post_types, timestamp FROM `lens-protocol-mainnet.post.record` post ORDER BY timestamp DESC LIMIT 5 """ # Execute the query query_job = client.query(query) # Process results for row in query_job: print(f"App: {row.app}, {row.post_types} Posted by: {row.account}, Posted at: {row.timestamp}") ``` ```python filename="Testnet" from google.cloud import bigquery # Initialize the client client = bigquery.Client() # Query recent posts app, account, post_types query = """ SELECT app, `lens-protocol-testnet.post.FORMAT_HEX`(account) as account, post_types, timestamp FROM `lens-protocol-testnet.post.record` post ORDER BY timestamp DESC LIMIT 5 """ # Execute the query query_job = client.query(query) # Process results for row in query_job: print(f"App: {row.app}, {row.post_types} Posted by: {row.account}, Posted at: {row.timestamp}") ``` ## Node.js Using the `@google-cloud/bigquery` package: ```javascript filename="Mainnet" const {BigQuery} = require('@google-cloud/bigquery'); async function queryLens() { const bigquery = new BigQuery(); const query = ` SELECT app, `lens-protocol-mainnet.post.FORMAT_HEX`(account) as account, post_types, ` timestamp FROM `lens-protocol-mainnet.post.record` post ORDER BY timestamp DESC LIMIT 5 `; const [rows] = await bigquery.query(query); console.log('Latest 5 post types by accounts:', rows); } ``` ```javascript filename="Testnet" const {BigQuery} = require('@google-cloud/bigquery'); async function queryLens() { const bigquery = new BigQuery(); const query = ` SELECT app, `lens-protocol-testnet.post.FORMAT_HEX`(account) as account, post_types, ` timestamp FROM `lens-protocol-testnet.post.record` post ORDER BY timestamp DESC LIMIT 5 `; const [rows] = await bigquery.query(query); console.log('Latest 5 post types by accounts:', rows); } ``` ## REST API Using the BigQuery REST API: ```bash filename="Mainnet" curl -X POST \ -H "Authorization: Bearer $(gcloud auth print-access-token)" \ -H "Content-Type: application/json" \ https://bigquery.googleapis.com/bigquery/v2/projects/lens-public-data/queries \ -d '{ "query": "SELECT `lens-protocol-mainnet.account.FORMAT_HEX`(account) as account_address, total_posts FROM `lens-protocol-mainnet.account`.post_summary ORDER BT total_posts DESC LIMIT 10"" }' ``` ```bash filename="Testnet" curl -X POST \ -H "Authorization: Bearer $(gcloud auth print-access-token)" \ -H "Content-Type: application/json" \ https://bigquery.googleapis.com/bigquery/v2/projects/lens-public-data/queries \ -d '{ "query": "SELECT `lens-protocol-testnet.account.FORMAT_HEX`(account) as account_address, total_posts FROM `lens-protocol-testnet.account`.post_summary ORDER BT total_posts DESC LIMIT 10"" }' ``` Remember to handle authentication appropriately in your applications. For local development, you can use the [Google Cloud CLI](https://cloud.google.com/sdk/docs/install). ================ File: src/pages/protocol/bigquery/introduction.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Lens Protocol BigQuery Lens Protocol on BigQuery --- ## Overview Lens social graph is published to a public BigQuery dataset, allowing anyone to query data in bulk without building complex infrastructure or indexers. BigQuery provides a solution for bulk data retrieval essential for machine learning training, analytics, and data profiling. All data in Bigquery datasets is public information from Lens Chain. Datasets are update**d eve**ry 15 minutes to reflect the latest network activity. If you need real-time data, please use the Lens GraphQL API, accessing snapshot data from the PostgreSQL database by [indexers](https://www.alchemy.com/overviews/blockchain-indexer). With the Lens Protocol BigQuery dataset, you can build various applications, such as: - **Analytics and Dashboards**: Create analytics dashboards to visualize user engagement, content performance, and community growth metrics. - **Machine Learning**: Extract large data volumes for training ML models to discover content trends, predict user engagement, or recommend content. - **Custom Feeds**: Develop personalized content feeds with your own algorithms using post data, reactions, and user interactions. ## Accessing the Data To query the Lens Protocol public datasets from BigQuery, use these fully-qualified table names: - **Mainnet**: `lens-protocol-mainnet.INSERT_YOUR_TABLE_NAME_HERE` - **Testnet**: `lens-protocol-testnet.INSERT_YOUR_TABLE_NAME_HERE` You MUST query this from the US region. If you try to use the EU region it will not be able to find it. To access 10 posts on mainnet/testnet: ```sql filename="Mainnet" SELECT * FROM lens-protocol-mainnet.post.record LIMIT 10 ``` ```sql filename="Testnet" SELECT * FROM lens-protocol-testnet.post.record LIMIT 10 ``` ## Data Format Some of the data in the dataset is stored in its raw binary format, e.g. address. When syncing with BigQuery, we transform this byte data into hexadecimal strings with the format `\x{string}`. Lens provide a public function called `FORMAT_HEX` that converts BigQuery's `\x` format to the standard Web3 `0x` format. ```sql filename="Mainnet" SELECT account as original_account_address, `lens-protocol-mainnet.app.FORMAT_HEX`(account) as web3_formatted_account_address FROM `lens-protocol-mainnet.app.user` LIMIT 5; ``` ```sql filename="Testnet" SELECT account as original_account_address, `lens-protocol-testnet.app.FORMAT_HEX`(account) as web3_formatted_account_address FROM `lens-protocol-testnet.app.user` LIMIT 5; ``` ================ File: src/pages/protocol/bigquery/schemas.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Lens Protocol BigQuery Schemas Tables Schema of Lens Chain BigQuery. --- [BigQuery Clustered tables](https://cloud.google.com/bigquery/docs/clustered-tables) can improve query performance and reduce query costs on clustered columns. Clustered columns is similar to index in traditional PostgresSQL. Please refer to [Clustered tables](#clustered-tables) for available clustered tables. ## Account Schema The Account Schema manages user account information and relationships. It tracks user profiles, followers, actions, metadata, and interactions between users. This schema is central to user identity and social connections in the platform. Hex Adresss, i.e. account, post, in the dataset is stored in its raw binary format (bytea). Lens provide a public function called `FORMAT_HEX` that converts BigQuery's `\x` format to the standard Web3 `0x` format. ```sql filename="Mainnet" SELECT account as original_account_address, `lens-protocol-mainnet.account.FORMAT_HEX`(account) as web3_formatted_account_address FROM `lens-protocol-mainnet.account.post_summary` LIMIT 5; ``` ```sql filename="Testnet" SELECT account as original_account_address, `lens-protocol-testnet.account.FORMAT_HEX`(account) as web3_formatted_account_address FROM `lens-protocol-testnet.account.post_summary` LIMIT 5; ``` ### Account Schema Tables - [account.acted](#account-schema-accountacted) - Records actions performed by accounts on posts - [account.action_config](#account-schema-accountaction-config) - Stores action configurations for accounts - [account.action_executed](#account-schema-accountaction-executed) - Logs executed actions by accounts - [account.action_executed_by_account_count](#account-schema-accountaction-executed-by-account-count) - Counts actions executed by specific accounts - [account.action_executed_count](#account-schema-accountaction-executed-count) - Counts actions executed on specific accounts - [account.action_metadata](#account-schema-accountaction-metadata) - Stores metadata for account actions - [account.blocked](#account-schema-accountblocked) - Records blocked accounts - [account.follow_rule_config](#account-schema-accountfollow-rule-config) - Configurations for follow rules - [account.follow_rule_selector](#account-schema-accountfollow-rule-selector) - Selectors for follow rules - [account.follower](#account-schema-accountfollower) - Records follower relationships - [account.follower_summary](#account-schema-accountfollower-summary) - Summarizes follower statistics - [account.known_smart_wallet](#account-schema-accountknown-smart-wallet) - Records known smart wallets - [account.manager](#account-schema-accountmanager) - Records account managers - [account.metadata](#account-schema-accountmetadata) - Stores account metadata - [account.notification](#account-schema-accountnotification) - Stores account notifications - [account.peer_to_peer_recommendation](#account-schema-accountpeer-to-peer-recommendation) - Records peer-to-peer account recommendations - [account.post_summary](#account-schema-accountpost-summary) - Summarizes post statistics for accounts - [account.reacted_summary](#account-schema-accountreacted-summary) - Summarizes reactions by account - [account.reaction_summary](#account-schema-accountreaction-summary) - Summarizes reactions on account content - [account.universal_action_config](#account-schema-accountuniversal-action-config) - Stores universal action configurations - [account.username_assigned](#account-schema-accountusername-assigned) - Records username assignments ### account.acted Records actions performed by accounts on posts. *Previous was publication.open_action_module_acted_record in Lens V2* ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.account.acted` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.account.acted` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | id | integer | Unique identifier | | account | bytea | Account address that performed the action | | post | bytea | Post identifier | | action_id | integer | Action identifier | | implementation | bytea | Implementation address | | action_data | text | Action data | | is_collect | boolean | Whether action is a collect | | tx_hash | bytea | Transaction hash | | block_hash | bytea | Hash of the block containing this record | | timestamp | timestamp with time zone | Action time | | sequence_id | numeric | Sequence identifier | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Account Schema](#account-schema) ### account.action_config Stores action configurations for accounts. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.account.action_config` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.account.action_config` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | id | integer | Unique identifier | | action_address | bytea | Action address | | type | USER-DEFINED | Action type | | account | bytea | Account address | | raw_config_params | jsonb | Raw configuration parameters | | decoded_config_params | jsonb | Decoded configuration parameters | | disabled | boolean | Whether action is disabled | | timestamp | timestamp with time zone | Configuration time | | sequence_id | numeric | Sequence identifier | | disable_extra_data | bytea | Disable extra data | | enable_extra_data | bytea | Enable extra data | | last_updated_sequence_id | numeric | Last update sequence ID | | return_data | bytea | Return data | | app | bytea | App identifier | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Account Schema](#account-schema) ### account.action_executed Logs executed actions by accounts. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.account.action_executed` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.account.action_executed` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | id | integer | Unique identifier | | action_address | bytea | Action address | | type | USER-DEFINED | Action type | | on_account | bytea | Account the action was performed on | | raw_params | jsonb | Raw parameters | | decoded_params | jsonb | Decoded parameters | | timestamp | timestamp with time zone | Execution time | | sequence_id | numeric | Sequence identifier | | app | bytea | App identifier | | by_account | bytea | Account that executed the action | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Account Schema](#account-schema) ### account.action_executed_by_account_count Counts actions executed by specific accounts. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.account.action_executed_by_account_count` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.account.action_executed_by_account_count` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | action_address | bytea | Action address | | by_account | bytea | Account that executed the action | | total | integer | Total count of executions | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Account Schema](#account-schema) ### account.action_executed_count Counts actions executed on specific accounts. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.account.action_executed_count` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.account.action_executed_count` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | action_address | bytea | Action address | | account | bytea | Account address | | total | integer | Total count of executions | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Account Schema](#account-schema) ### account.action_metadata Stores metadata for account actions. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.account.action_metadata` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.account.action_metadata` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | action | bytea | Action address | | metadata_uri | character varying | URI for the metadata | | metadata_snapshot_location_url | character varying | URL for metadata snapshot | | metadata | jsonb | Stored metadata | | name | character varying | Action name | | description | character varying | Action description | | metadata_version | character | Metadata version | | created_on | timestamp with time zone | Creation time | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Account Schema](#account-schema) ### account.blocked Records blocked accounts. *Previous was profile.blocked in Lens V2* ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.account.blocked` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.account.blocked` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | account | bytea | Blocked account address | | blocking_account | bytea | Account doing the blocking | | block_hash | bytea | Hash of the block containing this record | | tx_hash | bytea | Transaction hash | | timestamp | timestamp with time zone | Block time | | sequence_id | numeric | Sequence identifier | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Account Schema](#account-schema) ### account.follow_rule_config Configurations for follow rules. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.account.follow_rule_config` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.account.follow_rule_config` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | id | integer | Unique identifier | | rule_address | bytea | Rule address | | config_salt | bytea | Configuration salt | | type | USER-DEFINED | Rule type | | graph | bytea | Graph identifier | | account | bytea | Account address | | raw_config_params | jsonb | Raw configuration parameters | | decoded_config_params | jsonb | Decoded configuration parameters | | timestamp | timestamp with time zone | Configuration time | | sequence_id | numeric | Sequence identifier | | last_updated_sequence_id | numeric | Last update sequence ID | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Account Schema](#account-schema) ### account.follow_rule_selector Selectors for follow rules. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.account.follow_rule_selector` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.account.follow_rule_selector` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | id | integer | Unique identifier | | config_salt | bytea | Configuration salt | | selector | USER-DEFINED | Rule selector | | is_required | boolean | Whether the rule is required | | timestamp | timestamp with time zone | Configuration time | | sequence_id | numeric | Sequence identifier | | graph | bytea | Graph identifier | | account | bytea | Account address | | rule_address | bytea | Rule address | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Account Schema](#account-schema) ### account.follower Records follower relationships. *Previous was profile.follower in Lens V2* ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.account.follower` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.account.follower` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | id | integer | Unique identifier | | account_following | bytea | Account being followed | | account_follower | bytea | Account doing the following | | graph | bytea | Graph identifier | | follow_id | character varying | Follow identifier | | tx_hash | bytea | Transaction hash | | block_hash | bytea | Hash of the block containing this record | | timestamp | timestamp with time zone | Follow time | | sequence_id | numeric | Sequence identifier | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Account Schema](#account-schema) ### account.follower_summary Summarizes follower statistics. *Previous was global_stats.profile_follower in Lens V2* Corresponding clustered table [account.follower_summary_clustered_by_account](#account-schema-accountfollower-summary-cluster-accountfollower-summary-clustered-by-account) is available for better performance on `account` column. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.account.follower_summary` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.account.follower_summary` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | account | bytea | Account address | | total_followers | integer | Total number of followers | | total_following | integer | Total number of accounts followed | | graph | bytea | Graph identifier | | updated_at | timestamp with time zone | Last update time | #### Cluster account.follower_summary_clustered_by_account Clustered table for account.follower_summary on `account` column. [BigQuery Clustered tables](https://cloud.google.com/bigquery/docs/clustered-tables) can improve query performance and reduce query costs on clustered columns. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.account.follower_summary_clustered_by_account` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.account.follower_summary_clustered_by_account` LIMIT 1; ``` [Back to Top](#lens-protocol-bigquery-schemas) [Back to Account Schema](#account-schema) ### account.known_smart_wallet Records known smart wallets. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.account.known_smart_wallet` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.account.known_smart_wallet` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | address | bytea | Wallet address | | owned_by | bytea | Owner address | | legacy_profile_id | character varying | Legacy profile ID | | block_hash | bytea | Hash of the block containing this record | | tx_hash | bytea | Transaction hash | | timestamp | timestamp with time zone | Record time | | sequence_id | numeric | Sequence identifier | | last_updated_sequence_id | numeric | Last update sequence ID | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Account Schema](#account-schema) ### account.manager Records account managers. *Previous was profile.manager in Lens V2* ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.account.manager` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.account.manager` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | id | integer | Unique identifier | | account | bytea | Account address | | manager | bytea | Manager address | | is_hidden | boolean | Whether manager is hidden | | can_execute_transactions | boolean | Transaction execution permission | | can_transfer_tokens | boolean | Token transfer permission | | can_transfer_native | boolean | Native transfer permission | | can_set_metadata_uri | boolean | Metadata URI setting permission | | block_hash | bytea | Hash of the block containing this record | | tx_hash | bytea | Transaction hash | | timestamp | timestamp with time zone | Record time | | sequence_id | numeric | Sequence identifier | | last_updated_sequence_id | numeric | Last update sequence ID | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Account Schema](#account-schema) ### account.metadata Stores account metadata. *Previous was profile.metadata in Lens V2* ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.account.metadata` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.account.metadata` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | account | bytea | Account address | | metadata_uri | character varying | URI for the metadata | | metadata_snapshot_location_url | character varying | URL for metadata snapshot | | metadata | jsonb | Stored metadata | | name | character varying | Account name | | metadata_version | character | Metadata version | | created_on | timestamp with time zone | Creation time | | app | bytea | App identifier | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Account Schema](#account-schema) ### account.notification Stores account notifications. *Previous was notification.record in Lens V2* ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.account.notification` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.account.notification` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | notification_id | integer | Unique identifier | | generated_notification_id | character varying | Generated notification ID | | type | character varying | Notification type | | post | bytea | Post identifier | | post_pointer | bytea | Post pointer | | receiving_account | bytea | Account receiving the notification | | sender_account | bytea | Account sending the notification | | action_date | timestamp with time zone | Action date | | reaction | USER-DEFINED | Reaction type | | open_action_acted | bytea | Open action identifier | | is_collect | boolean | Whether action is a collect | | app | bytea | App identifier | | graph | bytea | Graph identifier | | feed | bytea | Feed identifier | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Account Schema](#account-schema) ### account.peer_to_peer_recommendation Records peer-to-peer account recommendations. *Previous was profile.peer_to_peer_recommendation in Lens V2* ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.account.peer_to_peer_recommendation` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.account.peer_to_peer_recommendation` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | account | bytea | Account address | | account_recommendation | bytea | Recommended account address | | created_at | timestamp with time zone | Recommendation time | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Account Schema](#account-schema) ### account.post_summary Summarizes post statistics for accounts. *Previous was global_stats.profile in Lens V2* Corresponding clustered table [account.post_summary_clustered_by_account](#account-schema-accountpost-summary-cluster-accountpost-summary-clustered-by-account) is available for better performance on `account` column. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.account.post_summary` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.account.post_summary` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | account | bytea | Account address | | feed | bytea | Feed identifier | | total_posts | integer | Total number of posts | | total_comments | integer | Total number of comments | | total_reposts | integer | Total number of reposts | | total_quotes | integer | Total number of quotes | | total_reacted | integer | Total times account reacted | | total_reactions | integer | Total reactions received | | total_collects | integer | Total collects | | updated_at | timestamp with time zone | Last update time | | total_main | integer | Total main posts | | total_tips | integer | Total tips | Repost/Mirros are not migrated from Lens V2. Total_reposts may not represent the actual numbers from Lens V2 #### Cluster account.post_summary_clustered_by_account Clustered table for account.post_summary on `account` column. [BigQuery Clustered tables](https://cloud.google.com/bigquery/docs/clustered-tables) can improve query performance and reduce query costs on clustered columns. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.account.post_summary_clustered_by_account` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.account.post_summary_clustered_by_account` LIMIT 1; ``` [Back to Top](#lens-protocol-bigquery-schemas) [Back to Account Schema](#account-schema) ### account.reacted_summary Summarizes reactions by account. *Previous was global_stats.profile_reacted in Lens V2* ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.account.reacted_summary` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.account.reacted_summary` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | account | bytea | Account address | | reaction_type | USER-DEFINED | Type of reaction | | total | integer | Total count | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Account Schema](#account-schema) ### account.reaction_summary Summarizes reactions on account content. *Previous was global_stats.profile_reaction in Lens V2* ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.account.reaction_summary` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.account.reaction_summary` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | account | bytea | Account address | | reaction_type | USER-DEFINED | Type of reaction | | total | integer | Total count | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Account Schema](#account-schema) ### account.universal_action_config Stores universal action configurations. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.account.universal_action_config` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.account.universal_action_config` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | id | integer | Unique identifier | | action_address | bytea | Action address | | type | USER-DEFINED | Action type | | raw_config_params | jsonb | Raw configuration parameters | | decoded_config_params | jsonb | Decoded configuration parameters | | timestamp | timestamp with time zone | Configuration time | | sequence_id | numeric | Sequence identifier | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Account Schema](#account-schema) ### account.username_assigned Records username assignments. *Previous was namespace.handle_link in Lens V2* ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.account.username_assigned` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.account.username_assigned` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | id | integer | Unique identifier | | account | bytea | Account address | | namespace | bytea | Namespace identifier | | local_name | character varying | Local username | | block_hash | bytea | Hash of the block containing this record | | tx_hash | bytea | Transaction hash | | timestamp | timestamp with time zone | Assignment time | | sequence_id | numeric | Sequence identifier | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Account Schema](#account-schema) ## App Schema The App Schema manages application-related data, including app records, feeds, groups, and metadata. It tracks app-specific statistics, signers, users, and their interactions with the platform. This schema enables apps to maintain their own configurations and track usage metrics. ### App Schema Tables - [app.account_post_summary](#app-schema-appaccount-post-summary) - Summarizes post statistics for accounts by app - [app.account_reacted_summary](#app-schema-appaccount-reacted-summary) - Summarizes reactions by account per app - [app.account_reaction_summary](#app-schema-appaccount-reaction-summary) - Summarizes reactions on account content per app - [app.feed](#app-schema-appfeed) - Records app feeds - [app.group](#app-schema-appgroup) - Records app groups - [app.metadata](#app-schema-appmetadata) - Stores app metadata - [app.post_feed_tag_summary](#app-schema-apppost-feed-tag-summary) - Summarizes post tags by feed and app - [app.post_reaction_summary](#app-schema-apppost-reaction-summary) - Summarizes post reactions by app - [app.post_summary](#app-schema-apppost-summary) - Summarizes post statistics by app - [app.post_tag_summary](#app-schema-apppost-tag-summary) - Summarizes post tags by app - [app.record](#app-schema-apprecord) - Stores app records - [app.signer](#app-schema-appsigner) - Records app signers - [app.user](#app-schema-appuser) - Records app users ### app.account_post_summary Summarizes post statistics for accounts by app. *Previous was app_stats.profile in Lens V2* ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.app.account_post_summary` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.app.account_post_summary` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | account | bytea | Account address | | feed | bytea | Feed identifier | | app | bytea | App identifier | | total_main | integer | Total main posts | | total_comments | integer | Total comments | | total_reposts | integer | Total reposts | | total_quotes | integer | Total quotes | | total_posts | integer | Total posts | | total_reacted | integer | Total times account reacted | | total_reactions | integer | Total reactions received | | total_collects | integer | Total collects | | updated_at | timestamp with time zone | Last update time | | total_tips | integer | Total tips | Repost/Mirros are not migrated from Lens V2. Total_reposts may not represent the actual numbers from Lens V2 [Back to Top](#lens-protocol-bigquery-schemas) [Back to App Schema](#app-schema) ### app.account_reacted_summary Summarizes reactions by account per app. *Previous was app_stats.profile_reacted in Lens V2* ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.app.account_reacted_summary` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.app.account_reacted_summary` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | account | bytea | Account address | | app | bytea | App identifier | | reaction_type | USER-DEFINED | Type of reaction | | total | integer | Total count | [Back to Top](#lens-protocol-bigquery-schemas) [Back to App Schema](#app-schema) | Column | Type | Description | |--------|------|-------------| | account | bytea | Account address | | app | bytea | App identifier | | reaction_type | USER-DEFINED | Type of reaction | | total | integer | Total count | [Back to Top](#lens-protocol-bigquery-schemas) [Back to App Schema](#app-schema) ### app.feed Records app feeds. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.app.feed` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.app.feed` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | id | integer | Unique identifier | | app | bytea | App identifier | | feed | bytea | Feed identifier | | block_hash | bytea | Hash of the block containing this record | | tx_hash | bytea | Transaction hash | | timestamp | timestamp with time zone | Record time | | sequence_id | numeric | Sequence identifier | [Back to Top](#lens-protocol-bigquery-schemas) [Back to App Schema](#app-schema) ### app.group Records app groups. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.app.group` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.app.group` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | id | integer | Unique identifier | | app | bytea | App identifier | | group | bytea | Group identifier | | block_hash | bytea | Hash of the block containing this record | | tx_hash | bytea | Transaction hash | | timestamp | timestamp with time zone | Record time | | sequence_id | numeric | Sequence identifier | [Back to Top](#lens-protocol-bigquery-schemas) [Back to App Schema](#app-schema) ### app.metadata Stores app metadata. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.app.metadata` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.app.metadata` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | app | bytea | App identifier | | metadata_uri | character varying | URI for the metadata | | metadata_snapshot_location_url | character varying | URL for metadata snapshot | | metadata | jsonb | Stored metadata | | name | character varying | App name | | icon | character varying | App icon URL | | tagline | character varying | App tagline | | description | character varying | App description | | website | character varying | App website | | metadata_version | character | Metadata version | | created_on | timestamp with time zone | Creation time | [Back to Top](#lens-protocol-bigquery-schemas) [Back to App Schema](#app-schema) ### app.post_feed_tag_summary Summarizes post tags by feed and app. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.app.post_feed_tag_summary` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.app.post_feed_tag_summary` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | tag | character varying | Tag name | | app | bytea | App identifier | | feed | bytea | Feed identifier | | total | integer | Total count | [Back to Top](#lens-protocol-bigquery-schemas) [Back to App Schema](#app-schema) ### app.post_reaction_summary Summarizes post reactions by app. *Previous was app_stats.publication_reaction in Lens V2* ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.app.post_reaction_summary` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.app.post_reaction_summary` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | post | bytea | Post identifier | | app | bytea | App identifier | | reaction_type | USER-DEFINED | Type of reaction | | total | integer | Total count | [Back to Top](#lens-protocol-bigquery-schemas) [Back to App Schema](#app-schema) ### app.post_summary Summarizes post statistics by app. *Previous was app_stats.publication in Lens V2* ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.app.post_summary` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.app.post_summary` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | post | bytea | Post identifier | | app | bytea | App identifier | | total_amount_of_collects | integer | Total collects | | total_amount_of_collects_by_flagged_accounts | integer | Collects by flagged accounts | | total_amount_of_acted | integer | Total actions | | total_amount_of_acted_by_flagged_accounts | integer | Actions by flagged accounts | | total_amount_of_reposts | integer | Total reposts | | total_amount_of_reposts_by_flagged_accounts | integer | Reposts by flagged accounts | | total_amount_of_comments | integer | Total comments | | total_amount_of_comments_by_flagged_accounts | integer | Comments by flagged accounts | | total_amount_of_comments_hidden_by_author | integer | Comments hidden by author | | total_amount_of_quotes | integer | Total quotes | | total_amount_of_quotes_by_flagged_accounts | integer | Quotes by flagged accounts | | total_reactions | integer | Total reactions | | total_reactions_by_flagged_accounts | integer | Reactions by flagged accounts | | total_bookmarks | integer | Total bookmarks | | total_bookmarks_by_flagged_accounts | integer | Bookmarks by flagged accounts | | total_amount_of_tips | integer | Total tips | | total_amount_of_tips_by_flagged_accounts | integer | Tips by flagged accounts | Repost/Mirros are not migrated from Lens V2. Total_reposts may not represent the actual numbers from Lens V2 [Back to Top](#lens-protocol-bigquery-schemas) [Back to App Schema](#app-schema) ### app.post_tag_summary Summarizes post tags by app. *Previous was app_stats.publication_tag in Lens V2* ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.app.post_tag_summary` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.app.post_tag_summary` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | tag | character varying | Tag name | | app | bytea | App identifier | | total | integer | Total count | [Back to Top](#lens-protocol-bigquery-schemas) [Back to App Schema](#app-schema) ### app.record Stores app records. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.app.record` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.app.record` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | app | bytea | App identifier | | graph | bytea | Graph identifier | | graph_last_updated_sequence_id | numeric | Graph update sequence ID | | sponsorship | bytea | Sponsorship identifier | | sponsorship_last_updated_sequence_id | numeric | Sponsorship update sequence ID | | namespace | bytea | Namespace identifier | | namespace_last_updated_sequence_id | numeric | Namespace update sequence ID | | default_feed | bytea | Default feed identifier | | default_feed_last_updated_sequence_id | numeric | Default feed update sequence ID | | treasury | bytea | Treasury address | | treasury_last_updated_sequence_id | numeric | Treasury update sequence ID | | source_stamp_verification_set | boolean | Source stamp verification status | | source_stamp_verification_set_last_updated_sequence_id | numeric | Source stamp verification update sequence ID | | block_hash | bytea | Hash of the block containing this record | | tx_hash | bytea | Transaction hash | | timestamp | timestamp with time zone | Record time | | sequence_id | numeric | Sequence identifier | [Back to Top](#lens-protocol-bigquery-schemas) [Back to App Schema](#app-schema) ### app.signer Records app signers. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.app.signer` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.app.signer` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | id | integer | Unique identifier | | app | bytea | App identifier | | signer | bytea | Signer address | | block_hash | bytea | Hash of the block containing this record | | tx_hash | bytea | Transaction hash | | timestamp | timestamp with time zone | Record time | | sequence_id | numeric | Sequence identifier | [Back to Top](#lens-protocol-bigquery-schemas) [Back to App Schema](#app-schema) ### app.user Records app users. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.app.user` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.app.user` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | app | bytea | App identifier | | account | bytea | User account address | | first_action_on | timestamp with time zone | First action time | | last_action_on | timestamp with time zone | Last action time | [Back to Top](#lens-protocol-bigquery-schemas) [Back to App Schema](#app-schema) ## Feed Schema The Feed Schema manages content feeds in the platform. It stores feed metadata, records, and usage statistics. Feeds are used to organize and display content in different ways across the platform, allowing for custom content streams. ### Feed Schema Tables - [feed.metadata](#feed-schema-feedmetadata) - Stores feed metadata - [feed.record](#feed-schema-feedrecord) - Stores feed records - [feed.record_stats](#feed-schema-feedrecord-stats) - Records feed usage statistics ### feed.metadata Stores feed metadata. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.feed.metadata` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.feed.metadata` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | feed | bytea | Feed identifier | | metadata_uri | character varying | URI for the metadata | | metadata_snapshot_location_url | character varying | URL for metadata snapshot | | metadata | jsonb | Stored metadata | | name | character varying | Feed name | | description | character varying | Feed description | | metadata_version | character | Metadata version | | created_on | timestamp with time zone | Creation time | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Feed Schema](#feed-schema) ### feed.record Stores feed records. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.feed.record` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.feed.record` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | feed | bytea | Feed identifier | | block_hash | bytea | Hash of the block containing this record | | tx_hash | bytea | Transaction hash | | timestamp | timestamp with time zone | Record time | | sequence_id | numeric | Sequence identifier | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Feed Schema](#feed-schema) ### feed.record_stats Records feed usage statistics. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.feed.record_stats` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.feed.record_stats` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | feed | bytea | Feed identifier | | used_by_apps_total | integer | Total app usage count | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Feed Schema](#feed-schema) ## Graph Schema The Graph Schema manages social graph data in the platform. It tracks relationships between users, stores graph metadata, and records usage statistics. This schema provides the foundation for social connections and interactions. ### Graph Schema Tables - [graph.metadata](#graph-schema-graphmetadata) - Stores graph metadata - [graph.record](#graph-schema-graphrecord) - Stores graph records - [graph.record_stats](#graph-schema-graphrecord-stats) - Records graph usage statistics ### graph.metadata Stores graph metadata. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.graph.metadata` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.graph.metadata` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | graph | bytea | Graph identifier | | metadata_uri | character varying | URI for the metadata | | metadata_snapshot_location_url | character varying | URL for metadata snapshot | | metadata | jsonb | Stored metadata | | name | character varying | Graph name | | description | character varying | Graph description | | metadata_version | character | Metadata version | | created_on | timestamp with time zone | Creation time | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Graph Schema](#graph-schema) ### graph.record Stores graph records. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.graph.record` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.graph.record` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | graph | bytea | Graph identifier | | block_hash | bytea | Hash of the block containing this record | | tx_hash | bytea | Transaction hash | | timestamp | timestamp with time zone | Record time | | sequence_id | numeric | Sequence identifier | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Graph Schema](#graph-schema) ### graph.record_stats Records graph usage statistics. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.graph.record_stats` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.graph.record_stats` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | graph | bytea | Graph identifier | | used_by_apps_total | integer | Total app usage count | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Graph Schema](#graph-schema) ## Group Schema The Group Schema manages community groups in the platform. It tracks group members, banned users, membership requests, and group metadata. Groups provide a way for users to organize around common interests or purposes. ### Group Schema Tables - [group.banned](#group-schema-groupbanned) - Records banned group members - [group.member](#group-schema-groupmember) - Records group members - [group.membership_approval_requests](#group-schema-groupmembership-approval-requests) - Records membership approval requests - [group.metadata](#group-schema-groupmetadata) - Stores group metadata - [group.record](#group-schema-grouprecord) - Stores group records - [group.record_stats](#group-schema-grouprecord-stats) - Stores group statistics ### group.banned Records banned group members. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.group.banned` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.group.banned` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | id | integer | Unique identifier | | group | bytea | Group identifier | | config_salt | bytea | Configuration salt | | account | bytea | Banned account address | | banned_by_account | bytea | Account that performed the ban | | block_hash | bytea | Hash of the block containing this record | | tx_hash | bytea | Transaction hash | | timestamp | timestamp with time zone | Ban time | | sequence_id | numeric | Sequence identifier | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Group Schema](#group-schema) ### group.member Records group members. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.group.member` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.group.member` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | id | integer | Unique identifier | | group | bytea | Group identifier | | account | bytea | Member account address | | block_hash | bytea | Hash of the block containing this record | | tx_hash | bytea | Transaction hash | | timestamp | timestamp with time zone | Membership time | | sequence_id | numeric | Sequence identifier | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Group Schema](#group-schema) ### group.membership_approval_requests Records membership approval requests. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.group.membership_approval_requests` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.group.membership_approval_requests` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | id | integer | Unique identifier | | group | bytea | Group identifier | | config_salt | bytea | Configuration salt | | account | bytea | Requesting account address | | block_hash | bytea | Hash of the block containing this record | | tx_hash | bytea | Transaction hash | | timestamp | timestamp with time zone | Request time | | sequence_id | numeric | Sequence identifier | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Group Schema](#group-schema) ### group.metadata Stores group metadata. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.group.metadata` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.group.metadata` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | group | bytea | Group identifier | | metadata_uri | character varying | URI for the metadata | | metadata_snapshot_location_url | character varying | URL for metadata snapshot | | metadata | jsonb | Stored metadata | | name | character varying | Group name | | icon | character varying | Group icon URL | | description | character varying | Group description | | metadata_version | character | Metadata version | | created_on | timestamp with time zone | Creation time | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Group Schema](#group-schema) ### group.record Stores group records. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.group.record` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.group.record` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | group | bytea | Group identifier | | block_hash | bytea | Hash of the block containing this record | | tx_hash | bytea | Transaction hash | | timestamp | timestamp with time zone | Record time | | sequence_id | numeric | Sequence identifier | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Group Schema](#group-schema) ### group.record_stats Stores group statistics. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.group.record_stats` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.group.record_stats` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | group | bytea | Group identifier | | members_total | integer | Total number of members | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Group Schema](#group-schema) ### Metadata Schema The Metadata Schema manages metadata processing and refresh operations across the platform. It tracks pending and failed metadata processing tasks and handles refresh requests for various entities in the system. This schema ensures metadata consistency and availability. ### Metadata Schema Tables - [metadata.failed](#metadata-schema-metadatafailed) - Records failed metadata processing - [metadata.pending](#metadata-schema-metadatapending) - Records pending metadata processing - [metadata.refresh](#metadata-schema-metadatarefresh) - Tracks metadata refresh requests ### metadata.failed Records failed metadata processing. *Previous was publication.failed in Lens V2* ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.metadata.failed` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.metadata.failed` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | id | integer | Unique identifier | | source | character varying | Source name | | account | bytea | Related account identifier | | graph | bytea | Related graph identifier | | post | bytea | Related post identifier | | feed | bytea | Related feed identifier | | group | bytea | Related group identifier | | sponsorship | bytea | Related sponsorship identifier | | app | bytea | Related app identifier | | namespace | bytea | Related namespace identifier | | metadata_uri | character varying | Metadata URI | | reason | character varying | Failure reason | | created_on | timestamp with time zone | Failure time | | action | bytea | Related action identifier | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Metadata Schema](#metadata-schema) ### metadata.pending Records pending metadata processing. *Previous was publication.pending in Lens V2* ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.metadata.pending` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.metadata.pending` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | id | integer | Unique identifier | | source | character varying | Source name | | account | bytea | Related account identifier | | graph | bytea | Related graph identifier | | post | bytea | Related post identifier | | feed | bytea | Related feed identifier | | group | bytea | Related group identifier | | sponsorship | bytea | Related sponsorship identifier | | app | bytea | Related app identifier | | namespace | bytea | Related namespace identifier | | metadata_uri | character varying | Metadata URI | | sequence_id | numeric | Sequence identifier | | created_on | timestamp with time zone | Creation time | | action | bytea | Related action identifier | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Metadata Schema](#metadata-schema) ### metadata.refresh Tracks metadata refresh requests. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.metadata.refresh` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.metadata.refresh` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | id | uuid | Unique identifier | | entity | text | Entity being refreshed | | status | USER-DEFINED | Refresh status | | reason | text | Refresh reason | | updated_at | timestamp with time zone | Last update time | | created_at | timestamp with time zone | Creation time | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Metadata Schema](#metadata-schema) ## ML Schema The ML Schema (Machine Learning Schema) manages AI-powered features in the platform. It stores data related to account quality scores, personalized feeds, trending content, and reply rankings. This schema enables intelligent content discovery and moderation. ### ML Schema Tables - [ml.account_score](#ml-schema-mlaccount-score) - Records quality scores for accounts - [ml.for_you_global_timeline](#ml-schema-mlfor-you-global-timeline) - Stores data for personalized "For You" feeds - [ml.popularity_trending_timeline](#ml-schema-mlpopularity-trending-timeline) - Tracks trending posts based on popularity - [ml.reply_ranking](#ml-schema-mlreply-ranking) - Ranks replies for improved display ### ml.account_score Records quality scores for accounts. *Previous was machine_learning.quality_profiles in Lens V2. Quality score used to be [0 - 10000] in V2. In V3, score is rescaled to [0 - 100] with 2 decimal point decision* ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.ml.account_score` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.ml.account_score` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | account | bytea | Account identifier | | score | numeric | Quality score. Ranging from [0 - 100] | | generated_at | timestamp with time zone | Score generation time | [Back to Top](#lens-protocol-bigquery-schemas) [Back to ML Schema](#ml-schema) ### ml.for_you_global_timeline Stores data for personalized "For You" feeds. *Previous was machine_learning.for_you_global_feed in Lens V2* ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.ml.for_you_global_timeline` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.ml.for_you_global_timeline` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | post | bytea | Post identifier | | account | bytea | Account identifier | | rank | integer | Ranking position | | source | character varying | Source of ranking | | generated_at | timestamp with time zone | Generation time | [Back to Top](#lens-protocol-bigquery-schemas) [Back to ML Schema](#ml-schema) ### ml.popularity_trending_timeline Tracks trending posts based on popularity. *Previous was machine_learning.popularity_trending_feed in Lens V2* ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.ml.popularity_trending_timeline` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.ml.popularity_trending_timeline` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | post | bytea | Post identifier | | account | bytea | Account identifier | | score | integer | Popularity score | | generated_at | timestamp with time zone | Generation time | [Back to Top](#lens-protocol-bigquery-schemas) [Back to ML Schema](#ml-schema) ### ml.reply_ranking Ranks replies for improved display. *Previous was machine_learning.reply_ranking in Lens V2* ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.ml.reply_ranking` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.ml.reply_ranking` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | parent_post | bytea | Parent post identifier | | post | bytea | Reply post identifier | | score | integer | Ranking score | | generated_at | timestamp with time zone | Generation time | [Back to Top](#lens-protocol-bigquery-schemas) [Back to ML Schema](#ml-schema) ## Post Schema The Post Schema is central to content management in the platform. It handles all aspects of posts including content, metadata, actions, rules, reactions, and mentions. This schema tracks post content, interactions, statistics, and relationships between posts. Hex Adresss, i.e. account, post, in the dataset is stored in its raw binary format (bytea). Lens provide a public function called `FORMAT_HEX` that converts BigQuery's `\x` format to the standard Web3 `0x` format. ```sql filename="Mainnet" SELECT account as original_account_address, `lens-protocol-mainnet.post.FORMAT_HEX`(account) as web3_formatted_account_address FROM `lens-protocol-mainnet.post.record` LIMIT 5; ``` ```sql filename="Testnet" SELECT account as original_account_address, `lens-protocol-testnet.post.FORMAT_HEX`(account) as web3_formatted_account_address FROM `lens-protocol-testnet.post.record` LIMIT 5; ``` ### Post Schema Tables - [post.account_mention](#post-schema-postaccount-mention) - Records account mentions in posts - [post.action](#post-schema-postaction) - Records post actions - [post.action_config](#post-schema-postaction-config) - Stores post action configurations - [post.action_executed](#post-schema-postaction-executed) - Records executed post actions - [post.action_executed_by_account_count](#post-schema-postaction-executed-by-account-count) - Counts actions executed by accounts on posts - [post.action_executed_count](#post-schema-postaction-executed-count) - Counts actions executed on posts - [post.action_metadata](#post-schema-postaction-metadata) - Stores metadata for post actions - [post.extra_data](#post-schema-postextra-data) - Stores extra data for posts - [post.feed_tag_summary](#post-schema-postfeed-tag-summary) - Summarizes post tags by feed - [post.group_mention](#post-schema-postgroup-mention) - Records group mentions in posts - [post.hashtag](#post-schema-posthashtag) - Records hashtags used in posts - [post.metadata](#post-schema-postmetadata) - Stores post metadata - [post.metadata_edited](#post-schema-postmetadata-edited) - Records edited post metadata - [post.reaction](#post-schema-postreaction) - Records reactions to posts - [post.reaction_summary](#post-schema-postreaction-summary) - Summarizes reactions to posts - [post.record](#post-schema-postrecord) - Stores post records - [post.rule_config](#post-schema-postrule-config) - Stores post rule configurations - [post.rule_selector](#post-schema-postrule-selector) - Stores post rule selectors - [post.summary](#post-schema-postsummary) - Summarizes post statistics - [post.tag](#post-schema-posttag) - Records tags used in posts - [post.tag_summary](#post-schema-posttag-summary) - Summarizes tag usage - [post.universal_action_config](#post-schema-postuniversal-action-config) - Stores universal action configurations for posts ### post.account_mention Records account mentions in posts. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.post.account_mention` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.post.account_mention` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | mention_id | integer | Unique identifier | | post | bytea | Post identifier | | account | bytea | Mentioned account | | namespace | bytea | Namespace identifier | | snapshot_username_used | character varying | Username used in mention | | timestamp | timestamp with time zone | Mention time | | sequence_id | numeric | Sequence identifier | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Post Schema](#post-schema) ### post.action Records post actions. *Previous was publication.open_action_module_multirecipient in Lens V2* ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.post.action` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.post.action` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | id | integer | Unique identifier | | post | bytea | Post identifier | | implementation | bytea | Implementation address | | setup_data | text | Setup data | | setup_return_data | text | Setup return data | | collect_limit | character varying | Collection limit | | collect_nft_address | bytea | Collect NFT address | | amount | character varying | Action amount | | follower_only | boolean | Follower-only restriction | | currency | bytea | Currency address | | recipients | ARRAY | Recipients list | | referral_fee | numeric | Referral fee percentage | | end_timestamp | timestamp with time zone | End time | | tx_hash | bytea | Transaction hash | | block_hash | bytea | Block hash | | timestamp | timestamp with time zone | Action time | | sequence_id | numeric | Sequence identifier | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Post Schema](#post-schema) ### post.action_config Stores post action configurations. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.post.action_config` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.post.action_config` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | id | integer | Unique identifier | | action_address | bytea | Action address | | type | USER-DEFINED | Action type | | feed | bytea | Feed identifier | | post_id | bytea | Post identifier | | setup_by_account | bytea | Setup account | | decoded_config_params | jsonb | Decoded config parameters | | disabled | boolean | Whether action is disabled | | timestamp | timestamp with time zone | Configuration time | | sequence_id | numeric | Sequence identifier | | disable_extra_data | bytea | Disable extra data | | enable_extra_data | bytea | Enable extra data | | last_updated_sequence_id | numeric | Last update sequence ID | | return_data | bytea | Return data | | raw_config_params | jsonb | Raw config parameters | | app | bytea | App identifier | | collect_nft_address | bytea | Collect NFT address | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Post Schema](#post-schema) ### post.action_executed Records executed post actions. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.post.action_executed` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.post.action_executed` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | id | integer | Unique identifier | | action_address | bytea | Action address | | type | USER-DEFINED | Action type | | feed | bytea | Feed identifier | | post_id | bytea | Post identifier | | raw_params | bytea | Raw parameters | | decoded_params | jsonb | Decoded parameters | | timestamp | timestamp with time zone | Execution time | | sequence_id | numeric | Sequence identifier | | app | bytea | App identifier | | account | bytea | Executing account | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Post Schema](#post-schema) ### post.action_executed_by_account_count Counts actions executed by accounts on posts. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.post.action_executed_by_account_count` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.post.action_executed_by_account_count` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | action_address | bytea | Action address | | post_id | bytea | Post identifier | | by_account | bytea | Executing account | | total | integer | Execution count | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Post Schema](#post-schema) ### post.action_executed_count Counts actions executed on posts. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.post.action_executed_count` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.post.action_executed_count` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | action_address | bytea | Action address | | post_id | bytea | Post identifier | | total | integer | Execution count | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Post Schema](#post-schema) ### post.action_metadata Stores metadata for post actions. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.post.action_metadata` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.post.action_metadata` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | action | bytea | Action identifier | | metadata_uri | character varying | Metadata URI | | metadata_snapshot_location_url | character varying | Metadata snapshot URL | | metadata | jsonb | Stored metadata | | name | character varying | Action name | | description | character varying | Action description | | metadata_version | character | Metadata version | | created_on | timestamp with time zone | Creation time | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Post Schema](#post-schema) ### post.extra_data Stores extra data for posts. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.post.extra_data` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.post.extra_data` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | id | integer | Unique identifier | | key | bytea | Data key | | value | bytea | Data value | | post | bytea | Post identifier | | block_hash | bytea | Block hash | | tx_hash | bytea | Transaction hash | | timestamp | timestamp with time zone | Record time | | last_updated_sequence_id | numeric | Last update sequence ID | | sequence_id | numeric | Sequence identifier | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Post Schema](#post-schema) ### post.feed_tag_summary Summarizes post tags by feed. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.post.feed_tag_summary` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.post.feed_tag_summary` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | tag | character varying | Tag name | | feed | bytea | Feed identifier | | total | integer | Usage count | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Post Schema](#post-schema) ### post.group_mention Records group mentions in posts. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.post.group_mention` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.post.group_mention` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | mention_id | integer | Unique identifier | | post | bytea | Post identifier | | group | bytea | Mentioned group | | timestamp | timestamp with time zone | Mention time | | sequence_id | numeric | Sequence identifier | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Post Schema](#post-schema) ### post.hashtag Records hashtags used in posts. *Previous was publication.hashtag in Lens V2* ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.post.hashtag` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.post.hashtag` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | post | bytea | Post identifier | | hashtag | character varying | Hashtag text | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Post Schema](#post-schema) ### post.metadata Stores post metadata. *Previous was publication.metadata in Lens V2* ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.post.metadata` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.post.metadata` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | post | bytea | Post identifier | | metadata_snapshot_location_url | character varying | Metadata snapshot URL | | metadata | jsonb | Stored metadata | | metadata_version | character | Metadata version | | content | text | Post content | | content_vector | tsvector | Content vector for search | | language | character | Language code | | region | character | Region code | | content_warning | USER-DEFINED | Content warning type | | main_content_focus | USER-DEFINED | Main content focus | | tags_vector | tsvector | Tags vector for search | | is_encrypted | boolean | Whether content is encrypted | | created_at | timestamp with time zone | Creation time | | app | bytea | App identifier | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Post Schema](#post-schema) ### post.metadata_edited Records edited post metadata. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.post.metadata_edited` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.post.metadata_edited` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | id | integer | Unique identifier | | post | bytea | Post identifier | | metadata_snapshot_location_url | character varying | Metadata snapshot URL | | metadata | jsonb | Stored metadata | | metadata_version | character | Metadata version | | content | text | Edited content | | content_vector | tsvector | Content vector for search | | language | character | Language code | | region | character | Region code | | content_warning | USER-DEFINED | Content warning type | | main_content_focus | USER-DEFINED | Main content focus | | tags_vector | tsvector | Tags vector for search | | is_encrypted | boolean | Whether content is encrypted | | created_at | timestamp with time zone | Edit time | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Post Schema](#post-schema) ### post.reaction Records reactions to posts. *Previous was publication.reaction in Lens V2* ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.post.reaction` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.post.reaction` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | post | bytea | Post identifier | | account | bytea | Reacting account | | type | USER-DEFINED | Reaction type | | action_at | timestamp with time zone | Reaction time | | app | bytea | App identifier | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Post Schema](#post-schema) ### post.reaction_summary Summarizes reactions to posts. *Previous was global_stats.publication_reaction in Lens V2* ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.post.reaction_summary` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.post.reaction_summary` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | post | bytea | Post identifier | | reaction_type | USER-DEFINED | Reaction type | | total | integer | Reaction count | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Post Schema](#post-schema) ### post.record Stores post records. *Previous was publication.record in Lens V2* Corresponding clustered table [post.record_clustered_by_account](#post-schema-postrecord-cluster-postrecord-clustered-by-account) is available for better performance on `account` column. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.post.record` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.post.record` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | id | bytea | Post identifier | | feed_local_sequential_id | bytea | Feed sequential ID | | legacy_id | character varying | Legacy post ID | | feed | bytea | Feed identifier | | account | bytea | Author account | | content_uri | character varying | Content URI | | post_types | JSON | Post types | | parent_post | bytea | Parent post | | quoted_post | bytea | Quoted post | | root_post | bytea | Root post | | app | bytea | App identifier | | metadata_passed | boolean | Whether metadata passed validation | | is_deleted | boolean | Whether post is deleted | | is_hidden_by_parent | boolean | Whether hidden by parent | | is_edited | boolean | Whether post is edited | | block_hash | bytea | Block hash | | tx_hash | bytea | Transaction hash | | timestamp | timestamp with time zone | Post time | | sequence_id | numeric | Sequence identifier | | slug | character varying | URL slug | #### Cluster post.record_clustered_by_account Clustered table for post.record on `account` column. [BigQuery Clustered tables](https://cloud.google.com/bigquery/docs/clustered-tables) can improve query performance and reduce query costs on clustered columns. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.post.record_clustered_by_account` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.post.record_clustered_by_account` LIMIT 1; ``` [Back to Top](#lens-protocol-bigquery-schemas) [Back to Post Schema](#post-schema) ### post.rule_config Stores post rule configurations. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.post.rule_config` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.post.rule_config` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | id | integer | Unique identifier | | rule_address | bytea | Rule address | | config_salt | bytea | Configuration salt | | type | USER-DEFINED | Rule type | | feed | bytea | Feed identifier | | post_id | bytea | Post identifier | | raw_config_params | jsonb | Raw config parameters | | decoded_config_params | jsonb | Decoded config parameters | | timestamp | timestamp with time zone | Configuration time | | sequence_id | numeric | Sequence identifier | | last_updated_sequence_id | numeric | Last update sequence ID | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Post Schema](#post-schema) ### post.rule_selector Stores post rule selectors. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.post.rule_selector` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.post.rule_selector` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | id | integer | Unique identifier | | config_salt | bytea | Configuration salt | | selector | USER-DEFINED | Rule selector | | is_required | boolean | Whether rule is required | | timestamp | timestamp with time zone | Configuration time | | sequence_id | numeric | Sequence identifier | | feed | bytea | Feed identifier | | post_id | bytea | Post identifier | | rule_address | bytea | Rule address | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Post Schema](#post-schema) ### post.summary Summarizes post statistics. *Previous was global_stats.publication in Lens V2* ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.post.summary` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.post.summary` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | post | bytea | Post identifier | | total_amount_of_collects | integer | Total collects | | total_amount_of_collects_by_flagged_accounts | integer | Collects by flagged accounts | | total_amount_of_acted | integer | Total actions | | total_amount_of_acted_by_flagged_accounts | integer | Actions by flagged accounts | | total_amount_of_reposts | integer | Total reposts | | total_amount_of_reposts_by_flagged_accounts | integer | Reposts by flagged accounts | | total_amount_of_comments | integer | Total comments | | total_amount_of_comments_by_flagged_accounts | integer | Comments by flagged accounts | | total_amount_of_comments_hidden_by_author | integer | Comments hidden by author | | total_amount_of_quotes | integer | Total quotes | | total_amount_of_quotes_by_flagged_accounts | integer | Quotes by flagged accounts | | total_reactions | integer | Total reactions | | total_reactions_by_flagged_accounts | integer | Reactions by flagged accounts | | total_bookmarks | integer | Total bookmarks | | total_bookmarks_by_flagged_accounts | integer | Bookmarks by flagged accounts | | total_amount_of_tips | integer | Total tips | | total_amount_of_tips_by_flagged_accounts | integer | Tips by flagged accounts | Repost/Mirros are not migrated from Lens V2. Total_reposts may not represent the actual numbers from Lens V2 [Back to Top](#lens-protocol-bigquery-schemas) [Back to Post Schema](#post-schema) ### post.tag Records tags used in posts. *Previous was publication.tag in Lens V2* ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.post.tag` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.post.tag` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | post | bytea | Post identifier | | tag | character varying | Tag text | | tag_vector | tsvector | Tag vector for search | | timestamp | timestamp with time zone | Tag time | | sequence_id | numeric | Sequence identifier | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Post Schema](#post-schema) ### post.tag_summary Summarizes tag usage. *Previous was global_stats.publication_tag in Lens V2* ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.post.tag_summary` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.post.tag_summary` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | tag | character varying | Tag text | | total | integer | Usage count | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Post Schema](#post-schema) ### post.universal_action_config Stores universal action configurations for posts. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.post.universal_action_config` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.post.universal_action_config` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | id | integer | Unique identifier | | action_address | bytea | Action address | | type | USER-DEFINED | Action type | | raw_config_params | jsonb | Raw config parameters | | decoded_config_params | jsonb | Decoded config parameters | | timestamp | timestamp with time zone | Configuration time | | sequence_id | numeric | Sequence identifier | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Post Schema](#post-schema) ## Rule Schema The Rule Schema manages platform rules and their configurations. It tracks rule definitions, configurations, and selectors that determine how rules are applied across different contexts in the platform. ### Rule Schema Tables - [rule.config](#rule-schema-ruleconfig) - Stores rule configurations - [rule.selector](#rule-schema-ruleselector) - Stores rule selectors ### rule.config Stores rule configurations. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.rule.config` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.rule.config` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | id | integer | Unique identifier | | rule_address | bytea | Rule address | | config_salt | bytea | Configuration salt | | type | USER-DEFINED | Rule type | | primitive | bytea | Primitive address | | raw_config_params | jsonb | Raw config parameters | | decoded_config_params | jsonb | Decoded config parameters | | timestamp | timestamp with time zone | Configuration time | | sequence_id | numeric | Sequence identifier | | last_updated_sequence_id | numeric | Last update sequence ID | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Rule Schema](#rule-schema) ### rule.selector Stores rule selectors. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.rule.selector` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.rule.selector` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | id | integer | Unique identifier | | config_salt | bytea | Configuration salt | | selector | USER-DEFINED | Rule selector | | is_required | boolean | Whether rule is required | | timestamp | timestamp with time zone | Configuration time | | sequence_id | numeric | Sequence identifier | | primitive | bytea | Primitive address | | rule_address | bytea | Rule address | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Rule Schema](#rule-schema) ## Username Schema The Username Schema manages username-related data in the platform. It tracks username records, reservations, namespace records, and metadata. This schema ensures unique, human-readable user identifiers across the platform. ### Username Schema Tables - [username.metadata](#username-schema-usernamemetadata) - Stores username namespace metadata - [username.namespace_record](#username-schema-usernamenamespace-record) - Records username namespaces - [username.namespace_record_stats](#username-schema-usernamenamespace-record-stats) - Records namespace statistics - [username.record](#username-schema-usernamerecord) - Stores username records - [username.reserved](#username-schema-usernamereserved) - Records reserved usernames ### username.metadata Stores username namespace metadata. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.username.metadata` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.username.metadata` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | namespace | bytea | Namespace identifier | | metadata_uri | character varying | Metadata URI | | metadata_snapshot_location_url | character varying | Metadata snapshot URL | | metadata | jsonb | Stored metadata | | name | character varying | Namespace name | | description | character varying | Namespace description | | metadata_version | character | Metadata version | | created_on | timestamp with time zone | Creation time | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Username Schema](#username-schema) ### username.namespace_record Records username namespaces. *Previous was namespace.record in Lens V2* ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.username.namespace_record` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.username.namespace_record` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | address | bytea | Namespace address | | namespace | character varying | Namespace name | | block_hash | bytea | Block hash | | tx_hash | bytea | Transaction hash | | timestamp | timestamp with time zone | Record time | | sequence_id | numeric | Sequence identifier | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Username Schema](#username-schema) ### username.namespace_record_stats Records namespace statistics. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.username.namespace_record_stats` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.username.namespace_record_stats` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | namespace | bytea | Namespace identifier | | usernames_total | integer | Total usernames count | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Username Schema](#username-schema) ### username.record Stores username records. *Previous was namespace.handle in Lens V2* ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.username.record` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.username.record` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | id | integer | Unique identifier | | namespace | bytea | Namespace identifier | | local_name | character varying | Local username | | account | bytea | Account address | | rule_data | bytea | Rule data | | block_hash | bytea | Block hash | | tx_hash | bytea | Transaction hash | | timestamp | timestamp with time zone | Record time | | sequence_id | numeric | Sequence identifier | | is_simple_charset | boolean | Whether simple charset is used | | token_id | bytea | Token ID | | last_transfer_updated_sequence_id | numeric | Last transfer sequence ID | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Username Schema](#username-schema) ### username.reserved Records reserved usernames. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.username.reserved` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.username.reserved` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | id | integer | Unique identifier | | namespace | bytea | Namespace identifier | | local_name | character varying | Reserved username | | config_salt | bytea | Configuration salt | | block_hash | bytea | Block hash | | tx_hash | bytea | Transaction hash | | timestamp | timestamp with time zone | Reservation time | | sequence_id | numeric | Sequence identifier | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Username Schema](#username-schema) ## Miscellaneous Schema The Miscellaneous Schema contains tables that don't fit into other categories. It includes currency records, transaction tracking, and extra data storage. These tables support various platform functions that aren't specific to a single domain. ### Miscellaneous Schema Tables - [currencies.record](#miscellaneous-schema-currenciesrecord) - Records supported currencies - [transaction.known_transactions](#miscellaneous-schema-transactionknown-transactions) - Records known transactions - [extra_data.record](#miscellaneous-schema-extra-datarecord) - Stores extra data records ### currencies.record Records supported currencies. *Previous was enabled.currency in Lens V2* ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.currencies.record` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.currencies.record` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | currency | bytea | Currency address | | name | character varying | Currency name | | pretty_name | character varying | Formatted currency name | | symbol | character varying | Currency symbol | | decimals | integer | Decimal places | | verified | boolean | Verification status | | block_hash | bytea | Hash of the block containing this record | | tx_hash | bytea | Transaction hash | | timestamp | timestamp with time zone | Record time | | sequence_id | numeric | Sequence identifier | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Miscellaneous Schema](#miscellaneous-schema) ### transaction.known_transactions Records known transactions. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.transaction.known_transactions` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.transaction.known_transactions` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | id | integer | Unique identifier | | tx_hash | bytea | Transaction hash | | indexing_status | USER-DEFINED | Indexing status | | failed_reason | character varying | Failure reason | | block_hash | bytea | Block hash | | block_timestamp | timestamp with time zone | Block timestamp | | sequence_id | numeric | Sequence identifier | | operation | USER-DEFINED | Operation type | | dependencies_operations | JSON | Dependent operations | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Miscellaneous Schema](#miscellaneous-schema) ### extra_data.record Stores extra data records. ```sql filename="Mainnet" SELECT * FROM `lens-protocol-mainnet.extra_data.record` LIMIT 1; ``` ```sql filename="Testnet" SELECT * FROM `lens-protocol-testnet.extra_data.record` LIMIT 1; ``` | Column | Type | Description | |--------|------|-------------| | id | integer | Unique identifier | | key | bytea | Key | | value | bytea | Value | | primitive | bytea | Primitive | | block_hash | bytea | Hash of the block containing this record | | tx_hash | bytea | Transaction hash | | timestamp | timestamp with time zone | Record time | | last_updated_sequence_id | numeric | Last update sequence ID | | sequence_id | numeric | Sequence identifier | [Back to Top](#lens-protocol-bigquery-schemas) [Back to Miscellaneous Schema](#miscellaneous-schema) ## Clustered Tables Clustered tables are optimized for performance and cost. If you frequently query on a specific column, consider using clustered tables. If desired clustered tables are not available, please contact Lens team to request them. - [account.follower_summary_clustered_by_account](#account-schema-accountfollower-summary-cluster-accountfollower-summary-clustered-by-account) - [account.post_summary_clustered_by_account](#account-schema-accountpost-summary-cluster-accountpost-summary-clustered-by-account) - [post.record_clustered_by_account](#post-schema-postrecord-cluster-postrecord-clustered-by-account) ================ File: src/pages/protocol/bigquery/setup.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; # Quick Start Guide Get started with querying Lens Chain data using BigQuery in minutes. --- ## Prerequisites Before you begin, ensure you have: - A Google Cloud account ([Sign up here](https://console.cloud.google.com)) - A Google Cloud project with billing enabled - BigQuery API access enabled New Google Cloud users get $300 in free credits valid for 90 days. For current pricing details, visit the [official BigQuery pricing page](https://cloud.google.com/bigquery/pricing). ## Setup Process ### Create or Select a Project 1. Go to [Google Cloud Console](https://console.cloud.google.com) 2. In the top navigation bar, click on the project dropdown menu 3. For a [new project](https://developers.google.com/workspace/guides/create-project#google-cloud-console): - Click "New Project" - Enter a project name (e.g., "lens-analytics") - Optional: Select an organization and location - Click "CREATE" 4. For an existing project: - Click on the project selector - Use the search bar to find your project - Select the project from the list Make sure you have sufficient permissions (Owner or Editor role) for the project you select. ### Enable Billing 1. Go to the [Google Cloud Console Billing page](https://console.cloud.google.com/billing) 2. Select your project 3. Click "Link a billing account" 4. Either select an existing billing account or create a new one ### Enable BigQuery API 1. Go to [BigQuery API page](https://console.cloud.google.com/apis/library/bigquery.googleapis.com) 2. Click "Enable" ### Access Lens Datasets 1. Open [BigQuery Console](https://console.cloud.google.com/bigquery) 2. In the query editor, paste and run this query to explore a table's schema: ```sql SELECT table_name FROM `lens-protocol-mainnet.INFORMATION_SCHEMA.TABLES`; ``` ```sql SELECT table_name FROM `lens-protocol-testnet.INFORMATION_SCHEMA.TABLES`; ``` or look at the schemas pages for specific tables: ```sql SELECT YOUR_COLUMN_NAME1, YOUR_COLUMN_NAME2 ... FROM `lens-protocol-mainnet.INSERT_YOUR_TABLE_NAME_HERE` ``` ```sql SELECT YOUR_COLUMN_NAME1, YOUR_COLUMN_NAME2 ... FROM `lens-protocol-testnet.INSERT_YOUR_TABLE_NAME_HERE` ``` **Remember**: Always verify your query's cost before running it by clicking the "Query Validation" button. ================ File: src/pages/protocol/concepts/account.mdx ================ export const meta = { showBreadcrumbs: true, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; import ConceptAccountIllustration from "@/components/mdx/components/concepts/ConceptIllustrations/ConceptsAccountIllustration"; export default ({ children }) => {children}; {/* Start of the page content */} # Account An Account for Lens is a smart contract designed to support flexible ownership and programmable interactions. Each Account enables collaborative management by allowing the primary owner to assign permissions to additional account managers. These managers can perform actions on behalf of the account—such as posting content, following others, or setting metadata—without compromising the owner’s authority to transfer ownership or revoke permissions. This structure provides flexibility for groups or organizations to control a single account collaboratively alongside. Accounts can also enforce custom controls, like blocking or unblocking users, and support features such as custom signature schemes, multi-sig capabilities, and spending limits. This level of programmability opens new possibilities for personalized and secure interactions within the Lens ecosystem, adapting to various use cases. ================ File: src/pages/protocol/concepts/actions.mdx ================ export const meta = { showBreadcrumbs: true, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; import ConceptsActionsIllustration from "@/components/mdx/components/concepts/ConceptIllustrations/ConceptsActionsIllustration"; export default ({ children }) => {children}; {/* Start of the page content */} # Actions Actions are modular components that offer a flexible and granular approach to interactions within the Lens protocol. They are categorized into global, account-based, and post-based types, each serving different scopes of functionality. Global actions affect multiple users or posts, account-based actions target specific profiles, and post-based actions focus on individual posts. This modular design allows developers to configure actions based on context. For instance, collectibility can be managed through a CollectAction contract, where authors set parameters like collection type and price. Actions can be batched into a single transaction using multicall, improving efficiency and reducing costs. Since accounts in the Lens protocol are fully programmable smart accounts (thanks to native account abstraction), they can initiate transactions and include arbitrary logic, such as batching multiple actions. This system enhances the user experience by enabling complex interactions in a streamlined and customizable manner. ================ File: src/pages/protocol/concepts/app.mdx ================ export const meta = { showBreadcrumbs: true, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; import ConceptsAppIllustration from "@/components/mdx/components/concepts/ConceptIllustrations/ConceptsAppIllustration"; export default ({ children }) => {children}; {/* Start of the page content */} # App An App on Lens is an onchain entity with a distinct identity and customizable settings. Developers can set metadata that describes the app, including images and descriptions, enhancing its presence within the network. Apps can specify which graphs they use, effectively defining the social network or audience they cater to. They can also manage feeds by adding multiple feeds and setting a default, allowing for organized content distribution tailored to their users. Moreover, apps can determine the namespace registries they utilize, ensuring consistent user identities within their platform. Administrative control is robust, with options to add or remove admins who can adjust app settings. To enhance user experience, apps can set up sponsorships that cover gas fees for users, facilitating seamless interaction. They can also associate with specific groups, integrating communal features and fostering engagement. ================ File: src/pages/protocol/concepts/feed.mdx ================ export const meta = { showBreadcrumbs: true, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; import ConceptsFeedIllustration from "@/components/mdx/components/concepts/ConceptIllustrations/ConceptsFeedIllustration"; export default ({ children }) => {children}; {/* Start of the page content */} # Feed A Feed is an onchain contract that serves as a customizable channel for content posting and sharing within Lens. Feeds can be global or specific to an app or group, and apps can deploy multiple feeds to cater to different content types or audiences. Each feed allows accounts to create posts and share others' content, with the ability to set rules that control who can post based on criteria like token ownership or other custom conditions. Administrators can manage feed settings, including adding or removing admins and setting metadata to describe the feed's purpose. The modular rule system enables the creation of tailored experiences—for example, a feed where only verified users can post. Posts within feeds can have Actions attached, such as collect actions that allow users to collect content under specific conditions (paid, free, limited editions or any other smart contract based Action.). This design provides a flexible and controlled environment for content distribution and interaction. ================ File: src/pages/protocol/concepts/graph.mdx ================ export const meta = { showBreadcrumbs: true, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; import ConceptsGraphIllustration from "@/components/mdx/components/concepts/ConceptIllustrations/ConceptsGraphIllustration"; export default ({ children }) => {children}; {/* Start of the page content */} # Graph The Graph represents the network of connections between accounts, such as follows and unfollows, within the Lens protocol. Both global and app-level graphs can be deployed as onchain contracts, allowing applications to build and manage their own audience. This enables apps to create tailored social experiences, defining how users interact within their platform. Administrators can add custom rules to their graphs using the modular rule system. For example, they might require users to own a specific NFT to establish connections within the graph. Metadata settings help describe the graph's purpose and context. Additionally, accounts can set personal graph rules, adding another layer of customization and control over their social interactions. ================ File: src/pages/protocol/concepts/group.mdx ================ export const meta = { showBreadcrumbs: true, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; import ConceptsGroupIllustration from "@/components/mdx/components/concepts/ConceptIllustrations/ConceptsGroupIllustration"; export default ({ children }) => {children}; {/* Start of the page content */} # Group The Group feature on Lens enables the creation of onchain communities with customizable rules and governance. Each group is a smart contract that can be linked to applications, allowing for tight integration between user groups and the platforms they interact with. Groups can set join rules using the modular rule system—for instance, requiring members to own a certain token or NFT, or to pay a fee to join. This flexibility allows groups to tailor their membership criteria to their specific needs. Members can participate in dedicated group feeds, fostering focused discussions and content sharing. Admins have control over membership, including the ability to ban or remove members, and can set up group-specific usernames and even associate an ERC20 token with the group. Metadata settings allow communities to define their purpose and guidelines clearly, enhancing transparency and cohesion among members. ================ File: src/pages/protocol/concepts/rules.mdx ================ export const meta = { showBreadcrumbs: true, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; import ConceptsRulesIllustration from "@/components/mdx/components/concepts/ConceptIllustrations/ConceptsRulesIllustration"; export default ({ children }) => {children}; {/* Start of the page content */} # Rules Rules are modular smart contracts that define custom logic and constraints across various elements like Graphs, Feeds, Usernames, and Groups within Lens. They enable developers to enforce specific conditions—for example, restricting access to a feed to users who own a particular NFT or token. This modularity allows for a high degree of customization without altering the core protocol, facilitating innovative use cases and tailored experiences within the network. By applying rules, developers and administrators can control interactions and access, ensuring that their platforms operate according to their desired parameters. Whether it's gating content, managing membership criteria, or setting transactional conditions, rules provide the flexibility to implement complex logic in a straightforward and maintainable way. Rules can be applied to Groups, Feeds, Graphs, and Usernames, allowing for a wide range of use cases and configurations. ================ File: src/pages/protocol/concepts/sponsorship.mdx ================ export const meta = { showBreadcrumbs: true, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; import ConceptsSponsorshipIllustration from "@/components/mdx/components/concepts/ConceptIllustrations/ConceptsSponsorshipIllustration"; export default ({ children }) => {children}; {/* Start of the page content */} # Sponsorship Sponsorships on Lens allow apps or entities to cover gas fees on behalf of users, enhancing accessibility and user experience. Leveraging the concept of Paymasters from native account abstraction, sponsors deploy paymaster contracts that can sponsor transactions for users, enabling them to pay transaction fees. This innovative approach eliminates the friction associated with transaction costs, particularly for new users unfamiliar with blockchain mechanics. Sponsors can set rate limits, access control lists, and exclusion lists to manage how and when users benefit from sponsored transactions. This includes defining daily or monthly limits and requiring backend validation for added security. Sponsorships are linked to specific apps and gives the apps full control of who they sponsor. Administrators have full control over sponsorship settings, including pausing or unpausing the service and adding metadata to describe the sponsorship. By significantly enhancing user experience, sponsorships pave the way for broader adoption of blockchain technology within the Lens ecosystem. ================ File: src/pages/protocol/concepts/username.mdx ================ export const meta = { showBreadcrumbs: true, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; import ConceptsUsernameIllustration from "@/components/mdx/components/concepts/ConceptIllustrations/ConceptsUsernameIllustration"; export default ({ children }) => {children}; {/* Start of the page content */} # Username The Username system on Lens offers a flexible and customizable approach to user identities. While there is a global namespace like "lens", applications can deploy their own namespaces, such as app/username, allowing them to create unique username ecosystems and even generate revenue. This hierarchical structure enables apps to align usernames with their brand and community, fostering a more personalized user experience. Usernames are deployed as individual contracts, which are tokenized for tradable access. Developers can set minting rules such as charging a fee or requiring ownership of a specific NFT—using the modular rule system. Additional features include defining minimum and maximum username lengths, setting secondary royalties compliant with EIP-2981, and managing admin access. This comprehensive system provides both flexibility and control over how usernames are created and managed within the network. ================ File: src/pages/protocol/feeds/bookmarks.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Bookmarks This guide explain how to create Account's private bookmarks. --- The bookmarks feature allows a user to save references to posts. The list is private to the authenticated Account, hence the owner and ANY Account Manager can access the list. This feature is provided by the Lens API as a convenience to the user. The bookmarks are stored off-chain, so they are instant and do not require signatures or gas to use. ## Add to Bookmarks You MUST be authenticated as Account Owner or Account Manager to make this request. Use the `bookmarkPost` action to add a post to the Account's bookmarks list. ```ts filename="AddBookmark" import { postId } from "@lens-protocol/client"; import { bookmarkPost } from "@lens-protocol/client/actions"; const result = await bookmarkPost(sessionClient, { post: postId("01234…"), }); if (result.isErr()) { return console.error(result.error); } ``` Use the `bookmarkPost` mutation to add a post to the Account's bookmarks list. ```graphql filename="AddBookmark.graphql" mutation { bookmarkPost(request: { post: "42" }) } ``` Coming soon That's it—the post is now saved to the Account's bookmarks list. ## List Bookmarks You MUST be authenticated as Account Owner or Account Manager to make this request. Use the paginated `fetchPostBookmarks` action to list the Account's bookmarks. ```ts filename="Any Feed" import { fetchPostBookmarks } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchPostBookmarks(client); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="Global Feed" import { evmAddress } from "@lens-protocol/client"; import { fetchPostBookmarks } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchPostBookmarks(client, { filter: { feeds: [ { globalFeed: true, }, ], }, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="Custom Feeds" import { evmAddress } from "@lens-protocol/client"; import { fetchPostBookmarks } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchPostBookmarks(client, { filter: { feeds: [ { feed: evmAddress("0x5678…"), }, ], }, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="All App Feeds" import { evmAddress } from "@lens-protocol/client"; import { fetchPostBookmarks } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchPostBookmarks(client, { filter: { feeds: [ { app: evmAddress("0x9123…"), }, ], }, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="Other Filters" import { ContentWarning, MainContentFocus } from "@lens-protocol/client"; import { fetchPostBookmarks } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchPostBookmarks(client, { filter: { metadata: { contentWarning: { oneOf: [ContentWarning.Sensitive] }, mainContentFocus: [MainContentFocus.Image], tags: { all: ["tagExample"] }, }, }, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; export const client = PublicClient.create({ environment: mainnet, }); ``` See the [Pagination](../best-practices/pagination) guide for more information on how to handle paginated results. Use the paginated `postBookmarks` query to list the Account's bookmarks. ```graphql filename="Query" query { postBookmarks( request: { # optional filter: { # optional, filter by feeds (by default, all feeds are included) feeds: [ # optional, filter by global feed { globalFeed: true } # and/or, filter by feed address # { # feed: EvmAddress # } # and/or, filter by ALL feeds associated w/ an app address # { # app: EvmAddress # } ] # optional, filter by metadata metadata: { mainContentFocus: [IMAGE] # optional, filter by tags # tags: PostMetadataTagsFilter # optional, filter by content warning # contentWarning: PostMetadataContentWarningFilter } } } ) { items { ... on Post { ...Post } ... on Repost { ...Repost } } pageInfo { prev next } } } ``` ```json filename="Response" { "data": { "postBookmarks": { "items": [ { "id": "42", "author": { "address": "0x1234…", "username": { "value": "lens/wagmi" }, "metadata": { "name": "WAGMI", "picture": "https://example.com/wagmi.jpg" } }, "timestamp": "2022-01-01T00:00:00Z", "metadata": { "image": { "item": "lens://4f91cab87ab5e4f5066f878b72…" } } } ], "pageInfo": { "prev": null, "next": null } } } } ``` See the [Pagination](../best-practices/pagination) guide for more information on how to handle paginated results. Coming soon ## Remove from Bookmarks You MUST be authenticated as Account Owner or Account Manager to make this request. Use the `undoBookmarkPost` action to remove a post from the Account's bookmarks list. ```ts filename="UndoBookmark" import { postId } from "@lens-protocol/client"; import { undoBookmarkPost } from "@lens-protocol/client/actions"; const result = await undoBookmarkPost(sessionClient, { post: postId("01234…"), }); if (result.isErr()) { return console.error(result.error); } ``` Use the `undoBookmarkPost` mutation to remove a post from the Account's bookmarks list. ```graphql filename="RemoveBookmark.graphql" mutation { undoBookmarkPost(request: { post: "42" }) } ``` Coming soon That's it—the post is now removed from the Account's bookmarks list. ================ File: src/pages/protocol/feeds/boost-engagement.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Boost Engagement This guide will show you the essential signal-boosting features on Lens. --- ## Reposting Users can repost any Post to their own Feed, another Feed, or even the Global Lens Feed. Reposting is an effective way for users to amplify content they find valuable and share it with their followers. You MUST be authenticated as Account Owner or Account Manager to repost content on Lens. ### Check Post Rules First, inspect the `post.operations.canRepost` field to determine whether the logged-in Account is allowed to repost it. Some posts may have restrictions on who can repost them. ```ts filename="Check Rules" switch (post.operations.canRepost.__typename) { case "PostOperationValidationPassed": // Reposting is allowed break; case "PostOperationValidationFailed": // Reposting is not allowed console.log(post.operations.canRepost.reason); break; case "PostOperationValidationUnknown": // Validation outcome is unknown break; } ``` Where: - `PostOperationValidationPassed`: The logged-in Account can repost the Post. - `PostOperationValidationFailed`: Reposting is not allowed. The `reason` field explains why, and `unsatisfiedRules` lists the unmet requirements. - `PostOperationValidationUnknown`: The Post or its Feed (for custom Feeds) has one or more _unknown rules_ requiring ad-hoc verification. The `extraChecksRequired` field provides the addresses and configurations of these rules. Treat the `PostOperationValidationUnknown` as _failed_ unless you intend to support the specific rules. See [Post Rules](./post-rules) for more information. ### Repost a Post Next, if the Post allows reposting, you can proceed to repost it. You MUST repost on the same Feed as the original Post. Cross-feed quoting is currently not supported. If you find this feature valuable, please let us know by [opening an issue](https://github.com/lens-protocol/lens-sdk/issues). Use the `repost` action to repost on Lens. ```ts filename="Repost on Global Feed" import { postId } from "@lens-protocol/client"; import { repost } from "@lens-protocol/client/actions"; const result = await repost(sessionClient, { post: postId("42"), }); if (result.isErr()) { return console.error(result.error); } ``` ```ts filename="Repost on Custom Feed" import { postId } from "@lens-protocol/client"; import { repost } from "@lens-protocol/client/actions"; const result = await repost(sessionClient, { post: postId("42"), feed: evmAddress("0xabc123…"), }); if (result.isErr()) { return console.error(result.error); } ``` Use the `repost` mutation to repost on Lens. ```graphql filename="Repost on Global Feed" mutation { repost(request: { post: "42" }) { ... on PostResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on PostOperationValidationFailed { reason } ... on TransactionWillFail { reason } } } ``` ```graphql filename="Repost on Custom Feed" mutation { repost(request: { post: "42", feed: "0xabc123…" }) { ... on PostResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on PostOperationValidationFailed { reason } ... on TransactionWillFail { reason } } } ``` ```json filename="PostResponse" { "data": { "repost": { "hash": "0x…" } } } ``` Coming soon ### Handle Result Then, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,7" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await repost(sessionClient, { post: postId("42"), }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,7" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await repost(sessionClient, { post: postId("42"), }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Then, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Coming soon ## Reactions Reactions let users express their opinion on a Post, providing immediate feedback to the Post's author on how their content resonates with the audience. Currently, reactions are not stored on-chain. We plan to implement a decentralized solution in the future that maintains a smooth user experience while aligning with the web3 ethos. ### Add a Reaction At present, users can react to a Post with an upvote or downvote. In the future, more reaction options may be introduced to offer a wider range of engagement possibilities. You MUST be authenticated as Account Owner or Account Manager to make this request. Use the `addReaction` action to add a reaction to a Post. ```ts filename="Add Reaction" import { PostReactionType, postId } from "@lens-protocol/client"; import { addReaction } from "@lens-protocol/client/actions"; const result = await addReaction(sessionClient, { post: postId("42"), reaction: , PostReactionType.Upvote // or Downvote }); if (result.isErr()) { return console.error(result.error); } // Boolean indicating success adding the reaction const success = result.value; ``` Use the `addReaction` mutation to add a reaction to a Post. ```graphql filename="Mutation" mutation { addReaction( request: { post: "42" reaction: UPVOTE # or DOWNVOTE } ) { ... on AddReactionResponse { success } ... on AddReactionFailure { reason } } } ``` ```json filename="AddReactionResponse" { "data": { "addReaction": { "success": true } } } ``` Coming soon ### Undo a Reaction Users can undo their reaction to a Post at any time. You MUST be authenticated as Account Owner or Account Manager to make this request. Use the `undoReaction` action to remove a reaction from a Post. ```ts filename="Undo Reaction" import { PostReactionType, postId } from "@lens-protocol/client"; import { undoReaction } from "@lens-protocol/client/actions"; const result = await undoReaction(sessionClient, { post: postId("42"), reaction: , PostReactionType.Upvote // or Downvote }); if (result.isErr()) { return console.error(result.error); } // Boolean indicating success adding the reaction const success = result.value; ``` Use the `undoReaction` mutation to remove a reaction from a Post. ```graphql filename="Mutation" mutation { undoReaction( request: { post: "42" reaction: DOWNVOTE # or UPVOTE } ) { ... on UndoReactionResponse { success } ... on UndoReactionFailure { reason } } } ``` ```json filename="UndoReactionResponse" { "data": { "undoReaction": { "success": true } } } ``` Coming soon ### Fetch Reactions Use the paginated `fetchPostReactions` action to fetch the reactions for a Post. ```ts filename="All Reactions" import { postId } from "@lens-protocol/client"; import { fetchPostReactions } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchPostReactions(client, { post: postId("1234…"), }); if (result.isErr()) { return console.error(result.error); } // items: Array: [{account: Account, reactions: PostReaction}, …] const { items, pageInfo } = result.value; ``` ```ts filename="Filter By Reaction" import { PostReactionType, postId } from "@lens-protocol/client"; import { fetchPostReactions } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchPostReactions(client, { app: postId("1234…"), filter: { anyOf: [PostReactionType.Upvote] }, }); if (result.isErr()) { return console.error(result.error); } // items: Array: [{account: Account, reactions: PostReaction}, …] const { items, pageInfo } = result.value; ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; export const client = PublicClient.create({ environment: mainnet, }); ``` ```graphql filename="Query" mutation { postReactions( request: { post: "42" # filter: { anyOf: [UPVOTE] } # Optional # orderBy: DEFAULT | ACCOUNT_SCORE } ) { items { reactions { reaction reactedAt } account { address username { value } metadata { name picture } } } pageInfo { prev next } } } ``` ```json filename="Response" { "data": { "postReactions": { "items": [ { "reactions": [ { "reaction": "UPVOTE", "reactedAt": "2021-09-01T00:00:00Z" } ], "account": { "address": "0x1234…", "username": { "value": "lens/bob" }, "metadata": { "name": "Bob", "picture": "https://example.com/bob.jpg" } } } ], "pageInfo": { "prev": null, "next": null } } } } ``` Coming soon See the [Pagination](../best-practices/pagination) guide for more information on how to handle paginated results. ================ File: src/pages/protocol/feeds/delete-post.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Delete a Post This guide will help you delete a post from a Lens Feed. --- To delete a Post on Lens, follow these steps. You MUST be authenticated as Account Owner or Account Manager for the post your are trying to delete. ## Check Post Rules First, inspect the `post.operations.canDelete` field to determine whether the logged-in Account is allowed to edit the Post. Some posts may have restrictions on who can ```ts filename="Check Rules" switch (post.operations.canDelete.__typename) { case "PostOperationValidationPassed": // Commenting is allowed break; case "PostOperationValidationFailed": // Commenting is not allowed console.log(post.operations.canEdit.reason); break; case "PostOperationValidationUnknown": // Validation outcome is unknown break; } ``` Where: - `PostOperationValidationPassed`: The logged-in Account is allowed delete the Post. - `PostOperationValidationFailed`: Deleting is not allowed. The `reason` field explains why, and `unsatisfiedRules` lists the unmet requirements. - `PostOperationValidationUnknown`: The Post or its Feed (for custom Feeds) has one or more _unknown rules_ requiring ad-hoc verification. The `extraChecksRequired` field provides the addresses and configurations of these rules. Treat the `PostOperationValidationUnknown` as _failed_ unless you intend to support the specific rules. See [Post Rules](./post-rules) for more information. ## Delete the Post Next, if allowed, delete the Post. Bear in mind that the trail of the Post existance will remain as part of the blockchain history. Use the `deletePost` action to submit/create a delete transaction. ```ts import { postId } from "@lens-protocol/client"; import { deletePost } from "@lens-protocol/client/actions"; const result = await deletePost(sessionClient, { post: postId("01234…"), }); if (result.isErr()) { return console.error(result.error); } ``` Use the `deletePost` mutation to submit/create a delete transaction. ```graphql filename="Mutation" mutation { deletePost( request: { post: "42" # Optional, any data required by custom Feed Rule # feedRules: FeedRulesInput } ) { ... on DeletePostResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ```json filename="DeletePostResponse" { "data": { "deletePost": { "hash": "0x5e9d8f8a4b8e4b8e4b8e4b8e4b8e4b8e4b8e4b" } } } ``` Coming soon ## Handle Result Next, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,7" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await deletePost(sessionClient, { post: postId("01234…"), }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,7" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await deletePost(sessionClient, { post: postId("01234…"), }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Next, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Coming soon ## Delete the Content Finally, to delete a Post's content, you must remove the Post Metadata object located at `post.contentURI`, along with any referenced files. The deletion process depends on the hosting solution used when the Post was created or last updated. For example, if IPFS was used, you should unpin the content to free up resources. If you used [Grove storage](../../storage), the result of the first step will determine your next steps: - If you received a `DeletePostResponse`, the Lens API has already handled the content deletion for you, and no further action is required. - If you received a `SponsoredTransactionRequest` or `SelfFundedTransactionRequest`, you will need to delete the content by calling the [Grove storage API](../../storage/usage/delete). ================ File: src/pages/protocol/feeds/edit-post.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Edit a Post This guide explains how to edit a Post content on Lens. --- To update a Post on Lens, follow these steps. You MUST be authenticated as Account Owner or Account Manager for the post your are trying to edit. ## Check Post Rules First, inspect the `post.operations.canEdit` field to determine whether the logged-in Account is allowed to edit the Post. Some posts may have restrictions on who can ```ts filename="Check Rules" switch (post.operations.canEdit.__typename) { case "PostOperationValidationPassed": // Commenting is allowed break; case "PostOperationValidationFailed": // Commenting is not allowed console.log(post.operations.canEdit.reason); break; case "PostOperationValidationUnknown": // Validation outcome is unknown break; } ``` Where: - `PostOperationValidationPassed`: The logged-in Account is allowed to edit the Post. - `PostOperationValidationFailed`: Editing is not allowed. The `reason` field explains why, and `unsatisfiedRules` lists the unmet requirements. - `PostOperationValidationUnknown`: The Post or its Feed (for custom Feeds) has one or more _unknown rules_ requiring ad-hoc verification. The `extraChecksRequired` field provides the addresses and configurations of these rules. Treat the `PostOperationValidationUnknown` as _failed_ unless you intend to support the specific rules. See [Post Rules](./post-rules) for more information. ## Create Post Metadata Next, if allowed, continue with creating a new Post Metadata object with the updated details. It's developer responsability to copy over any existing data that should be retained. The process is similar to the one in the [Create a Post](./post) guide, so we will keep this example brief. ```ts filename="Text-only" import { textOnly } from "@lens-protocol/metadata"; const metadata = textOnly({ content: `GM! GM!`, }); ``` ## Upload Metadata Next, upload the Post Metadata object to a public URI. If the hosting solution used when the Post was created or last updated allows edits, you may want to choose between: - Keeping a history of the file, like a document revision, by uploading it to a new URI. - Erasing the previous version by updating the content at the same URI. ```ts import { textOnly } from "@lens-protocol/metadata"; import { storageClient } from "./storage-client"; const metadata = textOnly({ content: `GM! GM!`, }); const { uri } = await storageClient.uploadAsJson(metadata); console.log(uri); // e.g., lens://4f91ca… ``` If [Grove storage](../../storage) was used you can decide if also metadata can be edited and delete as needed. See the [Editing Content](../../storage/usage/edit) and [Deleting Content](../../storage/usage/delete) guides for more information. ## Update Post Content URI Next, update the Post content URI with the new URI. Use the `editPost` action to update the Post content URI. ```ts import { postId, uri } from "@lens-protocol/client"; import { editPost } from "@lens-protocol/client/actions"; const result = await editPost(sessionClient, { contentUri: uri("lens://4f91ca…"), post: postId("42"), }); if (result.isErr()) { return console.error(result.error); } ``` Use the `editPost` mutation to update the Post content URI. ```graphql filename="Mutation" mutation { editPost(request: { post: "42", contentUri: "lens://4f91ca…" }) { ... on PostResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on PostOperationValidationFailed { reason } ... on TransactionWillFail { reason } } } ``` ```json filename="PostResponse" { "data": { "editPost": { "hash": "0x…" } } } ``` Coming soon ## Handle Result Finally, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await editPost(sessionClient, { contentUri: uri("lens://4f91ca…"), post: postId("01234…"), }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await editPost(sessionClient, { contentUri: uri("lens://4f91ca…"), post: postId("01234…"), }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Finally, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Coming soon ================ File: src/pages/protocol/feeds/feed-rules.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Feed Rules This guide explains how to use Feed Rules and how to implement custom ones. --- Feed Rules allow administrators to add requirements or constraints when creating content on a Feed. ## Using Feed Rules Lens provides four built-in Feed rules: - `SimplePaymentFeedRule` - Requires an ERC-20 payment to post on the Feed. - `TokenGatedFeedRule` - Requires an account to hold a certain token to post on the Feed. - `GroupGatedFeedRule` - Requires an account to be a member of a certain Group to post on the Feed. For the `SimplePaymentFeedRule`, a **1.5%** Lens treasury fee is deducted from the payment before the remaining amount is transferred to the designated recipient. It is also possible to use custom Feed Rules to extend the functionality of your Feed. ### Create a Feed with Rules As part of creating [Custom Feeds](./custom-feeds), you can pass a `rules` object that defines the `required` rules and/or an `anyOf` set, where satisfying any one rule allows posting on the Feed. These rules can be built-in or custom. This section presumes you are familiar with the process of [creating a Feed](./custom-feeds) on Lens. ```ts filename="SimplePaymentFeedRule" import { bigDecimal, evmAddress, uri } from "@lens-protocol/client"; import { createFeed } from "@lens-protocol/client/actions"; const result = await createFeed(sessionClient, { metadataUri: uri("lens://4f91ca…"), rules: { required: [ { simplePaymentRule: { cost: { currency: evmAddress("0x5678…"), value: bigDecimal("10.42"), }, recipient: evmAddress("0x9012…"), }, }, ], }, }); ``` ```ts filename="TokenGatedFeedRule" import { bigDecimal, evmAddress, TokenStandard, uri, } from "@lens-protocol/client"; import { createFeed } from "@lens-protocol/client/actions"; const result = await createFeed(sessionClient, { metadataUri: uri("lens://4f91ca…"), rules: { required: [ { tokenGatedRule: { token: { currency: evmAddress("0x1234…"), standard: TokenStandard.Erc721, value: bigDecimal("1"), }, }, }, ], }, }); ``` ```ts filename="GroupGatedFeedRule" import { evmAddress, TokenStandard, uri } from "@lens-protocol/client"; import { createFeed } from "@lens-protocol/client/actions"; const result = await createFeed(sessionClient, { metadataUri: uri("lens://4f91ca…"), rules: { required: [ { groupGatedRule: { group: evmAddress("0x1234…"), }, }, ], }, }); ``` ```ts filename="Custom Feed Rule" import { blockchainData, evmAddress, FeedRuleExecuteOn, uri, } from "@lens-protocol/client"; const result = await createFeed(sessionClient, { metadataUri: uri("lens://4f91ca…"), rules: { required: [ { unknownRule: { address: evmAddress("0x1234…"), executeOn: [FeedRuleExecuteOn.CreatingFeed], params: [ { raw: { // 32 bytes key (e.g., keccak(name)) key: blockchainData("0xac5f04…"), // an ABI encoded value data: blockchainData("0x00…"), }, }, ], }, }, ], }, }); ``` ```graphql filename="SimplePaymentFeedRule" mutation { createFeed( request: { metadataUri: "lens://4f91cab87ab5e4f5066f878b72…" rules: { required: [ { simplePaymentRule: { cost: { currency: "0x5678…", value: "10.42" } recipient: "0x9012…" } } ] } } ) { ... on CreateFeedResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ```graphql filename="TokenGatedFeedRule" mutation { createFeed( request: { metadataUri: "lens://4f91cab87ab5e4f5066f878b72…" rules: { required: [ { tokenGatedRule: { token: { currency: "0x1234…", standard: ERC721, value: "1" } } } ] } } ) { ... on CreateFeedResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ```graphql filename="GroupGatedGraphRule" mutation { createFeed( request: { metadataUri: "lens://4f91cab87ab5e4f5066f878b72…" rules: { required: [{ groupGatedRule: { group: "0x1234…" } }] } } ) { ... on CreateFeedResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ```graphql filename="Custom Feed Rule" mutation { createFeed( request: { metadataUri: "lens://4f91cab87ab5e4f5066f878b72…" rules: { required: [ { unknownRule: { address: "0x1234…" params: [{ raw: { key: "0x4f…", data: "0x00" } }] } } ] } } ) { ... on CreateFeedResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` Coming soon ### Update a Feed Rules To update a Feed rules configuration, follow these steps. You MUST be authenticated as [Builder](../authentication), [Account Manager](../authentication), or [Account Owner](../authentication) and be either the owner or an admin of the Feed you intend to configure. #### Identify Current Rules First, inspect the `feed.rules` field to know the current rules configuration. ```ts filename="FeedRules" type FeedRules = { required: FeedRule; anyOf: FeedRule; }; ``` ```ts filename="FeedRule" type FeedRule = { id: RuleId; type: FeedRuleType; address: EvmAddress; executesOn: FeedRuleExecuteOn[]; config: AnyKeyValue[]; }; ``` ```ts filename="AnyKeyValue" type AnyKeyValue = | IntKeyValue | IntNullableKeyValue | AddressKeyValue | StringKeyValue | BooleanKeyValue | RawKeyValue | BigDecimalKeyValue | DictionaryKeyValue | ArrayKeyValue; ``` ```ts filename="ArrayKeyValue" type ArrayKeyValue = { __typename: "ArrayKeyValue"; key: string; array: | IntKeyValue | IntNullableKeyValue | AddressKeyValue | StringKeyValue | BooleanKeyValue | RawKeyValue | BigDecimalKeyValue | DictionaryKeyValue; }; ``` ```ts filename="DictionaryKeyValue" type DictionaryKeyValue = { __typename: "DictionaryKeyValue"; key: string; dictionary: | IntKeyValue | IntNullableKeyValue | AddressKeyValue | StringKeyValue | BooleanKeyValue | RawKeyValue | BigDecimalKeyValue; }; ``` ```ts filename="Others" type IntKeyValue = { __typename: "IntKeyValue"; key: string; int: number; }; type IntNullableKeyValue = { __typename: "IntNullableKeyValue"; key: string; optionalInt: number | null; }; type AddressKeyValue = { __typename: "AddressKeyValue"; key: string; address: EvmAddress; }; type StringKeyValue = { __typename: "StringKeyValue"; key: string; string: string; }; type BooleanKeyValue = { __typename: "BooleanKeyValue"; key: string; boolean: boolean; }; type RawKeyValue = { __typename: "RawKeyValue"; key: string; data: BlockchainData; }; type BigDecimalKeyValue = { __typename: "BigDecimalKeyValue"; key: string; bigDecimal: BigDecimal; }; ``` ```graphql filename="FeedRules" type FeedRules { required: [FeedRule!]! anyOf: [FeedRule!]! } ``` ```graphql filename="FeedRule" type FeedRule { id: RuleId! type: FeedRuleType! address: EvmAddress! executesOn: [FeedRuleExecuteOn!]! config: [AnyKeyValue!]! } ``` ```graphql filename="AnyKeyValue" fragment AnyKeyValue on AnyKeyValue { ... on IntKeyValue { ...IntKeyValue } ... on IntNullableKeyValue { ...IntNullableKeyValue } ... on AddressKeyValue { ...AddressKeyValue } ... on StringKeyValue { ...StringKeyValue } ... on BooleanKeyValue { ...BooleanKeyValue } ... on RawKeyValue { ...RawKeyValue } ... on BigDecimalKeyValue { ...BigDecimalKeyValue } ... on DictionaryKeyValue { ...DictionaryKeyValue } ... on ArrayKeyValue { ...ArrayKeyValue } } ``` ```graphql filename="ArrayKeyValue" fragment ArrayKeyValue on ArrayKeyValue { __typename key array { ... on IntKeyValue { ...IntKeyValue } ... on IntNullableKeyValue { ...IntNullableKeyValue } ... on AddressKeyValue { ...AddressKeyValue } ... on StringKeyValue { ...StringKeyValue } ... on BooleanKeyValue { ...BooleanKeyValue } ... on RawKeyValue { ...RawKeyValue } ... on BigDecimalKeyValue { ...BigDecimalKeyValue } ... on DictionaryKeyValue { ...DictionaryKeyValue } } } ``` ```graphql filename="DictionaryKeyValue" fragment DictionaryKeyValue on DictionaryKeyValue { __typename key dictionary { ... on IntKeyValue { ...IntKeyValue } ... on IntNullableKeyValue { ...IntNullableKeyValue } ... on AddressKeyValue { ...AddressKeyValue } ... on StringKeyValue { ...StringKeyValue } ... on BooleanKeyValue { ...BooleanKeyValue } ... on RawKeyValue { ...RawKeyValue } ... on BigDecimalKeyValue { ...BigDecimalKeyValue } } } ``` ```graphql filename="Others" fragment IntKeyValue on IntKeyValue { __typename key int } fragment IntNullableKeyValue on IntNullableKeyValue { __typename key optionalInt } fragment AddressKeyValue on AddressKeyValue { __typename key address } fragment StringKeyValue on StringKeyValue { __typename key string } fragment BooleanKeyValue on BooleanKeyValue { __typename key boolean } fragment RawKeyValue on RawKeyValue { __typename key data } fragment BigDecimalKeyValue on BigDecimalKeyValue { __typename key bigDecimal } ``` The configuration for the built-in rules with one or more parameters is as follows. | Key | Type | Description | | --------------- | ------------ | ------------------------------------- | | `assetContract` | `EvmAddress` | Address of the ERC-20 token contract. | | `assetName` | `String` | Name of the ERC-20 token. | | `assetSymbol` | `String` | Symbol of the ERC-20 token. | | `amount` | `BigDecimal` | Payment required to post. | | Key | Type | Description | | --------------- | ------------ | ------------------------------------------ | | `assetContract` | `EvmAddress` | Address of the token contract. | | `assetName` | `String` | Name of the token. | | `assetSymbol` | `String` | Symbol of the token. | | `amount` | `BigDecimal` | Minimum number of tokens required to post. | | Key | Type | Description | | ------------------------ | ------------ | ----------------------------------------- | | `groupAddress` | `EvmAddress` | The Group contract address. | | `groupName` | `String` | The Group name. | | `groupRepliesRestricted` | `Boolean` | Wheter replies are restricted to members. | Keep note of the Rule IDs you might want to remove. #### Update the Rules Configuration Next, update the rules configuration of the Feed as follows. Use the `updateFeedRules` action to update the rules configuration of a given feed. ```ts filename="Add Rules" import { bigDecimal, evmAddress } from "@lens-protocol/client"; import { updateFeedRules } from "@lens-protocol/client/actions"; const result = await updateFeedRules(sessionClient, { feed: feed.address, toAdd: { required: [ { tokenGatedRule: { token: { standard: TokenStandard.Erc20, currency: evmAddress("0x5678…"), value: bigDecimal("1.5"), // Token value in its main unit }, }, }, ], }, }); ``` ```ts filename="Remove Rules" import { updateFeedRules } from "@lens-protocol/client/actions"; const result = await updateFeedRules(sessionClient, { feed: feed.address, toRemove: [feed.rules.required[0].id], }); ``` Use the `updateFeedRules` mutation to update the rules configuration of a given feed. ```graphql filename="Add Rules" mutation { updateFeedRules( request: { feed: "0x1234…" toAdd: { required: [ { tokenGatedRule: { token: { standard: ERC20, currency: "0x5678…", value: "1.5" } } } ] } } ) { ... on UpdateFeedRulesResponse { ...UpdateFeedRulesResponse } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ```graphql filename="Remove Rules" mutation { updateFeedRules(request: { feed: "0x1234…", toRemove: ["ej6g…"] }) { ... on UpdateFeedRulesResponse { ...UpdateFeedRulesResponse } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` #### Handle Result Then, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await updateFeedRules(sessionClient, { feed: evmAddress("0x1234…"), // … }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await updateFeedRules(sessionClient, { feed: evmAddress("0x1234…"), // … }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Then, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. --- ## Building a Feed Rule Let's illustrate the process with an example. We will build the `GroupGatedFeedRule` described above, a rule that requires accounts to be a member of a certain Group in order to Create a Post on the Feed. To build a custom Feed Rule, you must implement the following `IFeedRule` interface: ```solidity interface IFeedRule { function configure(bytes32 configSalt, KeyValue[] calldata ruleParams) external; function processCreatePost( bytes32 configSalt, uint256 postId, CreatePostParams calldata postParams, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external; function processEditPost( bytes32 configSalt, uint256 postId, EditPostParams calldata postParams, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external; function processDeletePost( bytes32 configSalt, uint256 postId, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external; function processPostRuleChanges( bytes32 configSalt, uint256 postId, RuleChange[] calldata ruleChanges, KeyValue[] calldata ruleParams ) external; } ``` Each function of this interface must assume to be invoked by the Feed contract. In other words, assume the `msg.sender` will be the Feed contract. A Lens dependency package with all relevant interfaces will be available soon. ### Implement the Configure Function First, implement the `configure` function. This function has the purpose of initializing any required state for the rule to work properly. It receives two parameters, a 32-byte configuration salt (`configSalt`), and an array of custom parameters as key-value pairs (`ruleParams`). The `configSalt` is there to allow the same rule contract to be used many times, with different configurations, for the same Feed. So, for a given Feed Rule implementation, the pair (Feed Address, Configuration Salt) should identify a rule configuration. For example, we could want to achieve the restriction "To post on this Feed, you must be a member of Group A or Group B". In that case, instead of writing a whole new contract that receives two groups instead of one, we would just configure the `GroupGatedFeedRule` rule twice in the same Feed, once for Group A with some configuration salt, and once for Group B with another configuration salt. The `configure` function can be called multiple times by the same Feed passing the same configuration salt in order to update that rule configuration (i.e. reconfigure it). The `ruleParams` is an array of key-value pairs that can be used to pass any custom configuration parameters to the rule. Each key is `bytes32`, we put the hash of the parameter name there, and each value is `bytes`, we set the ABI-encoded parameter value there. Given that `ruleParams` is an array, this allows the rule to define which parameters are optional and which are required, acting accordingly when any of them are not present. In our example, we need to decode an address parameter, which will represent the Group contract where the account trying to post must belong to. Let's define a storage mapping to store this configuration: ```solidity contract GroupGatedFeedRule is IFeedRule { mapping(address feed => mapping(bytes32 configSalt => address group)) internal _groupGate; } ``` The configuration is stored in the mapping using the Feed contract address and the configuration salt as keys. With this setup, the same rule can be used by different Feeds, as well as be used by the same Feed many times. Now let's code the `configure` function itself, decoding the required address parameter and storing it in the mapping: ```solidity contract GroupGatedFeedRule is IFeedRule { mapping(address feed => mapping(bytes32 configSalt => address group)) internal _groupGate; function configure(bytes32 configSalt, KeyValue[] calldata ruleParams) external override { address group = address(0); for (uint256 i = 0; i < ruleParams.length; i++) { if (ruleParams[i].key == keccak256("lens.param.group")) { group = abi.decode(ruleParams[i].value, (address)); break; } } // Aims to check if the passed group contract is valid IGroup(group).isMember(address(this)); _groupGate[msg.sender][configSalt] = group; } } ``` ### Implement the Process Create Post function Next, implement the `processCreatePost` function. This function is invoked by the Feed contract every time a Post is trying to be created, so then our custom logic can be applied to shape under which conditions this operation can succeed. The function receives the configuration salt (`configSalt`), so we know which configuration to use, the ID the Feed assigned to the Post (`postId`), the parameters of the Post (`postParams`, including things like author and contentURI), an array of key-value pairs with the custom parameters passed to the Feed (`primitiveParams`), and an array of key-value pairs in case the rule requires additional parameters to work (`ruleParams`). The function must revert in case of not meeting the requirements imposed by the rule. Groups follow the `IGroup` interface, which has the following function to query if an account is a member of the group or not: ```solidity function isMember(address account) external view returns (bool); ``` Now let's code the `processCreatePost` function taking all the described above into account: ```solidity contract GroupGatedFeedRule is IFeedRule { mapping(address feed => mapping(bytes32 configSalt => address group)) internal _groupGate; // . . . function processCreatePost( bytes32 configSalt, uint256 postId, CreatePostParams calldata postParams, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external override { require(IGroup(_groupGate[msg.sender][configSalt]).isMember(postParams.author)); } ``` ### Implement the Process Edit Post function Next, implement the `processEditPost` function. This function is invoked by the Feed contract every time a Post is trying to be edited, so then our custom logic can be applied to shape under which conditions this operation can succeed. The function receives the configuration salt (`configSalt`), so we know which configuration to use, the ID the Feed assigned to the Post (`postId`), the parameters of the Post to edit (`postParams`), an array of key-value pairs with the custom parameters passed to the Feed (`primitiveParams`), and an array of key-value pairs in case the rule requires additional parameters to work (`ruleParams`). The function must revert in case of not meeting the requirements imposed by the rule. In this case, given that to reach the point of editing a post, the post must have been created before, we can assume that the author of the post met the conditions to create it, and do not impose any restriction on the edit operation. As a good practice, we revert for unimplemented rule functions, as it is safer in case the rule becomes accidentally applied. ```solidity contract GroupGatedFeedRule is IFeedRule { // . . . function processEditPost( bytes32 configSalt, uint256 postId, EditPostParams calldata postParams, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external override { revert Errors.NotImplemented(); } ``` ### Implement the Process Delete Post function Next, implement the `processDeletePost` function. This function is invoked by the Feed contract every time a Post is trying to be deleted, so then our custom logic can be applied to shape under which conditions this operation can succeed. The function receives the configuration salt (`configSalt`), so we know which configuration to use, the ID the Feed assigned to the Post (`postId`), an array of key-value pairs with the custom parameters passed to the Feed (`primitiveParams`), and an array of key-value pairs in case the rule requires additional parameters to work (`ruleParams`). The function must revert in case of not meeting the requirements imposed by the rule. In this case, given that to reach the point of deleting a post, the post must have been created before, we can assume that the author of the post met the conditions to create it, and do not impose any restriction on the delete operation. As a good practice, we revert for unimplemented rule functions, as it is safer in case the rule becomes accidentally applied. ```solidity contract GroupGatedFeedRule is IFeedRule { // . . . function processDeletePost( bytes32 configSalt, uint256 postId, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external override { revert Errors.NotImplemented(); } ``` ### Implement the Process Post Rule Changes function Finally, implement the `processPostRuleChanges` function. This function is invoked by the Feed contract every time an account makes a change on the [Post Rules](./post-rules) of a Post it authors, so then our Feed rule can define if this change must be accepted or not. The function receives the configuration salt (`configSalt`), so we know which configuration to use, the ID the Feed assigned to the Post which rules are being changed (`postId`), the array of rules changes (`ruleChanges`), and an array of key-value pairs in case the rule requires additional parameters to work (`ruleParams`). The function must revert in case of not meeting the requirements imposed by the rule. In this case, the rule is not focused on controlling Post Rules changes, so we revert as a good practice to avoid unintended effects on if the rule gets applied accidentally. ```solidity contract GroupGatedFeedRule is IFeedRule { // . . . function processPostRuleChanges( bytes32 configSalt, uint256 postId, RuleChange[] calldata ruleChanges, KeyValue[] calldata ruleParams ) external override { revert Errors.NotImplemented(); } } ``` Now the `GroupGatedFeedRule` is ready to be applied into any Feed. See the full code below: ```solidity contract GroupGatedFeedRule is IFeedRule { mapping(address feed => mapping(bytes32 configSalt => address group)) internal _groupGate; function configure(bytes32 configSalt, KeyValue[] calldata ruleParams) external override { address group = address(0); for (uint256 i = 0; i < ruleParams.length; i++) { if (ruleParams[i].key == keccak256("lens.param.group")) { group = abi.decode(ruleParams[i].value, (address)); break; } } // Aims to check if the passed group contract is valid IGroup(group).isMember(address(this)); _groupGate[msg.sender][configSalt] = group; } function processCreatePost( bytes32 configSalt, uint256 postId, CreatePostParams calldata postParams, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external override { require(IGroup(_groupGate[msg.sender][configSalt]).isMember(postParams.author)); } function processEditPost( bytes32 configSalt, uint256 postId, EditPostParams calldata postParams, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external override { revert Errors.NotImplemented(); } function processDeletePost( bytes32 configSalt, uint256 postId, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external override { revert Errors.NotImplemented(); } function processPostRuleChanges( bytes32 configSalt, uint256 postId, RuleChange[] calldata ruleChanges, KeyValue[] calldata ruleParams ) external override { revert Errors.NotImplemented(); } } ``` Stay tuned for API integration of rules and more guides! ================ File: src/pages/protocol/feeds/feedback.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Provide Feedback This guide will show you tools for users to provide feedback on Lens. --- ## Report a Post Users can report a Post if they find it inappropriate or offensive. Reporting a Post will help Lens ML algorithms to identify and remove harmful content. Users can chose between the following reasons: ```graphql filename="PostReportReason" enum PostReportReason { ANIMAL_ABUSE HARASSMENT VIOLENCE SELF_HARM DIRECT_THREAT HATE_SPEECH NUDITY OFFENSIVE SCAM UNAUTHORIZED_SALE IMPERSONATION MISLEADING MISUSE_HASHTAGS UNRELATED REPETITIVE FAKE_ENGAGEMENT MANIPULATION_ALGO SOMETHING_ELSE } ``` You MUST be authenticated as Account Owner or Account Manager to make this request. Use the `reportPost` action to report a post on Lens. ```ts filename="Block Account" import { postId, PostReportReason } from "@lens-protocol/client"; import { reportPost } from "@lens-protocol/client/actions"; const result = await reportPost(sessionClient, { report: PostReportReason.SCAM, post: postId("42"), additionalComment: "This post is a scam!" // optional }); if (result.isErr()) { return console.error(result.error); } ``` Use the `reportPost` mutation to report a post on Lens. ```graphql filename="Mutation" mutation { reportPost( request: { post: "42" reason: SCAM # optional, free-form text # additionalComment: String } ) } ``` ```json filename="Response" { "data": { "reportPost": null } } ``` Coming soon That's it—you've successfully reported a post on Lens! ## Mark as Not Interested Users have the ability to mark a Post as "Not Interested" if they don't want to see similar content in the future. The Lens ML algorithms will use this feedback to improve the user's feed. You MUST be authenticated as Account Owner or Account Manager to make these requests. Use the `addPostNotInterested` action to mark a post as "Not Interested". ```ts filename="Add Post Not Interested" import { postId } from "@lens-protocol/client"; import { addPostNotInterested } from "@lens-protocol/client/actions"; const result = await addPostNotInterested(sessionClient, { post: postId("42"), }); if (result.isErr()) { return console.error(result.error); } ``` Use the `undoPostNotInterested` action to undo the "Not Interested" action. ```ts filename="Undo Post Not Interested" import { postId } from "@lens-protocol/client"; import { undoPostNotInterested } from "@lens-protocol/client/actions"; const result = await undoPostNotInterested(sessionClient, { post: postId("42"), }); if (result.isErr()) { return console.error(result.error); } ``` Use the `mlAddPostNotInterested` mutation to mark a post as "Not Interested". ```graphql filename="Mutation" mutation { mlAddPostNotInterested(request: { post: "42" }) } ``` ```json filename="Response" { "data": { "mlAddPostNotInterested": null } } ``` Use the `mlUndoPostNotInterested` to undo the "Not Interested" action. ```graphql filename="Mutation" mutation { mlUndoPostNotInterested(request: { post: "42" }) } ``` ```json filename="Response" { "data": { "mlUndoPostNotInterested": null } } ``` Coming soon ================ File: src/pages/protocol/feeds/fetch-posts.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Fetch Posts This guide will show you how to fetch Post data in different ways. --- Lens Post data has a rich structure that includes the following information: - Author's details - Post Metadata content - App used to create the Post - Post Actions such as Collect action or community defined actions - Logged-In Post Operations To illustrate how to fetch posts, we will use the following fragments, which includes the most common fields of a Post: ```graphql filename="Post" fragment Post on Post { id author { ...Account } timestamp app { address metadata { name logo } } metadata { ...PostMetadata } root { ...ReferencedPost } quoteOf { ...ReferencedPost } commentOn { ...ReferencedPost } stats { ...PostStats } } ``` ```graphql filename="Repost" fragment Repost on Repost { id author { ...Account } timestamp app { metadata { name logo } } repostOf { ...Post } } ``` ```graphql filename="Account" fragment Account on Account { address username metadata { name picture } } ``` ```graphql filename="ReferencedPost" fragment ReferencedPost on Post { id author { ...Account } metadata { ...PostMetadata } # root, quoteOf, commentOn omitted to avoid circular references } ``` ```graphql filename="PostStats" fragment PostStats on PostStats { # The total number of bookmarks. bookmarks # The total number of comments. comments # The total number of reposts. reposts # The total number of quotes. quotes # The total number of upvotes. upvotes: reactions(request: { type: UPVOTE }) # The total number of downvotes. downvotes: reactions(request: { type: DOWNVOTE }) } ``` In the end of this guide, we will expand on some of the Post fields that are not fully covered in the example above. ## Get a Post Use the `usePost` hook to fetch a single Post. ```tsx filename="With Loading" const { data, loading, error } = usePost(request); ``` ```tsx filename="With Suspense" const { data, error } = usePost({ suspense: true, ...request }); ``` A Post can be fetched by its ID or transaction hash. If the post is not found, the hook will return `null`. Fetching a Post by transaction hash is extremely useful when building a user experience where a user creates a Post and needs it presented back to them. ```ts filename="By Post ID" import { usePost, postId } from "@lens-protocol/react"; // … const { data, loading, error } = usePost({ post: postId("01234…"), }); if (loading) { return

Loading…

; } if (error) { return

{error.message}

; } // data: Post | null ``` ```ts filename="By Tx Hash" import { usePost, txHash } from "@lens-protocol/react"; // … const { data, loading, error } = usePost({ txHash: txHash("0x1234…"), }); if (loading) { return

Loading…

; } if (error) { return

{error.message}

; } // data: Post | null ```
Use the `fetchPost` action to fetch a single Post by ID or by transaction hash. Fetching a Post by transaction hash is extremely useful when building a user experience where a user creates a Post and needs it presented back to them. ```ts filename="By Post ID" import { postId } from "@lens-protocol/client"; import { fetchPost } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchPost(client, { post: postId("01234…"), }); if (result.isErr()) { return console.error(result.error); } const post = result.value; ``` ```ts filename="By Tx Hash" import { txHash } from "@lens-protocol/client"; import { fetchPost } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchPost(client, { txHash: txHash("0x1234…"), }); if (result.isErr()) { return console.error(result.error); } const post = result.value; ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; export const client = PublicClient.create({ environment: mainnet, }); ``` Use the `post` query to fetch a single Post by ID or by transaction hash. Fetching a Post by transaction hash is extremely useful when building a user experience where a user creates a Post and needs it presented back to them. ```graphql filename="Query" query { post( request: { post: "42" # OR # txHash: TxHash! } ) { ... on Post { ...Post } ... on Repost { ...Repost } } } ``` ```json filename="Response" { "data": { "post": { "id": "42", "author": { "address": "0x1234…", "username": { "value": "lens/wagmi" }, "metadata": { "name": "WAGMI", "picture": "https://example.com/wagmi.jpg" } }, "timestamp": "2022-01-01T00:00:00Z", "app": { "metadata": { "name": "Lens", "logo": "https://example.com/lens.jpg" } }, "metadata": { "content": "Hello, World!" }, "root": null, "quoteOf": null, "commentOn": null } } } ```
## List Posts Use the `usePosts` hook to fetch a list of Posts based on the provided filters. ```tsx filename="With Loading" const { data, loading, error } = usePosts(request); ``` ```tsx filename="With Suspense" const { data, error } = usePosts({ suspense: true, ...request }); ``` Posts can be fetched with the following filters: by author, search query, metadata, feed, collected by, account score, and apps. ```ts filename="By Author" import { usePosts, evmAddress } from "@lens-protocol/react"; // … const { data, loading, error } = usePosts({ filter: { authors: evmAddress("0x1234…"), // the author's EVM address }, }); if (loading) { return

Loading…

; } if (error) { return

{error.message}

; } // data: {items: Array, pageInfo: PageInfo} ``` ```ts filename="Search Posts" import { usePosts } from "@lens-protocol/react"; // … const { data, loading, error } = usePosts({ filter: { searchQuery: "Hello, World!", }, }); if (loading) { return

Loading…

; } if (error) { return

{error.message}

; } // data: {items: Array, pageInfo: PageInfo} ``` ```ts filename="By Metadata" import { usePosts, ContentWarning, MainContentFocus } from "@lens-protocol/react"; // … const { data, loading, error } = usePosts({ filter: { metadata: { contentWarning: { oneOf: [ContentWarning.Sensitive] }, mainContentFocus: [MainContentFocus.Image], tags: { all: ["tagExample"] }, }, }, }); if (loading) { return

Loading…

; } if (error) { return

{error.message}

; } // data: {items: Array, pageInfo: PageInfo} ``` ```ts filename="By Feed" import { usePosts, evmAddress } from "@lens-protocol/react"; // … const { data, loading, error } = usePosts({ filter: { feeds: [ // filter by the global feed { globalFeed: true, }, // filter by a specific feed address // { // feed: evmAddress("0x5678…"), // }, // filter by ALL feeds associated with an app address // { // app: evmAddress("0x9123…"), // } ], }, }); if (loading) { return

Loading…

; } if (error) { return

{error.message}

; } // data: {items: Array, pageInfo: PageInfo} ``` ```ts filename="Collected By" import { usePosts, evmAddress } from "@lens-protocol/react"; // … const { data, loading, error } = usePosts({ filter: { collectedBy: { account: evmAddress("0x1234…"), } }, }); if (loading) { return

Loading…

; } if (error) { return

{error.message}

; } // data: {items: Array, pageInfo: PageInfo} ``` ```ts filename="By Account Score" import { usePosts } from "@lens-protocol/react"; // … const { data, loading, error } = usePosts({ filter: { accountScore: { atLeast: 100, // or (one of the two) // lessThan: 200, } }, }); if (loading) { return

Loading…

; } if (error) { return

{error.message}

; } // data: {items: Array, pageInfo: PageInfo} ``` ```ts filename="By Apps" import { usePosts, evmAddress } from "@lens-protocol/react"; // … const { data, loading, error } = usePosts({ filter: { // apps used to publish the posts apps: [evmAddress("0x1234…"), evmAddress("0x5678…")] }, }); if (loading) { return

Loading…

; } if (error) { return

{error.message}

; } // data: {items: Array, pageInfo: PageInfo} ```
Use the paginated `fetchPosts` action to fetch a list of Posts based on the provided filters. ```ts filename="By Author" import { fetchPosts } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchPosts(client, { filter: { authors: evmAddress("0x1234…"), // the author's EVM address }, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="Search Posts" import { evmAddress } from "@lens-protocol/client"; import { fetchPosts } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchPosts(client, { filter: { searchQuery: "Hello, World!", }, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="By Metadata" import { ContentWarning, MainContentFocus } from "@lens-protocol/client"; import { fetchPosts } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchPosts(client, { filter: { metadata: { contentWarning: { oneOf: [ContentWarning.Sensitive] }, mainContentFocus: [MainContentFocus.Image], tags: { all: ["tagExample"] }, }, }, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="By Feed" import { evmAddress } from "@lens-protocol/client"; import { fetchPosts } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchPosts(client, { filter: { feeds: [ // filter by the global feed { globalFeed: true, }, // filter by a specific feed address // { // feed: evmAddress("0x5678…"), // }, // filter by ALL feeds associated with an app address // { // app: evmAddress("0x9123…"), // } ], }, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="Collected By" import { evmAddress } from "@lens-protocol/client"; import { fetchPosts } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchPosts(client, { filter: { collectedBy: { account: evmAddress("0x1234…"), }, }, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="By Account Score" import { fetchPosts } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchPosts(client, { filter: { accountScore: { atLeast: 100, // or (one of the two) // lessThan: 200, }, }, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="By Apps" import { evmAddress } from "@lens-protocol/client"; import { fetchPosts } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchPosts(client, { filter: { // apps used to publish the posts apps: [evmAddress("0x1234…"), evmAddress("0x5678…")], }, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` Use the paginated `posts` query to fetch a list of Posts based on the provided filters. ```graphql filename="Query" query { posts( request: { filter: { # accounts addresses authors: ["0x1234…"] # optional, the app used to create the post # apps: [EvmAddress!] # optional, the type of post # postTypes: [PostType!] # optional, filter by feeds (by default, all feeds are included) feeds: [ # optional, filter by global feed { globalFeed: true } # and/or, filter by feed address # { # feed: EvmAddress # } # and/or, filter by ALL feeds associated w/ an app address # { # app: EvmAddress # } ] # optional, filter by metadata metadata: { mainContentFocus: [IMAGE] # optional, filter by tags # tags: PostMetadataTagsFilter # optional, filter by content warning # contentWarning: PostMetadataContentWarningFilter } # optional, search query in post content or tags # searchQuery : String # optional, filter by account collected posts # collectedBy: { # account: EvmAddress # } # optional, filter by account score # accountScore: { # atLeast: Int # or (one of the two) # lessThan: Int # } } } ) { items { ... on Post { ...Post } ... on Repost { ...Repost } } pageInfo { prev next } } } ``` ```json filename="Response" { "data": { "posts": { "items": [ { "id": "42", "author": { "address": "0x1234…", "username": { "value": "lens/wagmi" }, "metadata": { "name": "WAGMI", "picture": "https://example.com/wagmi.jpg" } }, "timestamp": "2022-01-01T00:00:00Z", "app": { "metadata": { "name": "Lens", "logo": "https://example.com/lens.jpg" } }, "metadata": { "content": "Hello, World!" }, "root": null, "quoteOf": null, "commentOn": null } ], "pageInfo": { "prev": null, "next": null } } } } ```
See the [Pagination](../best-practices/pagination) guide for more information on how to handle paginated results. ## Posts For You Use the `usePostsForYou` hook to retrieve a list of recommended Posts based on Lens Machine Learning (ML) algorithms. ```tsx filename="With Loading" const { data, loading, error } = usePostsForYou(request); ``` ```tsx filename="With Suspense" const { data, error } = usePostsForYou({ suspense: true, ...request }); ``` Example of how to use the `usePostsForYou` hook: ```ts filename="Example" import { usePostsForYou, evmAddress } from "@lens-protocol/react"; // … const { data, loading, error } = usePostsForYou({ account: evmAddress("0x1234…"), shuffle: true, // optional, shuffle the results }); if (loading) { return

Loading…

; } if (error) { return

{error.message}

; } // data: {items: Array, pageInfo: PageInfo} ```
Use the paginated `fetchPostsForYou` action to retrieve a list of recommended Posts based on Lens Machine Learning (ML) algorithms. ```ts filename="Example" import { evmAddress } from "@lens-protocol/client"; import { fetchPostsForYou } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchPostsForYou(client, { account: evmAddress("0x1234…"), shuffle: true, // optional, shuffle the results }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; export const client = PublicClient.create({ environment: mainnet, }); ``` Use the paginated `mlPostsForYou` query to retrieve a list of recommended Posts based on Lens Machine Learning (ML) algorithms. ```graphql filename="Query" query { mlPostsForYou( request: { # accounts addresses account: "0x1234…" # optional, shuffle the results # shuffle: Boolean } ) { items { ...PostForYou } pageInfo { prev next } } } ``` ```json filename="Response" { "data": { "mlPostsForYou": { "items": [ { "post": { "id": "42", "author": { "address": "0x1234…", "username": { "value": "lens/wagmi" }, "metadata": { "name": "WAGMI", "picture": "https://example.com/wagmi.jpg" } }, "timestamp": "2022-01-01T00:00:00Z", "app": { "metadata": { "name": "Lens", "logo": "https://example.com/lens.jpg" } }, "metadata": { "content": "Hello, World!" }, "root": null, "quoteOf": null, "commentOn": null }, "source": "ML" } ], "pageInfo": { "prev": null, "next": null } } } } ```
See the [Pagination](../best-practices/pagination) guide for more information on how to handle paginated results. ## Posts to Explore The Explore Feed blends a mix of Popular-Trending content and a curated selection from Lens Machine Learning (ML) algorithms. Use the `usePostsToExplore` hook to retrieve a list of posts to explore. ```tsx filename="With Loading" const { data, loading, error } = usePostsToExplore(); ``` ```tsx filename="With Suspense" const { data, error } = usePostsToExplore({ suspense: true }); ``` Example of how to use the `usePostsToExplore` hook: ```ts filename="Example" import { usePostsToExplore } from "@lens-protocol/react"; // … const { data, loading, error } = usePostsToExplore(); if (loading) { return

Loading…

; } if (error) { return

{error.message}

; } // data: {items: Array, pageInfo: PageInfo} ```
Use the paginated `fetchPostsToExplore` action to retrieve a list of post to explore. ```ts filename="Example" import { fetchPostsToExplore } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchPostsToExplore(client); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; export const client = PublicClient.create({ environment: mainnet, }); ``` Use the paginated `mlPostsExplore` query to retrieve a list of post to explore. ```graphql filename="Query" query { mlPostsExplore( request: { # optional filter # filter: { # since: number # } } ) { items { ...Post } pageInfo { prev next } } } ``` ```json filename="Response" { "data": { "mlPostsExplore": { "items": [ { "id": "42", "author": { "address": "0x1234…", "username": { "value": "lens/wagmi" }, "metadata": { "name": "WAGMI", "picture": "https://example.com/wagmi.jpg" } }, "timestamp": "2022-01-01T00:00:00Z", "app": { "metadata": { "name": "Lens", "logo": "https://example.com/lens.jpg" } }, "metadata": { "content": "Hello, World!" }, "root": null, "quoteOf": null, "commentOn": null } ], "pageInfo": { "prev": null, "next": null } } } } ```
See the [Pagination](../best-practices/pagination) guide for more information on how to handle paginated results. ## Post References List all posts that reference a specific post (e.g., comments, quotes, reposts). Use the paginated `fetchPostReferences` action to fetch a list of Posts that reference a specific Post. ```ts filename="Comments" import { postId, PostReferenceType } from "@lens-protocol/client"; import { fetchPostReferences } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchPostReferences(client, { referencedPost: postId("01234…"), referenceTypes: [PostReferenceType.CommentOn], }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="Comment's Visibility Filter" import { postId, PostReferenceType, PostVisibilityFilter, } from "@lens-protocol/client"; import { fetchPostReferences } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchPostReferences(client, { referencedPost: postId("01234…"), referenceTypes: [PostReferenceType.CommentOn], visibilityFilter: PostVisibilityFilter.Visible, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="Relevancy Filter" import { postId, PostReferenceType, ReferenceRelevancyFilter, } from "@lens-protocol/client"; import { fetchPostReferences } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchPostReferences(client, { referencedPost: postId("01234…"), referenceTypes: [PostReferenceType.CommentOn, PostReferenceType.QuoteOf], relevancyFilter: ReferenceRelevancyFilter.Relevant, // or ReferenceRelevancyFilter.NotRelevant }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="By Authors" import { postId, PostReferenceType, ReferenceRelevancyFilter, } from "@lens-protocol/client"; import { fetchPostReferences } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchPostReferences(client, { referencedPost: postId("01234…"), referenceTypes: [PostReferenceType.CommentOn], authors: [evmAddress("0x5678…"), evmAddress("0x9abc…")], }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; export const client = PublicClient.create({ environment: mainnet, }); ``` Use the paginated `postReferences` query to fetch a list of Posts that reference a specific Post. ```graphql filename="Query" query { postReferences( request: { referencedPost: "42" referenceTypes: [COMMENT_ON] # optional, control visibility of hidden posts # visibilityFilter: PostVisibilityFilter! = VISIBLE # optional, control relevancy of posts # relevancyFilter: ReferenceRelevancyFilter! = RELEVANT } ) { items { ... on Post { ...Post } ... on Repost { ...Repost } } pageInfo { prev next } } } ``` ```json filename="Response" { "data": { "postReferences": { "items": [ { "id": "42", "author": { "address": "0x1234…", "username": { "value": "lens/wagmi" }, "metadata": { "name": "WAGMI", "picture": "https://example.com/wagmi.jpg" } }, "timestamp": "2022-01-01T00:00:00Z", "app": { "metadata": { "name": "Lens", "logo": "https://example.com/lens.jpg" } }, "metadata": { "content": "First comment!" }, "root": null, "quoteOf": null, "commentOn": { "id": "43", "author": { "address": "0x1234…", "username": { "value": "lens/alice" }, "metadata": { "name": "Alice", "picture": "https://example.com/alice.jpg" } }, "metadata": { "content": "Nice post!" } } } ], "pageInfo": { "prev": null, "next": null } } } } ``` The `visibilityFilter` allows you to control the visibility of post replies hidden by the author. For more information, see the [Moderating Own Threads](./moderating) guide. Use the `usePostReferences` hook to fetch a list of Posts that reference a specific Post. ```tsx filename="With Loading" const { data, loading, error } = usePostReferences(request); ``` ```tsx filename="With Suspense" const { data, error } = usePostReferences({ suspense: true, ...request }); ``` Posts can be fetched by various reference types and filters. ```ts filename="Comments" import { usePostReferences, postId, PostReferenceType } from "@lens-protocol/react"; // … const { data, loading, error } = usePostReferences({ referencedPost: postId("01234…"), referenceTypes: [PostReferenceType.CommentOn], }); if (loading) { return

Loading…

; } if (error) { return

{error.message}

; } // data: {items: Array, pageInfo: PageInfo} ``` ```ts filename="Comment's Visibility Filter" import { usePostReferences, postId, PostReferenceType, PostVisibilityFilter, } from "@lens-protocol/react"; // … const { data, loading, error } = usePostReferences({ referencedPost: postId("01234…"), referenceTypes: [PostReferenceType.CommentOn], visibilityFilter: PostVisibilityFilter.Visible, }); if (loading) { return

Loading…

; } if (error) { return

{error.message}

; } // data: {items: Array, pageInfo: PageInfo} ``` ```ts filename="Relevancy Filter" import { usePostReferences, postId, PostReferenceType, ReferenceRelevancyFilter, } from "@lens-protocol/react"; // … const { data, loading, error } = usePostReferences({ referencedPost: postId("01234…"), referenceTypes: [PostReferenceType.CommentOn, PostReferenceType.QuoteOf], relevancyFilter: ReferenceRelevancyFilter.Relevant, // or ReferenceRelevancyFilter.NotRelevant }); if (loading) { return

Loading…

; } if (error) { return

{error.message}

; } // data: {items: Array, pageInfo: PageInfo} ``` ```ts filename="By Authors" import { usePostReferences, postId, PostReferenceType, } from "@lens-protocol/react"; // … const { data, loading, error } = usePostReferences({ referencedPost: postId("01234…"), referenceTypes: [PostReferenceType.CommentOn], authors: [ evmAddress("0x5678…"), evmAddress("0x9abc…"), ], }); if (loading) { return

Loading…

; } if (error) { return

{error.message}

; } // data: {items: Array, pageInfo: PageInfo} ```
See the [Pagination](../best-practices/pagination) guide for more information on how to handle paginated results. ## Post Fields In this section we will expand on some of the Post fields that are not covered in the examples above. ### Post Slug The `slug` field of a Post is a unique identifier that can be used to reference the Post in places where length is a concern. ```ts filename="TypeScript" type Post = { id: PostId; slug: PostId; }; ``` ```graphql filename="GraphQL" type Post { id: PostId! slug: PostId! } ``` The typical use of a Post slug is to create a URL that points to the Post. For example: ```text https://lens.xyz/posts/13z23jpf7nz3kk6tn8h ``` where `13z23jpf7nz3kk6tn8h` is the Post slug. Lens API accepts the Post slug anywhere a Post ID is expected. ### Post Metadata The `metadata` field of any Post contains the Post Metadata object that was linked to the post at the time of creation. See the [Create a Post](../posts/create) guide for more information on how this object is created. In GraphQL this is represented as a union type: ```graphql filename="PostMetadata" union PostMetadata = | ArticleMetadata | AudioMetadata | CheckingInMetadata | EmbedMetadata | EventMetadata | ImageMetadata | LinkMetadata | LivestreamMetadata | MintMetadata | SpaceMetadata | StoryMetadata | TextOnlyMetadata | ThreeDMetadata | TransactionMetadata | VideoMetadata ``` If you used the `@lens-protocol/metadata` package to create the Post Metadata object, you will find the content of this union very familiar. The union type allows to discriminate between different types of Post Metadata objects and access their specific fields. This is especially useful when rendering the Post in a UI because enables you to componentize the rendering logic based on the type of Post Metadata. Coming soon Coming soon ### Mentions The `mentions` field of a Post contains a list of all the accounts or groups mentioned in the Post content. ```graphql filename="Post" fragment Post on Post { mentions { ... on AccountMention { ...AccountMention } ... on GroupMention { ...GroupMention } } } ``` ```graphql filename="AccountMention" fragment AccountMention on AccountMention { # The Account that was mentioned. account # EvmAddress! # The Username namespace of the mentioned username. namespace # EvmAddress! # The replacement information. replace { ...MentionReplace } } ``` ```graphql filename="GroupMention" fragment GroupMention on GroupMention { # The Group that was mentioned. group # EvmAddress! # The replacement information. replace { ...MentionReplace } } ``` ```graphql filename="MentionReplace" fragment MentionReplace on MentionReplace { from # e.g., @lens/foo to # e.g., @lens/bar new username for the given account } ``` Use the `replace` field to get the replacement information for the mentioned account. See the [Mentions](../best-practices/mentions#rendering-mentions) guide for more information on how to render mentions in a Post. ### Post Stats The `stats` field of a Post contains the aggregated statistics of the Post, such as the total number of bookmarks, comments, reposts, quotes, reactions. ```graphql filename="Post" fragment Post on Post { stats { bookmarks # Int! comments # Int! reposts # Int! quotes # Int! collects # Int! upvotes: reactions(request: { type: UPVOTE }) # Int! downvotes: reactions(request: { type: DOWNVOTE }) # Int! } } ``` Use the `reactions` field to get the number of reactions of a specific type like shown in the example above. ### Post Actions In essence a Post Action is smart contract that can be attached to a Lens Post to expand its functionality. The `actions` field of a Post contains a list of possible actions that can be performed on a given Post. ```graphql filename="Post" fragment Post on Post { actions { ... on SimpleCollectActionSettings { ...SimpleCollectActionSettings } ... on UnknownActionSettings { ...UnknownActionSettings } } } ``` ```graphql filename="SimpleCollectActionSettings" fragment SimpleCollectActionSettings on SimpleCollectActionSettings { contract { ...NetworkAddress } amount { ...Amount } collectNft # EvmAddress collectLimit # String followerOnly # Boolean! recipient # EvmAddress! referralFee # Float! endsAt # DateTime recipients { recipient # EvmAddress! split # Float! } } ``` ```graphql filename="UnknownActionSettings" fragment UnknownActionSettings on UnknownActionSettings { initializeCalldata # BlockchainData initializeResultData # BlockchainData verified # Boolean! contract { ...NetworkAddress } collectNft # EvmAddress } ``` ```graphql filename="Amount" fragment Amount on Amount { asset { ... on Erc20 { name # String! symbol # String! decimals # Int! contract { ...NetworkAddress } } } value # BigDecimal! } ``` ```graphql filename="NetworkAddress" fragment NetworkAddress on NetworkAddress { address # EvmAddress! chainId # Int! } ``` Lens API helps to make a distinction between protocol-native actions and community-defined actions, by giving a clear name to the former and leaving the latter as an `UnknownActionSettings` type. See the [Post Actions](./post-actions) guide for more information on how to use this field to execute actions on a Post. ### Logged-In Operations The Lens schema allows logged-in users to fetch details about available actions and actions already taken, via the `operations` field. ```ts filename="TS" type Post = { operations: LoggedInPostOperations | null; }; type LoggedInPostOperations = { id: ID; canComment: OperationValidationOutcome; canQuote: OperationValidationOutcome; canRepost: OperationValidationOutcome; hasBookmarked: boolean; hasCommented: BooleanValue; hasQuoted: BooleanValue; hasUpvoted: boolean; // hasReacted(request: { type: UPVOTE }) hasDownvoted: boolean; // hasReacted(request: { type: DOWNVOTE }) hasReported: boolean; hasReposted: BooleanValue; isNotInterested: boolean; }; ``` ```graphql filename="Post" fragment Post on Post { operations { ...LoggedInPostOperations } } ``` ```graphql filename="LoggedInPostOperations" fragment LoggedInPostOperations on LoggedInPostOperations { canComment { ...OperationValidationOutcome } canDelete { ...OperationValidationOutcome } canEdit { ...OperationValidationOutcome } canQuote { ...OperationValidationOutcome } canRepost { ...OperationValidationOutcome } hasBookmarked hasCommented { ...BooleanValue } hasQuoted { ...BooleanValue } hasUpvoted: hasReacted(request: { type: UPVOTE }) hasDownvoted: hasReacted(request: { type: DOWNVOTE }) hasReported hasReposted { ...BooleanValue } isNotInterested } ``` The `LoggedInPostOperations` type specifies both the actions the user can perform (e.g., _canComment_, _canRepost_) and the actions already taken (e.g., _hasReacted_, _hasCommented_). ```json filename="Example" { "operations": { "canComment": { "restrictedSignerRequired": false, "extraChecksRequired": [] }, "canDelete": { "restrictedSignerRequired": false, "extraChecksRequired": [] }, "canEdit": { "restrictedSignerRequired": false, "extraChecksRequired": [] }, "canQuote": { "restrictedSignerRequired": false, "extraChecksRequired": [] }, "canRepost": { "restrictedSignerRequired": false, "extraChecksRequired": [] }, "hasBookmarked": true, "hasCommented": { "optimistic": true, "onChain": false }, "hasQuoted": { "optimistic": false, "onChain": true }, "hasUpvoted": true, "hasDownvoted": false, "hasReported": false, "hasReposted": { "optimistic": true, "onChain": false }, "isNotInterested": false } } ``` Where: - `canComment`: Indicates whether the user passed the criteria to comment on the post. - `canQuote`: Indicates whether the user passed the criteria to quote the post. - `canRepost`: Indicates whether the user passed the criteria to repost. - `canEdit`: Indicates whether the user passed the criteria to edit the post. - `canDelete`: Indicates whether the user passed the criteria to delete the post. - `hasBookmarked`: Indicates whether the user has bookmarked the post. - `hasReported`: Indicates whether the user has reported the post. - `hasReacted` and derived fields: indicate whether the user has left a given reaction on the post. - `hasCommented`: Indicates whether the user has commented on the post. - `hasQuoted`: Indicates whether the user has quoted the post. - `hasReposted`: Indicates whether the user has reposted the post. - `isNotInterested`: Indicates whether the user has marked the post as not interesting. Fields returning an `OperationValidationOutcome` give information on the feasibility of the operation. More details in the [Querying Data](../best-practices/querying-data#operation-validation) guide. Fields returning a `BooleanValue` are operations that settles on-chain but could be optimistically assumed to be done. ```graphql filename="BooleanValue" fragment BooleanValue on BooleanValue { optimistic # true if the operation is optimistically assumed to be done onChain # true if the operation is settled on-chain } ``` ================ File: src/pages/protocol/feeds/moderating.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Moderating Own Threads This guide illustrates how users can moderate conversations on their own threads. --- ## Hide a Reply The Lens API allows you to hide replies to your own posts. This feature only affects the data returned by the API; the replies still exist on-chain. You MUST be authenticated as Account Owner or Account Manager to make this request. Use the `hideReply` action to hide a reply on your post. To undo this action, use the `unhideReply` action. ```ts filename="Hide Reply" import { postId } from "@lens-protocol/client"; import { hideReply } from "@lens-protocol/client/actions"; const result = await hideReply(sessionClient, { post: postId("1234…"), }); if (result.isErr()) { return console.error(result.error); } ``` ```ts filename="Unhide Reply" import { postId } from "@lens-protocol/client"; import { unhideReply } from "@lens-protocol/client/actions"; const result = await unhideReply(sessionClient, { post: postId("1234…"), }); if (result.isErr()) { return console.error(result.error); } ``` Use the `hideReply` mutation to hide a reply on your post. To undo this action, use the `unhideReply` mutation. ```graphql filename="Hide Mutation" mutation { hideReply(request: { post: "42" }) } ``` ```graphql filename="Undo Mutation" mutation { unhideReply(request: { post: "42" }) } ``` That's it—the reply is now hidden from your post. Coming soon ================ File: src/pages/protocol/feeds/post-actions.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Post Actions This guide explains how to use Post Actions and how to implement custom ones. --- Post Actions are contracts that extend the Lens Protocol functionality by allowing Accounts to execute actions on Posts. Lens provides two built-in Post Actions: - `SimpleCollectAction` - Allows an Account to collect an NFT from a Post. - `TippingPostAction` - Allows an Account to tip the author of a Post. It is also possible to create custom Post Actions to extend the functionality of the Lens Protocol. ## Post Action Initialization You can configure one or more Post Actions when creating a Post. This section presumes you are familiar with the process of [creating a Post](./post) on Lens. ### Simple Collect Action The `SimpleCollectAction` allows a user to collect an [ERC-721](https://ethereum.org/en/developers/docs/standards/tokens/erc-721/) NFT from a given post. The NFT collection is *eagerly deployed* when the Post is created. The Post author can configure the following parameters: - **Collect limit** (optional) – The maximum number of NFTs that can be minted. - **End time** (optional) – The deadline after which NFTs can no longer be collected. - **Follower requirement** (optional) – Whether the collector must be following the Post author on a specified Lens Graph (global or custom). - **NFT immutability** (optional) – If enabled, minted NFTs will retain a snapshot of the content URI at the time of minting. This ensures that any edits to the Post after minting will not affect previously minted NFTs. For this to be fully effective, the content URI itself should be immutable. - **Price** (optional) – The cost of the NFT. If set, the author can also specify: - **Referral share** (optional) – The percentage of the payment allocated to any referrers. - **Recipients** – A list of addresses that will receive a share of the payment. This is after the referral share has been deducted. For paid collects, a **1.5%** Lens treasury fee is deducted from the total amount paid by the collector before calculating referral and recipient shares. ```ts filename="Limited Collect" import { dateTime, evmAddress, uri } from "@lens-protocol/client"; const result = await post(sessionClient, { contentUri: uri("lens://4f91ca…"), actions: [ { simpleCollect: { collectLimit: 100, endsAt: dateTime("2032-12-22T00:00:00Z"), }, }, ], }); ``` ```ts filename="Paid Collect in GHO" import { dateTime, evmAddress, uri } from "@lens-protocol/client"; const result = await post(sessionClient, { contentUri: uri("lens://4f91ca…"), actions: [ { simpleCollect: { payToCollect: { native: "5.25", recipients: [ { address: evmAddress("0x5678…"), percent: 30, // 30% }, { address: evmAddress("0x9abc…"), percent: 70, // 70% }, ], referralShare: 5, // 5% }, }, }, ], }); ``` ```ts filename="Paid Collect in ERC20" import { dateTime, evmAddress, uri } from "@lens-protocol/client"; const result = await post(sessionClient, { contentUri: uri("lens://4f91ca…"), actions: [ { simpleCollect: { payToCollect: { erc20: { currency: evmAddress("0x1234…"), value: "42.42", }, recipients: [ { address: evmAddress("0x5678…"), percent: 30, // 30% }, { address: evmAddress("0x9abc…"), percent: 70, // 70% }, ], referralShare: 5, // 5% }, }, }, ], }); ``` ```ts filename="Followers Only" import { dateTime, evmAddress, uri } from "@lens-protocol/client"; const result = await post(sessionClient, { contentUri: uri("lens://4f91ca…"), actions: [ { simpleCollect: { followerOnGraph: { globalGraph: true, }, }, }, ], }); ``` ```ts filename="Immutable NFTs" import { dateTime, evmAddress, uri } from "@lens-protocol/client"; const result = await post(sessionClient, { contentUri: uri("lens://4f91ca…"), actions: [ { simpleCollect: { isImmutable: true, }, }, ], }); ``` ```graphql filename="Limited Collect" mutation { post( request: { contentUri: "lens://4f91ca…" actions: [ { simpleCollect: { collectLimit: 100, endsAt: "2032-12-22T00:00:00Z" } } ] } ) { ...PostResult } } ``` ```graphql filename="Paid Collect" mutation { post( request: { contentUri: "lens://4f91ca…" actions: [ { simpleCollect: { payToCollect: { native: "5.25" recipients: [ { address: "0x5678…", percent: 30 } { address: "0x9abc…", percent: 70 } ] referralShare: 5 } } } ] } ) { ...PostResult } } ``` ```graphql filename="Paid Collect in ERC20" mutation { post( request: { contentUri: "lens://4f91ca…" actions: [ { simpleCollect: { payToCollect: { erc20: { currency: "0x1234…", value: "42.42" } recipients: [ { address: "0x5678…", percent: 30 } { address: "0x9abc…", percent: 70 } ] referralShare: 5 } } } ] } ) { ...PostResult } } ``` ```graphql filename="Followers Only" mutation { post( request: { contentUri: "lens://4f91ca…" actions: [{ simpleCollect: { followerOnGraph: { globalGraph: true } } }] } ) { ...PostResult } } ``` ```graphql filename="Immutable NFTs" mutation { post( request: { contentUri: "lens://4f91ca…" actions: [{ simpleCollect: { isImmutable: true } }] } ) { ...PostResult } } ``` ```graphql filename="PostResult" fragment PostResult on PostResult { ... on PostResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on PostOperationValidationFailed { reason } ... on TransactionWillFail { reason } } ``` ### Tipping Post Action Lens includes a built-in `TippingPostAction`, which is enabled by default and allows an Account to tip the author of a Post using native GHO (GRASS on the Lens Testnet) or ERC20 token. The `TippingPostAction` supports a referral scheme that lets you reward accounts or apps that helped surface the tipped Post. A maximum of **20%** of the tipped amount can be allocated to referrals, but only if they're explicitly included when the tip is sent. A **1.5%** Lens treasury fee is deducted from the total amount paid by the tipper before calculating referral and recipient shares. ### Payment Source Post Actions can use funds from either the **Signer** or the **Lens Account** to pay for tips or collects. - **Signer** refers to the Account Owner or Account Manager, depending on the [authentication role](../authentication). - **Lens Account** is the account you are currently logged in with. ### Referral Fee Breakdown Let’s say a user tips the author of a Post with **100 GHO** using the `TippingPostAction`. Here's how the amount is split: - **1.5 GHO** (1.5%) is deducted for the **Lens treasury fee** - **98.5 GHO** remains The user includes two referral recipients, fully allocating the maximum **20% referral fee** between them: - `0xc0ffee` with a **30% share** of the referral portion - `0xbeef` with a **70% share** of the referral portion From the remaining **98.5 GHO**: - **19.7 GHO** (20%) is distributed as referrals: - **5.91 GHO** to `0xc0ffee` - **13.79 GHO** to `0xbeef` - **78.8 GHO** is sent to the **Post author** If the referral split adds up to less than 100% (e.g. a single referral with 50%), only the corresponding portion of the 20% referral fee will be used. The unused remainder goes to the Post author. ### Custom Post Actions ```ts filename="Custom Post Action" import { blockchainData, evmAddress, uri } from "@lens-protocol/client"; const result = await post(sessionClient, { contentUri: uri("lens://4f91ca…"), actions: [ { unknown: { address: evmAddress("0x1234…"), params: [ { raw: { // 32 bytes key (e.g., keccak(name)) key: blockchainData("0xac5f04…"), // an ABI encoded data data: blockchainData("0x00"), }, }, ], }, }, ], }); ``` ```graphql filename="Custom Post Action" mutation { post( request: { contentUri: "lens://4f91ca…" actions: [ { unknown: { address: "0x1234…" params: [ { raw: { key: "0x4f…" # 32-byte key (e.g., keccak(name)) data: "0x00" # ABI-encoded data } } ] } } ] } ) { ... on PostResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on PostOperationValidationFailed { reason } ... on TransactionWillFail { reason } } } ``` ## Executing Post Actions To execute a Post Action, follow these steps. You MUST be authenticated as the Account Owner or Account Manager to execute a Post Action. ### Inspect Post Actions First, inspect the `post.actions` field to determine what Post Actions are available on a given Post. ```ts filename="Post Actions" for (const action of post.actions) { switch (action.__typename) { case "SimpleCollectAction": // The Post has a Simple Collect Action break; case "UnknownAction": // The Post has a Custom Post Action break; } } ``` An example of each Post Action type is provided below. ```json filename="SimpleCollectAction" { "__typename": "SimpleCollectAction", "payToCollect": { "__typename": "PayToCollectConfig", "price": { "__typename": "Erc20Amount", "asset": { "__typename": "Erc20", "name": "Wrapped GHO", "symbol": "wGHO", "contract": { "__typename": "NetworkAddress", "address": "0x1234…", "chainId": 37111 }, "decimals": 18 }, "value": "42.42" }, "referralShare": 5, // 5% "recipients": [ { "__typename": "RecipientPercent", "address": "0x5678…", "percent": 30 // 30% }, { "__typename": "RecipientPercent", "address": "0x9abc…", "percent": 70 // 70% } ] }, "collectLimit": 100, "endsAt": "2032-12-22T00:00:00Z", "followerOnGraph": { "__typename": "FollowerOn", "globalGraph": true }, "isImmutable": true } ``` ```json filename="UnknownAction" { "__typename": "UnknownAction", "address": "0x1234…", "metadata": { "__typename": "UnknownAction", "id": "123e4567-e89b-12d3-a456-426614174000", "name": "SampleAction", "description": "This is a sample action description.", "authors": ["author1@example.com", "author2@example.com"], "source": "https://github.com/example/repo", "configureParams": [ { "__typename": "KeyValuePair", "key": "0x3e…", "name": "limit", "type": "uint256" } ], "executeParams": [ { "__typename": "KeyValuePair", "key": "0x4f…", "name": "sender", "type": "address" }, { "__typename": "KeyValuePair", "key": "0x5f…", "name": "amount", "type": "uint256" } ], "setDisabledParams": [] } } ``` ### Execute Post Action Next, execute the desired Post Action. Use the `executePostAction` action to execute any Post Action. ```ts filename="Collect" import { postId } from "@lens-protocol/client"; import { executePostAction } from "@lens-protocol/client/actions"; const result = await executePostAction(sessionClient, { post: postId("42"), action: { simpleCollect: { selected: true, }, }, }); if (result.isErr()) { return console.error(result.error); } ``` ```ts filename="Collect with Signer" import { postId, PaymentSource } from "@lens-protocol/client"; import { executePostAction } from "@lens-protocol/client/actions"; const result = await executePostAction(sessionClient, { post: postId("42"), action: { simpleCollect: { selected: true, paymentSource: PaymentSource.Signer, }, }, }); if (result.isErr()) { return console.error(result.error); } ``` ```ts filename="Collect w/ Referrals" import { postId } from "@lens-protocol/client"; import { executePostAction } from "@lens-protocol/client/actions"; const result = await executePostAction(sessionClient, { post: postId("42"), action: { simpleCollect: { selected: true, referrals: [ { address: "0x5678…", percent: 30, }, { address: "0x9abc…", percent: 70, }, ], }, }, }); if (result.isErr()) { return console.error(result.error); } ``` ```ts filename="Tipping Native" import { evmAddress, postId } from "@lens-protocol/client"; import { executePostAction } from "@lens-protocol/client/actions"; const result = await executePostAction(sessionClient, { post: postId("42"), action: { tipping: { native: bigDecimal(5), }, }, }); if (result.isErr()) { return console.error(result.error); } ``` ```ts filename="Tipping with Signer" import { evmAddress, postId, PaymentSource } from "@lens-protocol/client"; import { executePostAction } from "@lens-protocol/client/actions"; const result = await executePostAction(sessionClient, { post: postId("42"), action: { tipping: { native: bigDecimal(5), paymentSource: PaymentSource.Signer, }, }, }); if (result.isErr()) { return console.error(result.error); } ``` ```ts filename="Tipping with ERC20" import { evmAddress, postId } from "@lens-protocol/client"; import { executePostAction } from "@lens-protocol/client/actions"; const result = await executePostAction(sessionClient, { post: postId("42"), action: { tipping: { erc20: { currency: evmAddress("0x5678…"), value: bigDecimal(100), }, }, }, }); if (result.isErr()) { return console.error(result.error); } ``` ```ts filename="Tipping w/ Referrals" import { evmAddress, postId } from "@lens-protocol/client"; import { executePostAction } from "@lens-protocol/client/actions"; const result = await executePostAction(sessionClient, { post: postId("42"), action: { tipping: { native: bigDecimal(5), referrals: [ { address: evmAddress("0xc0ffee…"), percent: 10, }, { address: evmAddress("0xbeef…"), percent: 90, }, ], }, }, }); if (result.isErr()) { return console.error(result.error); } ``` ```ts filename="Custom Action" import { blockchainData, evmAddress, postId } from "@lens-protocol/client"; import { executePostAction } from "@lens-protocol/client/actions"; const result = await executePostAction(sessionClient, { post: postId("42"), action: { unknown: { address: evmAddress("0x1234…"), params: [ { raw: { // 32 bytes key (e.g., keccak(name)) key: blockchainData("0xac5f04…"), // an ABI encoded data data: blockchainData("0x00"), }, }, ], }, }, }); if (result.isErr()) { return console.error(result.error); } ``` Use the `executePostAction` mutation to execute any Post Action. ```graphql filename="Collect" mutation { executePostAction( request: { post: "42" action: { simpleCollect: { selected: true # paymentSource: "SIGNER" # default is "ACCOUNT" } # Tipping Action # tipping: { # native: { value: "100" }, # # or # erc20: { currency: "0x5678…", value: "100" }, # # paymentSource: "SIGNER" # default is "ACCOUNT" # } # referrals: [ # { address: "0x5678…", percent: 30 } # { address: "0x9abc…", percent: 70 } # ] # Custom Post Action # unknown: { # address: "0x1234…", # params: [ # { raw: { key: "0x4f…", data: "0x00" } }, # ], # }, } } ) { ... on ExecutePostActionResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } ... on SignerErc20ApprovalRequired { reason amount { ...Erc20Amount } } ... on InsufficientFunds { reason } } } ``` ```json filename="ExecutePostActionResponse" { "data": { "executePostAction": { "hash": "0x…" } } } ``` Coming soon ### Handle Result Then, the result will indicate what steps to take next. ```ts filename="viem" highlight="1,17,21,27" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const operation = await executePostAction(sessionClient, { post: postId("42"), action: { // … }, }); if (operation.isErr()) { return console.error(operation.error); } switch (operation.value.__typename) { case "InsufficientFunds": // handle insufficient funds scenario return console.log("Insufficient funds to perform the action"); case "SignerErc20ApprovalRequired": // handle ERC20 approval required scenario leveraging operation.value.amount: Erc20Amount return console.log("Signer ERC20 approval required to perform the action"); } const result = await operation .asyncAndThen(handleOperationWith(wallet)) .andThen(sessionClient.waitForTransaction); ``` ```ts filename="ethers" highlight="1,17,20,26" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const operation = await executePostAction(sessionClient, { post: postId("42"), action: { // … }, }); if (operation.isErr()) { return console.error(operation.error); } switch (operation.value.__typename) { case "InsufficientFunds": // handle insufficient funds scenario return console.log("Insufficient funds to perform the action"); case "SignerErc20ApprovalRequired": // handle ERC20 approval required scenario leveraging operation.value.amount: Erc20Amount return console.log("Signer ERC20 approval required to perform the action"); } const result = await operation .asyncAndThen(handleOperationWith(signer)) .andThen(sessionClient.waitForTransaction); ``` See the [ERC20 token approval](../best-practices/erc20-approval) guide for more information on how to handle ERC20 token approval. Then, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Coming soon That's it—you've successfully executed a Post Action. ## Action Execution History The Lens Protocol provides a method to track which accounts have executed specific actions on a Post. Use the paginated `fetchWhoExecutedActionOnPost` action to get a list of accounts that executed a specific Action on a Post. ```ts filename="Fetch All Actions" import { postId } from "@lens-protocol/client"; import { fetchWhoExecutedActionOnPost } from "@lens-protocol/client/queries"; const result = await fetchWhoExecutedActionOnPost(client, { post: postId("42"), }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="Filter by Collect" import { postId } from "@lens-protocol/client"; import { fetchWhoExecutedActionOnPost } from "@lens-protocol/client/queries"; const result = await fetchWhoExecutedActionOnPost(client, { post: postId("42"), filter: { anyOf: [ { simpleCollect: true, }, ], }, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="Filter by Tipping" import { postId } from "@lens-protocol/client"; import { fetchWhoExecutedActionOnPost } from "@lens-protocol/client/queries"; const result = await fetchWhoExecutedActionOnPost(client, { post: postId("42"), filter: { anyOf: [ { tipping: true, }, ], }, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="Filter by Unknown" import { evmAddress, postId } from "@lens-protocol/client"; import { fetchWhoExecutedActionOnPost } from "@lens-protocol/client/queries"; const result = await fetchWhoExecutedActionOnPost(client, { post: postId("42"), filter: { anyOf: [ { address: evmAddress("0x1234…"), }, ], }, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` Use the `whoExecutedPostAction` query to get a list of accounts that executed a specific Action on a Post. ```graphql filename="Query" query { whoExecutedPostAction( request: { post: "42" # Optional parameters # orderBy: WhoExecutedActionOnPostOrderBy, # filter: { # anyOf: [{ # address: "0x1234…", # }, # { # simpleCollect: true, # }, # { # tipping: true, # }]; # } } ) { items { account { address } total lastAt firstAt } pageInfo { next prev } } } ``` ```json filename="Response" { "data": { "whoExecutedPostAction": { "items": [ { "account": { "address": "0x1234…" }, "total": 2, "lastAt": "2032-12-22T00:00:00Z", "firstAt": "2032-12-22T00:00:00Z" } ], "pageInfo": { "next": null, "prev": null } } } } ``` Coming soon ## Configure Post Actions You can add a Post Action to a Post at any time. For example, this can be useful to re-enable the Simple Collect Action on posts migrated from Lens Protocol v2 to v3. You MUST be authenticated as the Account Owner or Account Manager of the Post's author account to configure Post Actions on it. ### Prepare the Request First, use the `configurePostAction` action to configure a Post Action on a Post. ```ts filename="Simple Collect Action" import { evmAddress, postId } from "@lens-protocol/client"; import { configurePostAction } from "@lens-protocol/client/actions"; const result = await configurePostAction(sessionClient, { post: postId("42"), params: { simpleCollect: { payToCollect: { collectLimit: 100, erc20: { value: "100", currency: evmAddress("0x5678…"), }, }, }, }, }); ``` ```ts filename="Custom Post Action" import { blockchainData, evmAddress, postId } from "@lens-protocol/client"; import { configurePostAction } from "@lens-protocol/client/actions"; const result = await configurePostAction(sessionClient, { post: postId("42"), params: { unknown: { address: evmAddress("0x1234…"), params: [ { raw: { // 32 bytes key (e.g., keccak(name)) key: blockchainData("0xac5f04…"), // an ABI encoded data data: blockchainData("0x00"), }, }, ], }, }, }); ``` First, use the `configurePostAction` mutation to configure a Post Action on a Post. ```graphql filename="Collect w/ GHO" mutation { configurePostAction( request: { post: "42" params: { simpleCollect: { payToCollect: { collectLimit: 100, native: "100" } } } } ) { ... on ConfigurePostActionResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ```graphql filename="Collect w/ ERC20" mutation { configurePostAction( request: { post: "42" params: { simpleCollect: { payToCollect: { collectLimit: 100 erc20: { value: "100", currency: "0x5678…" } } } } } ) { ... on ConfigurePostActionResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ```graphql filename="Custom Post Action" mutation { configurePostAction( request: { post: "42" params: { unknown: { address: "0x1234…" params: [ { raw: { key: "0x4f…" # 32-byte key (e.g., keccak(name)) data: "0x00" # ABI-encoded data } } ] } } } ) { ... on ConfigurePostActionResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ### Handle Result Then, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,10" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await configurePostAction(sessionClient, { post: postId("42"), params: { // … }, }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,10" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await configurePostAction(sessionClient, { post: postId("42"), params: { // … }, }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Then, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. ## Enable/Disable Post Actions You can enable or disable a Post Action on a Post at any time, provided that the Post Action has already been configured for that Post. Post Actions can be configured either during Post creation or later, as described in the [Configure Post Actions](#configure-post-actions) section above. You MUST be authenticated as the Account Owner or Account Manager of the Post's author account to enable or disable Post Actions. ### Prepare the Request First, enable or disable a specific Post Action invoking the corresponding `enablePostAction` or `disablePostAction` actions. ```ts filename="Enable" import { postId } from "@lens-protocol/client"; import { enablePostAction } from "@lens-protocol/client/actions"; const result = await enablePostAction(sessionClient, { post: postId("1234…"), action: { simpleCollect: true }, }); ``` ```ts filename="Disable" import { postId } from "@lens-protocol/client"; import { disablePostAction } from "@lens-protocol/client/actions"; const result = await disablePostAction(sessionClient, { post: postId("1234…"), action: { simpleCollect: true }, }); ``` First, enable or disable a specific Post Action invoking the corresponding `enablePostAction` or `disablePostAction` mutations. ```graphql filename="Enable" mutation { enablePostAction( request: { post: "1234…", action: { simpleCollect: true } } ) { ... on EnablePostActionResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ```graphql filename="Disable" mutation { disablePostAction( request: { post: "1234…", action: { simpleCollect: true } } ) { ... on DisablePostActionResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ### Handle Result Then, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await enablePostAction(sessionClient, { post: postId("1234…"), action: { simpleCollect: true }, }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await enablePostAction(sessionClient, { post: postId("1234…"), action: { simpleCollect: true }, }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Then, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. --- ## Building a Post Action The Post Actions are defined by the `IPostAction` interface, which basically requires three functions, one to configure the action, one to execute it, and another to disable it. ```solidity interface IPostAction { function configure(address originalMsgSender, address feed, uint256 postId, KeyValue[] calldata params) external returns (bytes memory); function execute(address originalMsgSender, address feed, uint256 postId, KeyValue[] calldata params) external returns (bytes memory); function setDisabled( address originalMsgSender, address feed, uint256 postId, bool isDisabled, KeyValue[] calldata params ) external returns (bytes memory); } ``` ### But first, let's talk about the ActionHub Before we dive into each of the functions from the `IPostAction` interface, we need to talk about the `ActionHub`. The `ActionHub` is a special contract that acts as the entry point for every Action in the Lens Protocol. The purpose of that is to help discovery of Actions, having a single point where all main Action-related events are emitted. So, each function of the `IPostAction` interface must be only callable by `ActionHub`. For this, you can inherit [`BasePostAction` contract](https://github.com/lens-protocol/lens-v3/blob/development/contracts/actions/post/base/BasePostAction.sol), which acts as template for your custom Post Action, applying the restrictions needed, so you do not need to worry about it. With this context, you can now understand why the first param of every `IPostAction` function is `originalMsgSender`: this is the address that called the `ActionHub` originally, either to configure, execute, or disable an Action, given that `msg.sender` will always have the `ActionHub` address in the context of your Action contract. ### Configuration The configuration of the Action is done through the `configure` function, which purpose is to initialize any required state that the Action might require to work properly. The function receives four parameters: - `originalMsgSender`: the address of the original msg.sender that invoked the ActionHub (as explained above). - `feed`: the address of the Feed where the Post the Action is being configured for belongs to. - `postId`: the ID of the Post for which the Action is configured for. - `params`: array of key-value pairs whose values can be decoded into any extra custom configuration parameters that the Action could require to work. Return of the `configure` function is bytes - some Actions might want to return custom information to the caller. This `configure` function could be called by anyone (through the ActionHub, as explained above), it will depend on the implementation of the Action and its purpose who is allowed to invoke it (based on originalMsgSender). For example, there might be Actions that do not require initialization at all, so the `configure` function implementation will be empty, while other Actions might require the caller to match the author of the given Post. Keep in mind that if no prior configuration is required, the `configure` function must still be implemented and must not revert. Every time the `configure` function is called, the ActionHub will emit a `Lens_ActionHub_PostAction_Configured` event matching the parameters of the call (or `Lens_ActionHub_PostAction_Reconfigured` if the configuration was updated). ### Execution The execution of the Action is done through the `execute` function, which purpose is to perform the actual action logic that the Action implements. The function receives four parameters: - `originalMsgSender`: the address of the original msg.sender that invoked the ActionHub (as explained above). - `feed`: the address of the Feed where the Post the Action is being executed for belongs to. - `postId`: the ID of the Post for which the Action is being executed. - `params`: array of key-value pairs whose values can be decoded into any extra custom execution parameters that the Action could require to work. Return of the `execute` function is bytes - some Actions might want to return custom information to the caller. This `execute` function could be called by anyone (through the ActionHub, as explained above), it will depend on the implementation of the Action and its purpose who is allowed to invoke it (based on originalMsgSender). For example, there might be Actions that do not require any permissions at all, so the `execute` function implementation will be open, while other Actions might require the caller to match the author of the given Post. Every time the `execute` function is called, the ActionHub will emit a `Lens_ActionHub_PostAction_Executed` event matching the parameters of the call. The ActionHub will not allow to invoke the `execute` function on a Post if the Action is disabled for it. ### Disabling The disabling of the Action is done through the `setDisabled` function, which purpose is to stop an Action to be executable for a given Post. The same function can be used to enable the Action back. The function receives five parameters: - `originalMsgSender`: the address of the original msg.sender that invoked the ActionHub (as explained above). - `feed`: the address of the Feed where the Post the Action is being disabled/enabled for belongs to. - `postId`: the ID of the Post for which the Action is being disabled/enabled. - `isDisabled`: boolean indicating if the Action is being disabled or enabled. - `params`: array of key-value pairs whose values can be decoded into any extra custom disabling/enabling parameters that the Action could require to work. Return of the `setDisabled` function is bytes - some Actions might want to return custom information to the caller. This `setDisabled` function could be called by anyone (through the ActionHub, as explained above), it will depend on the implementation of the Action and its purpose who is allowed to invoke it (based on originalMsgSender). Every time the `setDisabled` function is called, the ActionHub will emit a `Lens_ActionHub_PostAction_Disabled` event matching the parameters of the call. ### Example Let's illustrate the process with an example. We will build a Simple Poll Vote Action so users can vote (e.g., Yes/No) on a Post representing a poll. This action allows any user to vote once per post. #### Define the Event and State First, we define an event `PollVoted` to signal when a vote occurs and a mapping `_hasVoted` to track addresses that have already voted on a specific post to prevent double voting. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import { IPostAction } from "contracts/interfaces/IPostAction.sol"; import { KeyValue } from "contracts/core/types/Types.sol"; import { BasePostAction } from "contracts/actions/post/base/BasePostAction.sol"; contract SimplePollVoteAction is BasePostAction { event PollVoted(address indexed voter, uint256 indexed postId, bool vote); mapping(address feed => mapping(uint256 postId => mapping(address voter => bool hasVoted))) private _hasVoted; mapping(address feed => mapping(uint256 postId => mapping(bool vote => uint256 count))) private _voteCounts; ``` #### Implement the Configure Function Next, implement the `configure` function. Although this simple poll action doesn't store complex configuration state on-chain (like the specific poll question), it's crucial for the **post author** to explicitly enable or "attach" this action to their post. This signals intent and allows UIs to discover that the post is intended as a poll. We add a requirement that `originalMsgSender` (the address calling the ActionHub) must be the author of the `postId` on the given `feed`. ```solidity // ... inside SimplePollVoteAction ... function _configure( address originalMsgSender, address feed, uint256 postId, KeyValue[] calldata params ) internal override returns (bytes memory) { require( originalMsgSender == IFeed(feed).getPostAuthor(postId), "Only author can configure" ); // Any extra configuration logic could be added here (e.g. mapping each possible vote type to some string) // Emitting an event Lens_ActionHub_PostAction_Configured happens automatically via ActionHub return ""; } ``` #### Implement the Execute Function Implement the `execute` function. This is where the core voting logic resides. First, we check if the `originalMsgSender` (the user initiating the action via the ActionHub) has already voted on this `postId`. If they have, the transaction reverts. ```solidity // ... inside SimplePollVoteAction ... function _execute( address originalMsgSender, address feed, uint256 postId, KeyValue[] calldata params ) external override returns (bytes memory) { require(!_hasVoted[feed][postId][originalMsgSender], "Already voted"); _hasVoted[feed][postId][originalMsgSender] = true; bool voteFound; bool vote; for (uint256 i = 0; i < params.length; i++) { if (params[i].key == keccak256("lens.param.vote")) { voteFound = true; vote = abi.decode(params[i].value, (bool)); break; } } require(voteFound, "Vote not found in params"); _voteCounts[feed][postId][vote]++; return abi.encode(vote); } ``` We can also add some getters for vote counts and then the `SimplePollVoteAction` is ready to be deployed and used. See the full code below: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import { IPostAction } from "contracts/extensions/action/ActionHub.sol"; import { KeyValue } from "contracts/core/types/Types.sol"; import { BasePostAction } from "contracts/actions/post/base/BasePostAction.sol"; import { IFeed } from "contracts/interfaces/IFeed.sol"; /** * @title SimplePollVoteAction * @notice A simple post action allowing users to cast a boolean vote (e.g., Yes/No) on a post. * Prevents double voting. */ contract SimplePollVoteAction is BasePostAction { event PollVoted(address indexed voter, uint256 indexed postId, bool vote); // feed => postId => voter => hasVoted mapping(address => mapping(uint256 => mapping(address => bool))) private _hasVoted; /** * @notice Configures the SimplePollVote Action for a given post. * @param originalMsgSender The address initiating the configuration via the ActionHub. Must be post author. * @param feed The address of the feed contract where the post exists. * @param postId The ID of the post being configured. * @param params Not used * @return bytes Empty bytes. */ function _configure( address originalMsgSender, address feed, uint256 postId, KeyValue[] calldata params ) internal override returns (bytes memory) { require( originalMsgSender == IFeed(feed).getPostAuthor(postId), "Only author can configure" ); // Any extra configuration logic could be added here (e.g. mapping each possible vote type to some string) // Emitting an event Lens_ActionHub_PostAction_Configured happens automatically via ActionHub return ""; } /** * @notice Executes a vote on a given post. * @param originalMsgSender The address initiating the vote via the ActionHub. * @param feed The address of the feed contract where the post exists. * @param postId The ID of the post being voted on. * @param params Array of key-value pairs. Expected to contain at least one element, * where the `value` of the first element is the ABI-encoded boolean vote. * @return bytes Empty bytes. * Requirements: * - The `originalMsgSender` must not have voted on this `postId` before. * - `params` must not be empty and the first element's value must be abi-decodable as a boolean. */ function _execute( address originalMsgSender, address feed, uint256 postId, KeyValue[] calldata params ) external override returns (bytes memory) { require(!_hasVoted[feed][postId][originalMsgSender], "Already voted"); _hasVoted[feed][postId][originalMsgSender] = true; bool voteFound; bool vote; for (uint256 i = 0; i < params.length; i++) { if (params[i].key == keccak256("lens.param.vote")) { voteFound = true; vote = abi.decode(params[i].value, (bool)); break; } } require(voteFound, "Vote not found in params"); _voteCounts[feed][postId][vote]++; return abi.encode(vote); } /** * @notice Gets the vote counts for a specific post. * @param feed The address of the feed contract where the post exists. * @param postId The ID of the post to get vote counts for. * @return (uint256, uint256) A tuple containing the counts for false and true votes respectively. */ function getVoteCounts(address feed, uint256 postId) external view returns (uint256 ya, uint256 nay) { return (_voteCounts[feed][postId][false], _voteCounts[feed][postId][true]); } } ``` ================ File: src/pages/protocol/feeds/post-rules.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Post Rules This guide will explain how to use Post Rules and how to implement custom ones. --- ## Using Post Rules When creating an new Post or a Quote, you can specify Post Rules that control how other Accounts can interact with it. These restrictions carry over to any Comments, or Comments on Comments, that are made on the post. For [Custom Feeds](./custom-feeds), these rules work in combination with any [Feed Rules](./feed-rules) set on the given feed. This section presumes you are familiar with the process of [creating a Post](./post) on Lens. ### Followers Only Post Rule Lens provides a built-in `FollowerOnlyPostRule`, which restricts interactions with a post to only those Accounts that follow the author. You can specify if the restriction applies to the Global Lens Graph or a [Custom Graph](../graphs/custom-graphs). Additionally, you can specify whether followers are allowed to comment, quote, and/or repost. ```ts filename="Post with Followers Only Rule" import { evmAddress, uri } from "@lens-protocol/client"; const result = await post(sessionClient, { contentUri: uri("lens://4f91ca…"), rules: { required: [ { followersOnlyRule: { graph: evmAddress("0x1234…"), quotesRestricted: true, repliesRestricted: true, repostRestricted: true, }, }, ], }, }); ``` ```graphql filename="Post with Followers Only Rule" mutation { post( request: { contentUri: "lens://4f91ca…" rules: { required: [ { followersOnlyRule: { graph: "0x1234…" quotesRestricted: true repliesRestricted: true repostRestricted: true } } ] } } ) { ... on PostResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on PostOperationValidationFailed { reason } ... on TransactionWillFail { reason } } } ``` ### Custom Post Rules You can also configure [custom Post Rules](#building-a-post-rule) as follows: ```ts filename="Post with Custom Rules" import { blockchainData, evmAddress, PostRuleExecuteOn, uri, } from "@lens-protocol/client"; const result = await post(sessionClient, { contentUri: uri("lens://4f91ca…"), rules: { required: [ { unknownRule: { address: evmAddress("0x1234…"), executeOn: [PostRuleExecuteOn.CreatingPost], params: [ { raw: { // 32 bytes key (e.g., keccak(name)) key: blockchainData("0xac5f04…"), // an ABI encoded value data: blockchainData("0x00"), }, }, ], }, }, ], }, }); ``` ```graphql filename="Post with Custom Rules" mutation { post( request: { contentUri: "lens://4f91ca…" rules: { required: [ { unknownRule: { address: "0x1234…" executeOn: ["CREATING_POST"] params: [ { raw: { key: "0x4f…" # 32-byte key (e.g., keccak(name)) data: "0x00" # ABI-encoded value } } ] } } ] } } ) { ... on PostResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on PostOperationValidationFailed { reason } ... on TransactionWillFail { reason } } } ``` Coming soon Where `raw[n].key` and `raw[n].value` are encoded hex strings specific to the desired rule. ### Combining Rules Additionally, multiple rules can be combined: ```ts filename="Post with Multiple Rules" import { evmAddress, uri, PostRuleExecuteOn } from "@lens-protocol/client"; const result = await post(sessionClient, { contentUri: uri("lens://4f91ca…"), rules: { required: [ { followersOnlyRule: { // … }, }, ], anyOf: [ { unknownRule: { address: evmAddress("0x1234…"), // … }, }, { unknownRule: { address: evmAddress("0x5678…"), // … }, }, ], }, }); ``` ```graphql filename="Post with Multiple Rules" mutation { post( request: { contentUri: "lens://4f91ca…", rules: { required: [ { followersOnlyRule: { # … } } ], anyOf: [ { unknownRule: { address: "0x1234…" # … } }, { unknownRule: { address: "0x5678…" # … } } ] } } ) { ... on PostResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on PostOperationValidationFailed { reason } ... on TransactionWillFail { reason } } } ``` --- ## Building a Post Rule Let's illustrate the process with an example. We will build a custom Post Rule that only allows a limited amount of replies to a Post. To build a custom Post Rule, you must implement the following `IPostRule` interface: ```solidity filename="IPostRule.sol" interface IPostRule { function configure(bytes32 configSalt, uint256 postId, KeyValue[] calldata ruleParams) external; function processCreatePost( bytes32 configSalt, uint256 rootPostId, uint256 postId, CreatePostParams calldata postParams, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external; function processEditPost( bytes32 configSalt, uint256 rootPostId, uint256 postId, EditPostParams calldata postParams, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external; } ``` Each function of this interface must assume to be invoked by the Feed contract where the rule is applied. In other words, assume the `msg.sender` will be the Feed contract. A Lens dependency package with all relevant interfaces will be available soon. ### Implement the Configure Function First, implement the `configure` function. This function initializes any required state for the rule to work correctly for a specific post configuration. It receives three parameters: - `configSalt`: A 32-byte value allowing the same rule contract to be used multiple times with different configurations for the same post. The triple (Feed Address, Post ID, Configuration Salt) identifies a unique rule configuration instance. - `postId`: The ID of the post for which this rule is configured for. - `ruleParams`: An array of key-value pairs that can be used to pass any custom configuration parameters to the rule. Each key is `bytes32`, we put the hash of the parameter name there, and each value is `bytes`, we set the ABI-encoded parameter value there. Being an array allows the rule to define which parameters are optional and which are required, acting accordingly when any of them are not present. The `configure` function can be called multiple times by the same Feed, passing the same `postId` and `configSalt`, to update an existing rule configuration (i.e., reconfigure it). Reply posts and reposts cannot have their own rules configured directly on them. Instead, they inherit the rules enforced by their root post. Only root posts (original posts and quotes) can have rules configured directly on them. In our example, we need to configure the maximum number of replies allowed for the given `postId`. We'll decode an integer parameter from `ruleParams`. Let's define storage mappings for the replies limit and for the counter: ```solidity contract LimitedRepliesPostRule is IPostRule { mapping(address feed => mapping(uint256 postId => mapping(bytes32 configSalt => uint256 limit))) internal _repliesLimit; mapping(address feed => mapping(uint256 postId => mapping(bytes32 configSalt => uint256 counter))) internal _repliesCounter; } ``` The configuration parameters are stored in the mappings using the Namespace contract address (`msg.sender`) and the configuration salt as keys. With this setup, the same rule can be used by different Namespaces, as well as be used by the same Namespace many times. Now let's code the `configure` function itself: ```solidity contract LimitedRepliesPostRule is IPostRule { mapping(address feed => mapping(uint256 postId => mapping(bytes32 configSalt => uint256 limit))) internal _repliesLimit; mapping(address feed => mapping(uint256 postId => mapping(bytes32 configSalt => uint256 counter))) internal _repliesCounter; function configure(bytes32 configSalt, uint256 postId, KeyValue[] calldata ruleParams) external override { uint256 limit = 0; for (uint256 i = 0; i < ruleParams.length; i++) { if (ruleParams[i].key == keccak256("lens.param.repliesLimit")) { limit = abi.decode(ruleParams[i].value, (uint256)); break; } } // Require a limit to be explicitly set during configuration require(limit > 0); // We store the limit... _repliesLimit[msg.sender][postId][configSalt] = limit; // ...but we do not do anything with the counter here, allowing reconfiguration of the limit // without losing the current reply count. } } ``` As you can see, we treat the `repliesLimit` parameter as required. And the rule treats the counter state carefully, by not overriding its value, knowing that the rule can be reconfigured at any time. ### Implement the Process Create Post Function Next, implement the `processCreatePost` function. This function is invoked by the Feed contract every time a Post is being created in relation to the Post that has this rule configured (which ID is `rootPostId`), to decide to allow or reject its creation. It receives: - `configSalt`: Identifies the specific rule configuration instance, in combination with the feed address (`msg.sender`) and the `rootPostId`. - `rootPostId`: The ID of the post where the rule was configured. - `postId`: The ID of the new post trying to be created. - `postParams`: The parameters of the new post being created (includes `author`, `contentURI`, `repliedPostId`, `quotedPostId`, `repostedPostId`, etc). - `primitiveParams`: Additional parameters that were passed to the Feed's `createPost` function call. - `ruleParams`: Additional parameters specific to this rule execution. The function must revert if the rule's requirements are not met. For our example, we only care about applying the restriction when the post being created is a **reply**. We can identify a reply by checking if `postParams.repliedPostId` is non-zero. 1. Check if `postParams.repliedPostId != 0`. 2. If it is a reply, increment the reply counter for the `rootPostId` and `configSalt`. 3. Retrieve the `repliesLimit` for the `rootPostId` and `configSalt`. 4. Require that the incremented counter does not exceed the limit. ```solidity import {CreatePostParams} from "contracts/core/interfaces/IFeed.sol"; // Assumed import import {Errors} from "contracts/core/types/Errors.sol"; // Assumed import contract LimitedRepliesPostRule is IPostRule { mapping(address feed => mapping(uint256 postId => mapping(bytes32 configSalt => uint256 limit))) internal _repliesLimit; mapping(address feed => mapping(uint256 postId => mapping(bytes32 configSalt => uint256 counter))) internal _repliesCounter; // ... function processCreatePost( bytes32 configSalt, uint256 rootPostId, uint256 postId, CreatePostParams calldata postParams, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external override { // Only apply the restriction if the post being created is a reply if (postParams.repliedPostId != 0) { // Increment the reply counter uint256 replyCounter = ++_repliesCounter[msg.sender][rootPostId][configSalt]; // Get the reply limit uint256 replyLimit = _repliesLimit[msg.sender][rootPostId][configSalt]; // Require the counter to not exceed the limit require(replyCounter <= replyLimit); } } } ``` ### Implement the Process Edit Post Function Next, implement the `processEditPost` function. This function is invoked by the Feed contract every time a Post related to the Post that has this rule configured (which ID is `rootPostId`) is being edited, then the rule can decide to approve or reject the editing of it. It receives: - `configSalt`: Identifies the specific rule configuration instance, in combination with the feed address (`msg.sender`) and the `rootPostId`. - `rootPostId`: The ID of the post where the rule was configured. - `postId`: The ID of the new post trying to be edited. - `postParams`: The parameters of the post to be edited. - `primitiveParams`: Additional parameters that were passed to the Feed's `editPost` function call. - `ruleParams`: Additional parameters specific to this rule execution. The function must revert if the rule's requirements are not met. In our example, we only restrict the amount of replies at creation time, not the editing of existing posts (i.e. existing replies are allowed to be edited). Therefore, this function does not need to impose any restrictions. As good practice, we revert with `NotImplemented` error in case the rule is accidentally enabled for this selector. ```solidity contract LimitedRepliesPostRule is IPostRule { // ... function processEditPost( bytes32 configSalt, uint256 rootPostId, uint256 postId, EditPostParams calldata postParams, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external pure override { revert Errors.NotImplemented(); } } ``` Now the `LimitedRepliesPostRule` is ready to be applied to any root Post under any Feed. See the full code below: ```solidity contract LimitedRepliesPostRule is IPostRule { mapping(address feed => mapping(uint256 postId => mapping(bytes32 configSalt => uint256 limit))) internal _repliesLimit; mapping(address feed => mapping(uint256 postId => mapping(bytes32 configSalt => uint256 counter))) internal _repliesCounter; function configure(bytes32 configSalt, uint256 postId, KeyValue[] calldata ruleParams) external override { uint256 limit = 0; for (uint256 i = 0; i < ruleParams.length; i++) { if (ruleParams[i].key == keccak256("lens.param.repliesLimit")) { limit = abi.decode(ruleParams[i].value, (uint256)); break; } } // Require a limit to be explicitly set during configuration require(limit > 0); // We store the limit... _repliesLimit[msg.sender][postId][configSalt] = limit; // ...but we do not do anything with the counter here, allowing reconfiguration of the limit // without losing the current reply count. } function processCreatePost( bytes32 configSalt, uint256 rootPostId, uint256 postId, CreatePostParams calldata postParams, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external override { // Only apply the restriction if the post being created is a reply if (postParams.repliedPostId != 0) { // Increment the reply counter uint256 replyCounter = ++_repliesCounter[msg.sender][rootPostId][configSalt]; // Get the reply limit uint256 replyLimit = _repliesLimit[msg.sender][rootPostId][configSalt]; // Require the counter to not exceed the limit require(replyCounter <= replyLimit); } } function processEditPost( bytes32 configSalt, uint256 rootPostId, uint256 postId, EditPostParams calldata postParams, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external pure override { revert Errors.NotImplemented(); } } ``` Stay tuned for API integration of rules and more guides! ================ File: src/pages/protocol/feeds/post.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Create a Post This guide will walk you through the process of creating a Post. --- Lens Post content, including text, images, videos, and more, is stored in what's known as Post Metadata. This metadata is a JSON file linked to the Lens Post via its public URI. ## Your First Post To create a Post on Lens, follow these steps. You MUST be authenticated as Account Owner or Account Manager to post on Lens. ### Create Post Metadata First, construct a Post Metadata object with the necessary content. The Post Metadata Standard is part of the [Lens Metadata Standards](../best-practices/metadata-standards) and covers various content types. Below are the most common ones. Used to describe content that is text-only, such as a message or a comment. ```ts filename="Text-only" import { textOnly } from "@lens-protocol/metadata"; const metadata = textOnly({ content: `GM! GM!`, }); ``` See `textOnly(input): TextOnlyMetadata` reference [doc](https://lens-protocol.github.io/metadata/functions/textOnly.html). Used to describe content where the primary focus is an audio file. ```ts filename="Audio" import { audio, MediaAudioMimeType } from "@lens-protocol/metadata"; const metadata = audio({ title: "Great song!", audio: { item: "https://example.com/song.mp3", type: MediaAudioMimeType.MP3, artist: "John Doe", cover: "https://example.com/cover.png", }, }); ``` See `audio(input): AudioMetadata` reference [doc](https://lens-protocol.github.io/metadata/functions/audio.html). Used to describe content where the primary focus is an image. ```ts filename="Image" import { image, MediaImageMimeType, MetadataLicenseType, } from "@lens-protocol/metadata"; const metadata = image({ title: "Touch grass", image: { item: "https://example.com/image.png", type: MediaImageMimeType.PNG, altTag: "Me touching grass", license: MetadataLicenseType.CCO, }, }); ``` See `image(input): ImageMetadata` reference [doc](https://lens-protocol.github.io/metadata/functions/image.html). Used to describe content where the primary focus is a video file. ```ts filename="Video" import { MetadataLicenseType, MediaVideoMimeType, video, } from "@lens-protocol/metadata"; const metadata = video({ title: "Great video!", video: { item: "https://example.com/video.mp4", type: MediaVideoMimeType.MP4, cover: "https://example.com/thumbnail.png", duration: 123, altTag: "The video of my life", license: MetadataLicenseType.CCO, }, content: ` In this video I will show you how to make a great video. And maybe I will show you how to make a great video about making a great video. `, }); ``` See `video(input): VideoMetadata` reference [doc](https://lens-protocol.github.io/metadata/functions/video.html). Used to describe long-form text content such as blog posts, articles, and news stories. ```ts filename="Article" import { article } from "@lens-protocol/metadata"; const metadata = article({ title: "Great Question" content: ` ## Heading My article is great ## Question What is the answer to life, the universe and everything? ## Answer 42 ![The answer](https://example.com/answer.png) `, tags: ["question", "answer"], }); ``` See `article(input): ArticleMetadata` reference [doc](https://lens-protocol.github.io/metadata/functions/article.html). There are also helpers for more specialized content types: - [checkingIn](https://lens-protocol.github.io/metadata/functions/checkingIn.html) - to share your location with your community - [embed](https://lens-protocol.github.io/metadata/functions/embed.html) - to share embeddable resources such as games or mini-apps - [event](https://lens-protocol.github.io/metadata/functions/event.html) - for sharing physical or virtual events - [link](https://lens-protocol.github.io/metadata/functions/link.html) - for sharing a link - [liveStream](https://lens-protocol.github.io/metadata/functions/liveStream.html) - for scheduling a live stream event - [mint](https://lens-protocol.github.io/metadata/functions/mint.html) - for sharing a link to mint an NFT - [space](https://lens-protocol.github.io/metadata/functions/space.html) - for organizing a social space - [story](https://lens-protocol.github.io/metadata/functions/story.html) - for sharing audio, image, or video content in a story format - [threeD](https://lens-protocol.github.io/metadata/functions/threeD.html) - for sharing a 3D digital asset - [transaction](https://lens-protocol.github.io/metadata/functions/transaction.html) - for sharing an interesting on-chain transaction - [shortVideo](https://lens-protocol.github.io/metadata/functions/shortVideo.html) - for publications where the main focus is a short video If you opted to manually create Metadata objects, make sure they conform to the corresponding Post JSON Schema. ```json filename="Text-only Example" { "$schema": "https://json-schemas.lens.dev/posts/text-only/3.0.0.json", "lens": { "id": "6418053f-89ae-4f9e-8362-8d1b6c0cdafb", "content": "GM!", "locale": "en", "mainContentFocus": "TEXT_ONLY" } } ``` Below is a list of Version 3 JSON Schemas: - [Article v3](https://json-schemas.lens.dev/posts/article/3.0.0.json) - for blog articles, news, or Medium-like posts - [Audio v3](https://json-schemas.lens.dev/posts/audio/3.0.0.json) - for Posts where the main focus is audio files - [CheckingIn v3](https://json-schemas.lens.dev/posts/checking-in/3.0.0.json) - to notify your community of your presence in a specific place - [Embed v3](https://json-schemas.lens.dev/posts/embed/3.0.0.json) - to share embeddable resources such as games or mini-apps - [Event v3](https://json-schemas.lens.dev/posts/event/3.0.0.json) - for physical or virtual events - [Image v3](https://json-schemas.lens.dev/posts/image/3.0.0.json) - for Posts where the main focus is images - [Link v3](https://json-schemas.lens.dev/posts/link/3.0.0.json) - for sharing a link - [LiveStream v3](https://json-schemas.lens.dev/posts/livestream/3.0.0.json) - for planning a live stream event - [Mint v3](https://json-schemas.lens.dev/posts/mint/3.0.0.json) - for sharing a link to mint an NFT - [Space v3](https://json-schemas.lens.dev/posts/space/3.0.0.json) - for planning a social space - [Story v3](https://json-schemas.lens.dev/posts/story/3.0.0.json) - for sharing audio, image, or video content in a story format - [TextOnly v3](https://json-schemas.lens.dev/posts/text-only/3.0.0.json) - for purely textual Posts (e.g., most comments) - [3D v3](https://json-schemas.lens.dev/posts/3d/3.0.0.json) - for sharing a 3D digital asset - [Transaction v3](https://json-schemas.lens.dev/posts/transaction/3.0.0.json) - for sharing an interesting on-chain transaction - [Video v3](https://json-schemas.lens.dev/posts/video/3.0.0.json) - for Posts where the main focus is videos ### Upload Post Metadata Then, upload the Post Metadata object to a public URI. ```ts import { textOnly } from "@lens-protocol/metadata"; import { storageClient } from "./storage-client"; const metadata = textOnly({ content: `GM! GM!`, }); const { uri } = await storageClient.uploadAsJson(metadata); console.log(uri); // e.g., lens://4f91ca… ``` This example uses [Grove storage](../../storage) to host the Metadata object. See the [Lens Metadata Standards](../best-practices/metadata-standards#host-metadata-objects) guide for more information on hosting Metadata objects. ### Create the Post Then, use the `post` action to create a Lens Post. ```ts filename="Simple Post" import { uri } from "@lens-protocol/client"; import { post } from "@lens-protocol/client/actions"; const result = await post(sessionClient, { contentUri: uri("lens://4f91ca…") }); ``` ```ts filename="Post with Rules" import { evmAddress, uri } from "@lens-protocol/client"; import { post } from "@lens-protocol/client/actions"; const result = await post(sessionClient, { contentUri: uri("lens://4f91ca…"), rules: { required: [ { followersOnlyRule: { graph: evmAddress("0x1234…"), quotesRestricted: true, repliesRestricted: true, repostRestricted: true, }, }, ], }, }); ``` Then, use the `post` mutation to create a Lens Post. See below examples for creating a Post, Comment, and Quote. ```graphql filename="Post on Global Feed" mutation { post(request: { contentUri: "lens://4f91ca…" }) { ... on PostResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on PostOperationValidationFailed { reason } ... on TransactionWillFail { reason } } } ``` ```graphql filename="Post with Rules" mutation { post( request: { contentUri: "lens://4f91ca…" rules: { required: [ { followersOnlyRule: { graph: "0x1234…" quotesRestricted: true repliesRestricted: true repostRestricted: true } } ] } } ) { ... on PostResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on PostOperationValidationFailed { reason } ... on TransactionWillFail { reason } } } ``` ```json filename="PostResponse" { "data": { "post": { "hash": "0x…" } } } ``` Coming soon To learn more about how to use Post Rules, see the [Post Rules](./post-rules) guide. ### Handle Result Finally, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,6" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await post(sessionClient, { contentUri: uri("lens://4f91ca…"), }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,6" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await post(sessionClient, { contentUri: uri("lens://4f91ca…"), }).andThen(handleOperationWith(signer)); ``` The Lens SDK example here leverages a functional approach to chaining operations using the `Result` object. See the [Error Handling](../best-practices/error-handling) guide for more information. See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Finally, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Coming soon ## Posting on a Custom Feed Whenever you are creating a _root Post_ (i.e., not a Comment or Quote) on a [Custom Feed](./custom-feeds), you need to verify that the logged-in Account has the necessary requisites to post on that Feed. As with Global Feeds, you MUST be authenticated as Account Owner or Account Manager to post on a Custom Feed. ### Check Feed Rules First, inspect the `feed.operations.canPost` field to determine whether the logged-in Account is allowed to post on the Custom Feed. ```ts filename="Check Rules" switch (feed.operations.canPost.__typename) { case "FeedOperationValidationPassed": // Posting is allowed break; case "FeedOperationValidationFailed": // Posting is not allowed console.log(feed.operations.canPost.reason); break; case "FeedOperationValidationUnknown": // Validation outcome is unknown break; } ``` Where: - `FeedOperationValidationPassed`: The logged-in Account can post on the Custom Feed. - `FeedOperationValidationFailed`: Posting is not allowed. The `reason` field explains why, and `unsatisfiedRules` lists the unmet requirements. - `FeedOperationValidationUnknown`: The Custom Feed has one or more _unknown rules_ requiring ad-hoc verification. The `extraChecksRequired` field provides the addresses and configurations of these rules. Treat the `FeedOperationValidationUnknown` as _failed_ unless you intend to support the specific rules. See [Feed Rules](./feed-rules) for more information. ### Create the Post Then, if allowed, post on the Custom Feed. For simplicity, we will omit the details on creating and uploading Post Metadata. ```ts filename="Post on Custom Feed" import { evmAddress, postId, uri } from "@lens-protocol/client"; import { post } from "@lens-protocol/client/actions"; const result = await post(sessionClient, { contentUri: uri("lens://4f91ca…"), commentOn: { post: postId("42"), // the post to comment on }, feed: evmAddress("0x1234…"), // the custom feed address }); ``` ```graphql filename="Post on Custom Feed" mutation { post( request: { contentUri: "lens://4f91ca…", commentOn: "42", feed: "0x1234…" } ) { ... on PostResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on PostOperationValidationFailed { reason } ... on TransactionWillFail { reason } } } ``` Coming soon Continue as you would with a regular Post on the Global Feed. ## Commenting on a Post The process of commenting on a Post is similar to creating a Post so we will focus on the differences. You MUST be authenticated as Account Owner or Account Manager to comment on Lens. ### Check Parent Rules First, inspect the `post.operations.canComment` field to determine whether the logged-in Account is allowed to comment on a given post. Some posts may have restrictions on who can comment on them. Comments cannot have their own [Post Rules](./post-rules). Instead, they inherit the rules of the root post (either a Post or a Quote) in the thread. The operations field of a comment reflects the rules of the root post. ```ts filename="Check Rules" switch (post.operations.canComment.__typename) { case "PostOperationValidationPassed": // Commenting is allowed break; case "PostOperationValidationFailed": // Commenting is not allowed console.log(post.operations.canComment.reason); break; case "PostOperationValidationUnknown": // Validation outcome is unknown break; } ``` Where: - `PostOperationValidationPassed`: The logged-in Account can comment on the Post. - `PostOperationValidationFailed`: Commenting is not allowed. The `reason` field explains why, and `unsatisfiedRules` lists the unmet requirements. - `PostOperationValidationUnknown`: The Post or its Feed (for custom Feeds) has one or more _unknown rules_ requiring ad-hoc verification. The `extraChecksRequired` field provides the addresses and configurations of these rules. Treat the `PostOperationValidationUnknown` as _failed_ unless you intend to support the specific rules. See [Post Rules](./post-rules) for more information. ### Create Comment Then, if allowed, create a Comment on the Post. Cross-feed commenting is currently not supported. If you find this feature valuable, please let us know by [opening an issue](https://github.com/lens-protocol/lens-sdk/issues). For simplicity, we will omit the details on creating and uploading Post Metadata. ```ts filename="Example" import { postId, uri } from "@lens-protocol/client"; import { post } from "@lens-protocol/client/actions"; const result = await post(sessionClient, { contentUri: uri("lens://4f91ca…"), commentOn: { post: postId("42"), // the post to comment on }, }); ``` ```graphql filename="Example" mutation { post(request: { contentUri: "lens://4f91ca…", commentOn: "42" }) { ... on PostResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on PostOperationValidationFailed { reason } ... on TransactionWillFail { reason } } } ``` Coming soon Continue as you would with a regular Post. ## Quoting a Post The process of quoting a Post is similar to creating a Post so we will focus on the differences. You MUST be authenticated as Account Owner or Account Manager to quote on Lens. ### Check Post Rules First, inspect the `post.operations.canQuote` field to determine whether the logged-in Account is allowed to quote a given Post. Some posts may have restrictions on who can quote them. ```ts filename="Check Rules" switch (post.operations.canQuote.__typename) { case "PostOperationValidationPassed": // Quoting is allowed break; case "PostOperationValidationFailed": // Quoting is not allowed console.log(post.operations.canQuote.reason); break; case "PostOperationValidationUnknown": // Validation outcome is unknown break; } ``` Where: - `PostOperationValidationPassed`: The logged-in Account can quote the Post. - `PostOperationValidationFailed`: Quoting is not allowed. The `reason` field explains why, and `unsatisfiedRules` lists the unmet requirements. - `PostOperationValidationUnknown`: The Post or its Feed (for custom Feeds) has one or more _unknown rules_ requiring ad-hoc verification. The `extraChecksRequired` field provides the addresses and configurations of these rules. Treat the `PostOperationValidationUnknown` as _failed_ unless you intend to support the specific rules. See [Post Rules](./post-rules) for more information. ### Create Quote Then, if allowed, create a Quote of the Post. Cross-feed quoting is currently not supported. If you find this feature valuable, please let us know by [opening an issue](https://github.com/lens-protocol/lens-sdk/issues). For simplicity, we will omit the details on creating and uploading Post Metadata. ```ts filename="Quote" import { postId, uri } from "@lens-protocol/client"; import { post } from "@lens-protocol/client/actions"; const result = await post(sessionClient, { contentUri: uri("lens://4f91ca…"), quoteOf: { post: postId("42"), // the post to quote }, }); ``` ```ts filename="Quote with Rules" import { evmAddress, postId, uri } from "@lens-protocol/client"; import { post } from "@lens-protocol/client/actions"; const result = await post(sessionClient, { contentUri: uri("lens://4f91ca…"), quoteOf: { post: postId("42"), // the post to quote }, rules: { required: [ { followersOnlyRule: { graph: evmAddress("0x1234…"), quotesRestricted: true, repliesRestricted: true, repostRestricted: true, }, }, ], }, }); ``` ```graphql filename="Quote" mutation { post(request: { contentUri: "lens://4f91ca…", quoteOf: "0x01" }) { ... on PostResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on PostOperationValidationFailed { reason } ... on TransactionWillFail { reason } } } ``` ```graphql filename="Quote with Rules" mutation { post( request: { contentUri: "lens://4f91ca…" quoteOf: "0x01" rules: { required: [ { followersOnlyRule: { graph: "0x1234…" quotesRestricted: true repliesRestricted: true repostRestricted: true } } ] } } ) { ... on PostResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on PostOperationValidationFailed { reason } ... on TransactionWillFail { reason } } } ``` Coming soon To learn more about how to use Post Rules, see the [Post Rules](./post-rules) guide. Then, continue as you would with a regular Post. ================ File: src/pages/protocol/feeds/timelines.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Timelines Timelines are personalized lists of posts, tailored for each account according to their social graph. --- ## Account Timeline The account timeline is the aggregated list of items (root posts, comments, reposts) generated from the account's social graph. The timeline is organized based on relevance and recency. You MUST be authenticated as the Account Owner or Account Manager to fetch any account timeline. Use the `fetchTimeline` action to fetch the account timeline. ```ts filename="Any Feed" import { evmAddress } from "@lens-protocol/client"; import { fetchTimeline } from "@lens-protocol/client/actions"; const result = await fetchTimeline(sessionClient, { account: evmAddress("0x1234…"), }); if (result.isErr()) { return console.error(result.error); } // items: Array: [{ id: UUID, primary: Post, ... }, ... ] const { items, pageInfo } = result.value; ``` ```ts filename="Global Feed" import { evmAddress } from "@lens-protocol/client"; import { fetchTimeline } from "@lens-protocol/client/actions"; const result = await fetchTimeline(sessionClient, { account: evmAddress("0x1234…"), filter: { feeds: [ { globalFeed: true, }, ], }, }); if (result.isErr()) { return console.error(result.error); } // Array: [{ id: UUID, primary: Post, ... }, ... ] const { items, pageInfo } = result.value; ``` ```ts filename="Custom Feeds" import { evmAddress } from "@lens-protocol/client"; import { fetchTimeline } from "@lens-protocol/client/actions"; const result = await fetchTimeline(sessionClient, { account: evmAddress("0x1234…"), filter: { feeds: [ { feed: evmAddress("0x5678…"), }, ], }, }); if (result.isErr()) { return console.error(result.error); } // Array: [{ id: UUID, primary: Post, ... }, ... ] const { items, pageInfo } = result.value; ``` ```ts filename="All App Feeds" import { evmAddress } from "@lens-protocol/client"; import { fetchTimeline } from "@lens-protocol/client/actions"; const result = await fetchTimeline(sessionClient, { account: evmAddress("0x1234…"), filter: { feeds: [ { app: evmAddress("0x9123…"), }, ], }, }); if (result.isErr()) { return console.error(result.error); } // Array: [{ id: UUID, primary: Post, ... }, ... ] const { items, pageInfo } = result.value; ``` ```ts filename="Other Filters" import { evmAddress, ContentWarning, MainContentFocus, } from "@lens-protocol/client"; import { fetchTimeline } from "@lens-protocol/client/actions"; const result = await fetchTimeline(sessionClient, { account: evmAddress("0x1234…"), filter: { // with specific metadata values metadata: { contentWarning: { oneOf: [ContentWarning.Sensitive] }, mainContentFocus: [MainContentFocus.Image], tags: { all: ["tagExample"] }, }, // and published via the specified apps apps: [evmAddress("0x5678…")], }, }); if (result.isErr()) { return console.error(result.error); } // items: Array: [{ id: UUID, primary: Post, ... }, ... ] const { items, pageInfo } = result.value; ``` See the [Pagination](../best-practices/pagination) guide for more information on how to handle paginated results. Use the `timeline` query to fetch the account timeline. ```graphql filename="Query" query { timeline( request: { account: "0x1234…" filter: { # optional, the timeline event type # eventType: [TimelineEventItemType!] # optional, filter by feeds (by default, all feeds are included) feeds: [ # optional, filter by global feed { globalFeed: true } # and/or, filter by feed address # { # feed: EvmAddress # } # and/or, filter by ALL feeds associated w/ an app address # { # app: EvmAddress # } ] # optional, filter by metadata metadata: { mainContentFocus: [IMAGE] # optional, filter by tags # tags: PostMetadataTagsFilter # optional, filter by content warning # contentWarning: PostMetadataContentWarningFilter } } } ) { items { id primary { ...Post } comments { ...Post } reposts { ...Repost } } pageInfo { prev next } } } ``` ```json filename="Response" { "data": { "timeline": { "items": [ { "id": "17a2b121-68a4-4372-a975-30f55b772294 ", "primary": { "id": "42", "author": { "address": "0x1234…", "username": { "value": "lens/wagmi" }, "metadata": { "name": "WAGMI", "picture": "https://example.com/wagmi.jpg" } }, "timestamp": "2022-01-01T00:00:00Z", "app": { "metadata": { "name": "Lens", "logo": "https://example.com/lens.jpg" } }, "metadata": { "content": "Hello, World!" }, "root": null, "quoteOf": null, "commentOn": null }, "comments": [ { "id": "43", "author": { "address": "0x1234…", "username": { "value": "lens/alice" }, "metadata": { "name": "Alice", "picture": "https://example.com/alice.jpg" } }, "timestamp": "2022-01-01T00:00:00Z", "app": { "metadata": { "name": "Lens", "logo": "https://example.com/lens.jpg" } }, "metadata": { "content": "Nice post!" } } ], "reposts": [] } ], "pageInfo": { "prev": null, "next": null } } } } ``` Where: - `id`: The timeline event identifier. - `primary`: The primary post of the timeline event. - `comments`: The list of comments that are related to the primary post. Note: it's not always parent -child relation, can be a nested comment. - `reposts`: The list of reposts related the primary post. The [Fetch Posts](..) guide goes into details of how to fetch `Post` and `Repost` details. See the [Pagination](../best-practices/pagination) guide for more information on how to handle paginated results. Coming soon ## Timeline Highlights Timeline Highlights is a curated selection of posts from an account timeline that have received the most engagement. This feature is useful for quickly catching up on the most popular content. Use the `fetchTimelineHighlights` action to fetch most popular posts and quotes from an account timeline. ```ts filename="Any Feed" import { evmAddress } from "@lens-protocol/client"; import { fetchTimelineHighlights } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchTimelineHighlights(client, { account: evmAddress("0x1234…"), }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="Global Feed" import { evmAddress } from "@lens-protocol/client"; import { fetchTimelineHighlights } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchTimelineHighlights(client, { account: evmAddress("0x1234…"), filter: { feeds: [ { globalFeed: true, }, ], }, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="Custom Feeds" import { evmAddress } from "@lens-protocol/client"; import { fetchTimelineHighlights } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchTimelineHighlights(client, { account: evmAddress("0x1234…"), filter: { feeds: [ { feed: evmAddress("0x5678…"), }, ], }, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="All App Feeds" import { evmAddress } from "@lens-protocol/client"; import { fetchTimelineHighlights } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchTimelineHighlights(client, { account: evmAddress("0x1234…"), filter: { feeds: [ { app: evmAddress("0x9123…"), }, ], }, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="Other Filters" import { evmAddress, ContentWarning, MainContentFocus, } from "@lens-protocol/client"; import { fetchTimelineHighlights } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchTimelineHighlights(client, { account: evmAddress("0x1234…"), filter: { // with specific metadata values metadata: { contentWarning: { oneOf: [ContentWarning.Sensitive] }, mainContentFocus: [MainContentFocus.Image], tags: { all: ["tagExample"] }, }, // and published via the specified apps apps: [evmAddress("0x1234…")], }, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; export const client = PublicClient.create({ environment: mainnet, }); ``` See the [Pagination](../best-practices/pagination) guide for more information on how to handle paginated results. Use the `timelineHighlights` query to fetch most popular posts and quotes from an account timeline. ```graphql filename="Query" query { timelineHighlights( request: { account: "0x1234…" filter: { # optional, filter by feeds (by default, all feeds are included) feeds: [ # optional, filter by global feed { globalFeed: true } # and/or, filter by feed address # { # feed: EvmAddress # } # and/or, filter by ALL feeds associated w/ an app address # { # app: EvmAddress # } ] # optional, filter by metadata metadata: { mainContentFocus: [IMAGE] # optional, filter by tags # tags: PostMetadataTagsFilter # optional, filter by content warning # contentWarning: PostMetadataContentWarningFilter } } } ) { items { ...Post } pageInfo { prev next } } } ``` ```json filename="Response" { "data": { "timelineHighlights": { "items": [ { "id": "42", "author": { "address": "0x1234…", "username": { "value": "lens/wagmi" }, "metadata": { "name": "WAGMI", "picture": "https://example.com/wagmi.jpg" } }, "timestamp": "2022-01-01T00:00:00Z", "app": { "metadata": { "name": "Lens", "logo": "https://example.com/lens.jpg" } }, "metadata": { "content": "Hello, World!" }, "root": null, "quoteOf": null, "commentOn": null } ], "pageInfo": { "prev": null, "next": null } } } } ``` The [Fetch Posts](..) guide goes into details of how to fetch `Post` details. See the [Pagination](../best-practices/pagination) guide for more information on how to handle paginated results. Coming soon ================ File: src/pages/protocol/getting-started/graphql.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # GraphQL Get started with the Lens GraphQL API. --- The Lens API utilizes [GraphQL](https://graphql.org/), a query language for APIs that allows clients to precisely request the data they need, leading to more efficient data retrieval. GraphQL queries are executed using a type system defined for your data. It is written in rust so it is blazing fast. ## Environments The API links below act as API endpoints as well as playgrounds for testing queries. | **Network** | **URL** | | -------------------------------------------------------- | -------------------------------------- | | [Lens API Mainnet](https://api.lens.xyz/graphql) | `https://api.lens.xyz/graphql` | | [Lens API Testnet](https://api.testnet.lens.xyz/graphql) | `https://api.testnet.lens.xyz/graphql` | ## Rate Limits If you require higher rate limits when using the Lens API in a server-to-server context, you can generate a Server API Key for your application in the [Lens Developer Dashboard](https://developer.lens.xyz/apps). Include this key in your requests using the `x-lens-app` header. **DO NOT** use the Server API Key in a client-side context. It is meant for server-to-server communication only. ```http filename="HTTP" POST /graphqlnew HTTP/1.1 Host: api.lens.xyz x-lens-app: ... ``` ## GraphQL Clients Below are examples of how to interact with the Lens API using different GraphQL clients. If you are connecting to the Lens API from a JavaScript or TypeScript environment, we recommend using the Lens [TypeScript SDK](./typescript). Use one of the examples below if you need more control over your GraphQL queries. The [URQL](https://formidable.com/open-source/urql/) client is a lightweight and flexible GraphQL client that can be used in both web and mobile applications. ```ts filename="urql.ts" import { gql, Client, cacheExchange, fetchExchange } from "urql"; const ENDPOINT = "https://api.lens.xyz/graphql"; const client = new Client({ url: ENDPOINT, exchanges: [cacheExchange, fetchExchange], }); const result = await client.query(gql` query { posts(request: { pageSize: TEN }) { items { id author { username { value } } metadata { ... on TextOnlyMetadata { content } } } pageInfo { prev next } } } `); console.log(result); ``` The [Apollo Client](https://www.apollographql.com/docs/react/get-started) is a comprehensive GraphQL client that can be used in web and mobile applications. ```ts filename="apollo.ts" import { ApolloClient, InMemoryCache, gql } from "@apollo/client"; const ENDPOINT = "https://api.lens.xyz/graphql"; const client = new ApolloClient({ uri: ENDPOINT, cache: new InMemoryCache(), }); const { data } = await client.query({ query: gql` query { posts(request: { pageSize: TEN }) { items { id author { username { value } } metadata { ... on TextOnlyMetadata { content } } } pageInfo { prev next } } } `, }); console.log(data); ``` Although not a specialized GraphQL client, the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) can be used to send GraphQL queries. ```ts filename="fetch.ts" const ENDPOINT = "https://api.lens.xyz/graphql"; const graphqlQuery = { query: ` query { posts(request: { pageSize: TEN }) { items { id author { username { value } } metadata { ... on TextOnlyMetadata { content } } } pageInfo { prev next } } } `, }; const response = await fetch(ENDPOINT, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(graphqlQuery), }); const data = await response.json(); console.log(data); ``` ================ File: src/pages/protocol/getting-started/react.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # React Get started with the Lens React SDK. --- The Lens React SDK is a collection of React Hooks for both web and React Native applications. It's the simplest way to interact with the Lens API and build decentralized applications on Lens Protocol. The Lens React SDK is actively being developed, with new features added regularly. To benefit from the latest improvements and features, we recommend always using the most recent version of the SDK. ## Getting Started To get started, follow the steps below. ### Install SDK First, install the `@lens-protocol/react` package using your package manager of choice. ```bash filename="npm" npm install @lens-protocol/react@canary ``` ```bash filename="yarn" yarn add @lens-protocol/react@canary ``` ```bash filename="pnpm" pnpm add @lens-protocol/react@canary ``` ### Define Fragments Next, define the structure of the data for the key entities in your application by creating [GraphQL fragments](https://graphql.org/learn/queries/#fragments) that customize the data you retrieve. This step is critical to keeping your queries efficient and focused, helping you avoid overfetching unnecessary data. See the example below for a few common fragments. ```ts filename="fragments/accounts.ts" import { graphql, MediaImageFragment, UsernameFragment } from "@lens-protocol/react"; export const AccountMetadataFragment = graphql( ` fragment AccountMetadata on AccountMetadata { name bio thumbnail: picture( request: { preferTransform: { fixedSize: { height: 128, width: 128 } } } ) picture } `, [MediaImageFragment] ); export const AccountFragment = graphql( ` fragment Account on Account { __typename username { ...Username } address metadata { ...AccountMetadata } } `, [UsernameFragment, AccountMetadataFragment] ); ``` ```ts filename="fragments/posts.ts" import { ArticleMetadataFragment, AudioMetadataFragment, TextOnlyMetadataFragment, ImageMetadataFragment, VideoMetadataFragment, graphql, } from "@lens-protocol/react"; export const PostMetadataFragment = graphql( ` fragment PostMetadata on PostMetadata { __typename ... on ArticleMetadata { ...ArticleMetadata } ... on AudioMetadata { ...AudioMetadata } ... on TextOnlyMetadata { ...TextOnlyMetadata } ... on ImageMetadata { ...ImageMetadata } ... on VideoMetadata { ...VideoMetadata } } `, [ ArticleMetadataFragment, AudioMetadataFragment, TextOnlyMetadataFragment, ImageMetadataFragment, VideoMetadataFragment, ] ); ``` ```ts filename="fragments/images.ts" import { graphql } from "@lens-protocol/react"; export const MediaImageFragment = graphql( ` fragment MediaImage on MediaImage { __typename full: item large: item(request: { preferTransform: { widthBased: { width: 2048 } } }) thumbnail: item( request: { preferTransform: { fixedSize: { height: 128, width: 128 } } } ) altTag license type } ` ); ``` Throughout this documentation, you’ll explore additional fragments and fields that you may want to tailor to your needs. See the Custom Fragments [best-practices guide](/protocol/best-practices/custom-fragments) for more information. ### TypeScript Definitions Then, create an `index.ts` where you extend the TypeScript definitions for the entities you are customizing. ```ts filename="fragments/index.ts" import type { FragmentOf } from "@lens-protocol/react"; import { AccountFragment, AccountMetadataFragment } from "./accounts"; import { PostMetadataFragment } from "./posts"; import { MediaImageFragment } from "./images"; declare module "@lens-protocol/react" { export interface Account extends FragmentOf {} export interface AccountMetadata extends FragmentOf {} export interface MediaImage extends FragmentOf {} export type PostMetadata = FragmentOf; } export const fragments = [ AccountFragment, PostMetadataFragment, MediaImageFragment, ]; ``` ### Create a PublicClient Next, create an instance of the `PublicClient` pointing to the desired environment. ```ts filename="client.ts (Mainnet)" import { PublicClient, mainnet } from "@lens-protocol/react"; import { fragments } from "./fragments"; export const client = PublicClient.create({ environment: mainnet, fragments, }); ``` ```ts filename="client.ts (Testnet)" import { PublicClient, testnet } from "@lens-protocol/react"; import { fragments } from "./fragments"; export const client = PublicClient.create({ environment: testnet, fragments, }); ``` ### Wrap Your App Finally, wrap your app with the `` component and pass the `PublicClient` instance you created in the previous step. ```tsx filename="App.tsx" highlight="1,7,9" import { LensProvider } from "@lens-protocol/react"; import { client } from "./client"; export function App() { return ( {/* Your application components */} ); } ``` That's it—you're ready to use the Lens React SDK in your app. ================ File: src/pages/protocol/getting-started/typescript.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # TypeScript Get started with the Lens TypeScript SDK. --- The Lens TypeScript SDK is a low-level API client for interacting with the Lens API. It provides a lightweight abstraction over the bare GraphQL API and is suitable for server-to-server communication or very bespoke client-side integrations where the Lens React SDK is not suitable. Designed with a modular, functional approach, this SDK draws inspiration from the _viem client-actions architecture_. It structures functionality into distinct, reusable actions, each focused on a specific API feature. ## Getting Started To get started, follow the steps below. ### Install SDK First, install the `@lens-protocol/client` package using your package manager of choice. ```bash filename="npm" npm install @lens-protocol/client@canary ``` ```bash filename="yarn" yarn add @lens-protocol/client@canary ``` ```bash filename="pnpm" pnpm add @lens-protocol/client@canary ``` ### Define Fragments Next, define the structure of the data for the key entities in your application by creating [GraphQL fragments](https://graphql.org/learn/queries/#fragments) that customize the data you retrieve. This step is critical to keeping your queries efficient and focused, helping you avoid overfetching unnecessary data. See the example below for a few common fragments. ```ts filename="fragments/accounts.ts" import { graphql, MediaImageFragment, UsernameFragment } from "@lens-protocol/react"; export const AccountMetadataFragment = graphql( ` fragment AccountMetadata on AccountMetadata { name bio thumbnail: picture( request: { preferTransform: { fixedSize: { height: 128, width: 128 } } } ) picture } `, [MediaImageFragment] ); export const AccountFragment = graphql( ` fragment Account on Account { __typename username { ...Username } address metadata { ...AccountMetadata } } `, [UsernameFragment, AccountMetadataFragment] ); ``` ```ts filename="fragments/posts.ts" import { ArticleMetadataFragment, AudioMetadataFragment, TextOnlyMetadataFragment, ImageMetadataFragment, VideoMetadataFragment, graphql, } from "@lens-protocol/client"; export const PostMetadataFragment = graphql( ` fragment PostMetadata on PostMetadata { __typename ... on ArticleMetadata { ...ArticleMetadata } ... on AudioMetadata { ...AudioMetadata } ... on TextOnlyMetadata { ...TextOnlyMetadata } ... on ImageMetadata { ...ImageMetadata } ... on VideoMetadata { ...VideoMetadata } } `, [ ArticleMetadataFragment, AudioMetadataFragment, TextOnlyMetadataFragment, ImageMetadataFragment, VideoMetadataFragment, ] ); ``` ```ts filename="fragments/images.ts" import { graphql } from "@lens-protocol/client"; export const MediaImageFragment = graphql( ` fragment MediaImage on MediaImage { __typename full: item large: item(request: { preferTransform: { widthBased: { width: 2048 } } }) thumbnail: item( request: { preferTransform: { fixedSize: { height: 128, width: 128 } } } ) altTag license type } ` ); ``` Throughout this documentation, you’ll explore additional fragments and fields that you may want to tailor to your needs. See the Custom Fragments [best-practices guide](/protocol/best-practices/custom-fragments) for more information. ### TypeScript Definitions Then, create an `index.ts` where you extend the TypeScript definitions for the entities you are customizing. ```ts filename="fragments/index.ts" import type { FragmentOf } from "@lens-protocol/client"; import { AccountFragment, AccountMetadataFragment } from "./accounts"; import { PostMetadataFragment } from "./posts"; import { MediaImageFragment } from "./images"; declare module "@lens-protocol/client" { export interface Account extends FragmentOf {} export interface AccountMetadata extends FragmentOf {} export interface MediaImage extends FragmentOf {} export type PostMetadata = FragmentOf; } export const fragments = [ AccountFragment, PostMetadataFragment, MediaImageFragment, ]; ``` ### Create a PublicClient Finally, create an instance of the `PublicClient` pointing to the desired environment. ```ts filename="client.ts (Mainnet)" import { PublicClient, mainnet } from "@lens-protocol/client"; import { fragments } from "./fragments"; export const client = PublicClient.create({ environment: mainnet, fragments, }); ``` ```ts filename="client.ts (Testnet)" import { PublicClient, testnet } from "@lens-protocol/client"; import { fragments } from "./fragments"; export const client = PublicClient.create({ environment: testnet, fragments, }); ``` That's it—you can now use the `@lens-protocol/client/actions` to interact with the Lens API. ## Additional Options Below are some additional options you can pass to the `PublicClient.create` method. ### Origin Header The [Authentication Flow](../authentication) requires request to be made with the HTTP Origin header. If you are logging in from an environment (e.g. Node.js) other than a browser, you need to set the `origin` option when creating the client. ```ts filename="client.ts" highlight="8" import { PublicClient, mainnet } from "@lens-protocol/client"; import { fragments } from "./fragments"; export const client = PublicClient.create({ environment: mainnet, fragments, origin: "https://myappdomain.xyz", }); ``` ### Server API Key If you need higher rate limits for server-to-server usage of the Lens API, you can generate a Server API Key for your application in the [Lens Developer Dashboard](https://developer.lens.xyz/apps). Provide this key when creating the client. **DO NOT** use the Server API Key in a client-side context. It is meant for server-to-server communication only. ```ts filename="client.ts" highlight="8" import { PublicClient, mainnet } from "@lens-protocol/client"; import { fragments } from "./fragments"; export const client = PublicClient.create({ environment: mainnet, fragments, apiKey: "", }); ``` ================ File: src/pages/protocol/graphs/custom-graphs.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Custom Graphs This guide will introduce the concept of Custom Graphs and how to create and manage them. --- As outlined in the [Graph](../../concepts/graph) concept page, there are two classes of Graph instances: - **The Global Graph**: This is the default, familiar graph, encompassing all connections to date. - **Custom Graphs**: These are app-specific graphs, accessible openly or governed by [Graph Rules](./rules). ## Create a Custom Graph To create a Graph, follow these steps. You MUST be authenticated as [Builder](../authentication) to create a Graph. ### Create Graph Metadata First, construct a Graph Metadata object with the necessary content. Use the `@lens-protocol/metadata` package to construct a valid `GraphMetadata` object: ```ts filename="Example" import { graph } from "@lens-protocol/metadata"; const metadata = graph({ name: "XYZ", description: "My custom graph description", }); ``` If you opted for manually create Metadata objects, make sure it conform to the [Graph Metadata JSON Schema](https://json-schemas.lens.dev/graph/1.0.0.json). ```json filename="Example" { "$schema": "https://json-schemas.lens.dev/graph/1.0.0.json", "lens": { "id": "6418053f-89ae-4f9e-8362-8d1b6c0cdafb", "name": "XYZ", "description": "My custom graph description" } } ``` ### Upload Graph Metadata Next, upload the Graph Metadata object to a public URI. ```ts import { storageClient } from "./storage-client"; const { uri } = await storageClient.uploadAsJson(metadata); console.log(uri); // e.g., lens://4f91ca… ``` This example uses [Grove storage](../../storage) to host the Metadata object. See the [Lens Metadata Standards](../best-practices/metadata-standards#host-metadata-objects) guide for more information on hosting Metadata objects. ### Deploy Graph Contract Next, deploy the Lens Graph smart contract. Use the `createGraph` action to deploy the Lens Graph smart contract. ```ts filename="Simple Graph" import { uri } from "@lens-protocol/client"; import { createGraph } from "@lens-protocol/client/actions"; const result = await createGraph(sessionClient, { metadataUri: uri("lens://4f91ca…"), }); ``` ```ts filename="Graph with Admins" import { evmAddress, uri } from "@lens-protocol/client"; import { createGraph } from "@lens-protocol/client/actions"; const result = await createGraph(sessionClient, { admins: [evmAddress("0x5071DeEcD24EBFA6161107e9a875855bF79f7b21")], metadataUri: uri("lens://4f91ca…"), }); ``` ```ts filename="Graph with Rules" import { evmAddress, uri } from "@lens-protocol/client"; import { createGraph } from "@lens-protocol/client/actions"; const result = await createGraph(sessionClient, { metadataUri: uri("lens://4f91ca…"), rules: { required: [ { groupGatedRule: { group: evmAddress("0x1234…"), }, }, ], }, }); ``` Use the `createGraph` mutation to deploy the Lens Graph smart contract. ```graphql filename="Simple Graph" mutation { createGraph(request: { metadataUri: "lens://4f91cab87ab5e4f5066f878b72…" }) { ... on CreateGraphResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ```graphql filename="Graph with Admins" mutation { createGraph( request: { metadataUri: "lens://4f91cab87ab5e4f5066f878b72…" admins: ["0x5071DeEcD24EBFA6161107e9a875855bF79f7b21"] } ) { ... on CreateGraphResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ```graphql filename="Graph with Rules" mutation { createGraph( request: { metadataUri: "lens://4f91cab87ab5e4f5066f878b72…" rules: { required: [{ groupGatedRule: { group: "0x1234…" } }] } } ) { ... on CreateGraphResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ```json filename="CreateGraphResponse" { "data": { "createGraph": { "hash": "0x…" } } } ``` To learn more about how to use Graph Rules, see the [Graph Rules](./graph-rules) guide. ### Handle Result Next, handle the result using the adapter for the library of your choice and wait for it to be indexed. ```ts filename="viem" highlight="1,8,9" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await createGraph(sessionClient, { metadataUri: uri("lens://4f91…"), // the URI from the previous step }) .andThen(handleOperationWith(walletClient)) .andThen(sessionClient.waitForTransaction); ``` ```ts filename="ethers" highlight="1,8,9" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await createGraph(sessionClient, { metadataUri: uri("lens://4f91…"), // the URI from the previous step }) .andThen(handleOperationWith(signer)) .andThen(sessionClient.waitForTransaction); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Next, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. ### Fetch New Graph Finally, fetch the newly created Graph using the `fetchGraph` action. ```ts filename="viem" highlight="1,10" import { fetchGraph } from "@lens-protocol/client/actions"; // … const result = await createGraph(sessionClient, { metadataUri: uri("lens://4f91…"), // the URI from the previous step }) .andThen(handleOperationWith(walletClientOrSigner)) .andThen(sessionClient.waitForTransaction) .andThen((txHash) => fetchGraph(sessionClient, { txHash })); if (result.isErr()) { return console.error(result.error); } // graph: Graph | null const graph = result.value; ``` Finally, fetch the newly created Graph using the `graph` query. ```graphql filename="Query" query { graph(request: { txHash: "0x1234…" }) { address metadata { name description } } } ``` ```json filename="Response" { "data": { "graph": { "address": "0xdeadbeef…", "metadata": { "name": "XYZ", "description": "My custom graph description" } } } } ``` ## Fetch a Graph Use the `fetchGraph` action to fetch a single Graph by address or by transaction hash. ```ts filename="By Address" import { evmAddress } from "@lens-protocol/client"; import { fetchGraph } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchGraph(client, { address: evmAddress("0x1234…"), }); if (result.isErr()) { return console.error(result.error); } const graph = result.value; ``` ```ts filename="By Tx Hash" import { txHash } from "@lens-protocol/client"; import { fetchGraph } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchGraph(client, { txHash: txHash("0x1234…"), }); if (result.isErr()) { return console.error(result.error); } const graph = result.value; ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; export const client = PublicClient.create({ environment: mainnet, }); ``` Use the `graph` query to fetch a single Graph by address or by transaction hash. ```graphql filename="Query" query { graph( request: { graph: "0xdeadbeef…" # OR # txHash: TxHash! } ) { address metadata { id name description } # other fields such as graph rules # will be documented in due course } } ``` ```json filename="Response" { "data": { "graph": { "address": "0xdeadbeef…" } } } ``` Coming soon ## Search Graphs Use the paginated `fetchGraphs` action to search for graphs. ```ts filename="Search By Query" import { fetchGraphs } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchGraphs(client, { filter: { searchBy: "graphName", }, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="Search by Managed" import { evmAddress } from "@lens-protocol/client"; import { fetchGraphs } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchGraphs(client, { filter: { managedBy: { includeOwners: true, // optional address: evmAddress("0x1234…"), }, }, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; export const client = PublicClient.create({ environment: mainnet, }); ``` Use the paginated `graphs` query to search for graphs. ```graphql filename="Query" query { graphs( request: { filter: { searchBy: 'graphName' # OPTIONAL # managedBy: { # includeOwners: true # optional # address: "0x1234…" # } } orderBy: LATEST_FIRST # other options: ALPHABETICAL, OLDEST_FIRST } ) { items { address createdAt metadata { id name description } # other fields such as graph rules # will be documented in due course } pageInfo { prev next } } } ``` ```json filename="Response" { "data": { "graphs": { "items": [ { "address": "0xdeadbeef…", "createdAt": "2021-09-01T00:00:00Z", "metadata": { "id": "6418053f-89ae-4f9e-8362-8d1b6c0cdafb", "name": "XYZ", "description": "My custom graph description" } } ], "pageInfo": { "prev": null, "next": "U29mdHdhcmU=" } } } } ``` Continue with the [Pagination](../best-practices/pagination) guide for more information on how to handle paginated results. ## Access Control The Graph contract supports two roles: _Owner_ and _Administrator_. Administrators can: - Update the Graph Metadata - Update the Graph Rules - Update the Graph Extra Data The Owner can do everything the administrators can do, plus transfer ownership of the Graph to another address. See the [Team Management](../best-practices/team-management) guide for more information on how to manage these roles. ================ File: src/pages/protocol/graphs/follow-rules.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Follow Rules This guide explains how to use Follow Rules and how to implement custom ones. --- Follow Rules allow [accounts](../../concepts/account) to add requirements or constraints that will be applied when another account tries to Follow them. ## Using Follow Rules Users have the option to apply Follow Rules to their accounts. These rules dictates the conditions under which another account can Follow them. For [Custom Graphs](./custom-graphs), these rules work in combination with [Graph Rules](./graph-rules) set on the given graph. This section presumes you are familiar with the process of [following an Account](./follow-unfollow) on Lens. ### Built-in Follow Rules #### Simple Payment Follow Rule The `SimplePaymentFollowRule` is a built-in rule that can be applied to an Account within the context of a specific Graph. It requires an ERC-20 payment from any Account attempting to follow it in order for the operation to succeed. A **1.5%** Lens treasury fee is deducted from the payment before the remaining amount is transferred to the designated recipient. #### Token Gated Follow Rule The `TokenGatedFollowRule` is a built-in rule that can be applied to an Account within the context of a specific Graph. It requires any other Account attempting to follow it to hold a certain balance of a token (both fungible and non-fungible are supported). ### Update Follow Rules You MUST be authenticated as Account Owner or Account Manager of the Account you intend to update the Follow Rules for. #### Submit Update Request First, prepare the update request with the new Follow Rules configuration. Use the `updateAccountFollowRules` action to update the Follow Rules for the logged-in Account. ```ts filename="SimplePaymentFollowRule" import { bigDecimal, evmAddress } from "@lens-protocol/client"; import { updateAccountFollowRules } from "@lens-protocol/client/actions"; const result = await updateAccountFollowRules(sessionClient, { toAdd: { required: [ { simplePaymentRule: { cost: { currency: evmAddress("0x5678…"), value: bigDecimal("10.42"), }, recipient: evmAddress("0x9012…"), }, }, ], }, }); if (result.isErr()) { return console.error(result.error); } ``` ```ts filename="TokenGatedFollowRule" import { bigDecimal, evmAddress, TokenStandard } from "@lens-protocol/client"; import { updateAccountFollowRules } from "@lens-protocol/client/actions"; const result = await updateAccountFollowRules(sessionClient, { toAdd: { required: [ { tokenGatedRule: { token: { currency: evmAddress("0x1234…"), standard: TokenStandard.Erc721, value: bigDecimal("1"), }, }, }, ], }, }); if (result.isErr()) { return console.error(result.error); } ``` ```ts filename="Custom Rule" import { evmAddress } from "@lens-protocol/client"; import { updateAccountFollowRules } from "@lens-protocol/client/actions"; const result = await updateAccountFollowRules(sessionClient, { toAdd: { required: [ { unknownRule: { address: evmAddress("0x1234…"), params: [ { raw: { // 32 bytes key (e.g., keccak(name)) key: blockchainData("0xac5f04…"), // an ABI encoded data data: blockchainData("0x00"), }, }, ], }, }, ], }, }); if (result.isErr()) { return console.error(result.error); } ``` ```ts filename="Remove Rules" import { evmAddress } from "@lens-protocol/client"; import { updateAccountFollowRules } from "@lens-protocol/client/actions"; const result = await updateAccountFollowRules(sessionClient, { toRemove: [account.rules.required[0].id], }); if (result.isErr()) { return console.error(result.error); } ``` Use the `updateAccountFollowRules` mutation to update the Follow Rules for the logged-in Account. ```graphql filename="SimplePaymentFollowRule" mutation { updateAccountFollowRules( request: { toAdd: { required: [ { simplePaymentRule: { cost: { currency: "0x5678…", value: "10.42" } recipient: "0x9012…" } } ] } } ) { ... on UpdateAccountFollowRulesResult { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ```graphql filename="TokenGatedFollowRule" mutation { updateAccountFollowRules( request: { toAdd: { required: [ { tokenGatedRule: { token: { standard: ERC721, currency: "0x1234…", value: "1" } } } ] } } ) { ... on UpdateAccountFollowRulesResult { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ```graphql filename="Custom Rule" mutation { updateAccountFollowRules( request: { toAdd: { required: [ { unknownRule: { address: "0x1234…" params: [ { raw: { key: "0x4f…" # 32-byte key (e.g., keccak(name)) data: "0x00" # ABI-encoded data } } ] } } ] } } ) { ... on UpdateAccountFollowRulesResult { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ```graphql filename="Remove Rules" mutation { updateAccountFollowRules(request: { toRemove: ["rule-id-1"] }) { ... on UpdateAccountFollowRulesResult { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` Coming soon #### Handle Result Then, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,7" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await updateAccountFollowRules(sessionClient, { // … }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,7" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await await updateAccountFollowRules(sessionClient, { // … }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Then, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Coming soon --- ## Building a Follow Rule Let's illustrate the process with an example. We will build a custom Follow Rule that once applied it will only accept Follows from accounts that you previously Followed in some Graph (particularly, the same Graph where the rule is applied can be used, so only the accounts that you Follow will be able to Follow you back). To build a custom Follow Rule, you must implement the following `IFollowRule` interface: ```solidity filename="IFollowRule.sol" interface IFollowRule { function configure(bytes32 configSalt, address account, KeyValue[] calldata ruleParams) external; function processFollow( bytes32 configSalt, address originalMsgSender, address followerAccount, address accountToFollow, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external; } ``` Each function of this interface must assume to be invoked by the Graph contract where the rule is applied. In other words, assume the `msg.sender` will be the Graph contract. A Lens dependency package with all relevant interfaces will be available soon. ### Implement the Configure Function First, implement the `configure` function. This function initializes any required state for the rule to work correctly for a specific account on a specific graph configuration. It receives three parameters: - `configSalt`: A 32-byte value allowing the same rule contract to be used multiple times with different configurations for the same account within the same graph. The triple (Graph Address, Account Address, Configuration Salt) identifies a unique rule configuration instance. - `account`: The address of the account for which this rule is being configured for. - `ruleParams`: An array of key-value pairs that can be used to pass any custom configuration parameters to the rule. Each key is `bytes32`, we put the hash of the parameter name there, and each value is `bytes`, we set the ABI-encoded parameter value there. Given that `ruleParams` is an array, this allows the rule to define which parameters are optional and which are required, acting accordingly when any of them are not present. The `configure` function can be called multiple times by the same Graph, passing the same `account` and `configSalt`, to update an existing rule configuration (i.e., reconfigure it). In our example, we need to configure which Graph should be checked to see if the `accountToFollow` is already following the `followerAccount`. We'll decode an address parameter representing this target Graph. Let's define a storage mapping to store this configuration: ```solidity contract AlreadyFollowedFollowRule is IFollowRule { mapping(address graph => mapping(bytes32 configSalt => mapping(address account => address graph))) _graphToCheck; } ``` The configuration parameters are stored using the Graph contract address (`msg.sender`), the `configSalt`, and the `account` as keys. This allows the same rule contract to be reused across different Graphs and Accounts as several times. Now let's code the `configure` function, decoding the target graph address from `ruleParams`: ```solidity contract AlreadyFollowedFollowRule is IFollowRule { mapping(address graph => mapping(bytes32 configSalt => mapping(address account => address graph))) _graphToCheck; function configure(bytes32 configSalt, address account, KeyValue[] calldata ruleParams) external override { address graph; for (uint256 i = 0; i < ruleParams.length; i++) { if (ruleParams[i].key == keccak256("lens.param.graph")) { graph = abi.decode(ruleParams[i].value, (address)); break; } } // A call to check if the address holds a valid Graph IGraph(graph).isFollowing(address(this), address(account)); _graphToCheck[msg.sender][configSalt][account] = graph; } } ``` Here, we made the graph a required parameter, given that when it is not present it will be `address(0)` and then revert on the `isFollowing` call. ### Implement the Process Follow Function Next, implement the `processFollow` function. This function is invoked by the Graph contract every time a Follow is executed, so then the rule logic can be applied to shape under which conditions this operation can succeed. The function receives: - `configSalt`: Identifies the specific rule configuration instance. - `originalMsgSender`: The address that invoked the `follow` function in the Graph contract. - `followerAccount`: The account taking the role of follower. - `accountToFollow`: The account that is being followed, which is the one the rule was configured for. - `primitiveParams`: Additional parameters that were passed to the Graph's `follow` function call. - `ruleParams`: Additional parameters specific to this rule execution. For our example, we need to: 1. Retrieve the configured graph to check. 2. Check if `accountToFollow` is already following `followerAccount` on that graph. 3. Revert if the `accountToFollow` is not already following the `followerAccount` on that graph. The `IGraph` interface contains this function in order to check whether an account is following another account or not: ```solidity function isFollowing( address followerAccount, address targetAccount ) external view returns (bool); ``` Let's see how each step looks like in code: ```solidity contract AlreadyFollowedFollowRule is IFollowRule { mapping(address graph => mapping(bytes32 configSalt => mapping(address account => address graph))) _graphToCheck; // ... function processFollow( bytes32 configSalt, address originalMsgSender, address followerAccount, address accountToFollow, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external view override { // Retrieve the configured graph to check IGraph graphToCheck = _graphToCheck[msg.sender][configSalt][accountToFollow]; // Check if `accountToFollow` is already following `followerAccount` on that graph bool isFollowedBack = graphToCheck.isFollowing(accountToFollow, followerAccount); // Revert if the `accountToFollow` is not already following the `followerAccount` on that graph require(isFollowedBack); } } ``` Now the `AlreadyFollowedFollowRule` is ready to be applied to any account under any Graph that supports Follow Rules. See the full code below: ```solidity contract AlreadyFollowedFollowRule is IFollowRule { mapping(address graph => mapping(bytes32 configSalt => mapping(address account => address graph))) _graphToCheck; function configure(bytes32 configSalt, address account, KeyValue[] calldata ruleParams) external override { address graph; for (uint256 i = 0; i < ruleParams.length; i++) { if (ruleParams[i].key == keccak256("lens.param.graph")) { graph = abi.decode(ruleParams[i].value, (address)); break; } } // A call to check if the address holds a valid Graph IGraph(graph).isFollowing(address(this), address(account)); _graphToCheck[msg.sender][configSalt][account] = graph; } function processFollow( bytes32 configSalt, address originalMsgSender, address followerAccount, address accountToFollow, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external view override { // Retrieve the configured graph to check IGraph graphToCheck = _graphToCheck[msg.sender][configSalt][accountToFollow]; // Check if `accountToFollow` is already following `followerAccount` on that graph bool isFollowedBack = graphToCheck.isFollowing(accountToFollow, followerAccount); // Revert if the `accountToFollow` is not already following the `followerAccount` on that graph require(isFollowedBack); } } ``` Stay tuned for API integration of rules and more guides! ================ File: src/pages/protocol/graphs/follow-unfollow.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Follow and Unfollow This guide explains how to follow and unfollow an Account on Lens. --- Lens Account can follow other Lens accounts on the Global Graph or on [Custom Graphs](../graphs/custom-graphs). Follow operations are regulated by [Follow Rules](./follow-rules) set on the target Account and by the [Graph Rule](./graph-rules) of the Graph where the follow relationship is established. ## Follow an Account To follow an Account, implement the following steps. You MUST be authenticated as Account Owner or Account Manager to follow an Account. ### Check Rules First, inspect the `account.operations` field to determine if you can follow an Account. ```ts filename="Account" type Account = { __typename: "Account"; address: EvmAddress; // … operations: LoggedInAccountOperations; }; ``` ```ts filename="LoggedInAccountOperations" type LoggedInAccountOperations = { __typename: "LoggedInAccountOperations"; // … canFollow: AccountFollowOperationValidationOutcome; isFollowedByMe: boolean; }; ``` ```ts filename="AccountFollowOperationValidationOutcome" type AccountFollowOperationValidationOutcome = | AccountFollowOperationValidationPassed | AccountFollowOperationValidationUnknown | AccountFollowOperationValidationFailed; ``` ```graphql filename="Account" fragment Account on Account { # … operations { # defaults to the Global Graph canFollow isFollowedByMe # or use aliases for a specific Graph # canFollowOnMyGraph: canFollow(request: { graph: "0x123…" }) # isFollowedByMeOnMyGraph: isFollowedByMe(request: { graph: "0x123…" }) # … } } ``` The `account.operations.canFollow` can assume one of the following values: ```ts filename="Check Rules" switch (account.operations.canFollow.__typename) { case "AccountFollowOperationValidationPassed": // Follow is allowed break; case "AccountFollowOperationValidationFailed": // Following is not possible console.log(account.operations.canFollow.reason); break; case "AccountFollowOperationValidationUnknown": // Validation outcome is unknown break; } ``` Where: - `AccountFollowOperationValidationPassed`: The logged-in Account can follow the target Account. - `AccountFollowOperationValidationFailed`: Following is not allowed. The `reason` field explains why. This could be for two reasons: - The logged-in Account already follows the target Account. In this case, the `account.operation.isFollowedByMe` field will be `true`. - The logged-in Account does not meet the follow criteria for the target Account. In this case, `unsatisfiedRules` lists the unmet requirements. - `AccountFollowOperationValidationUnknown`: The target Account or the Graph (for custom Graphs) has one or more _unknown rules_ requiring ad-hoc verification. The `extraChecksRequired` field provides the addresses and configurations of these rules. Treat the `AccountFollowOperationValidationUnknown` as _failed_ unless you intend to support the specific rules. See [Follow Rules](./follow-rules) for more information. ### Submit Follow Request Then, if your follow request is allowed, you can proceed with submitting the request. Then, use the `follow` action with the target Account address. ```ts filename="Follow on Global Graph" import { evmAddress } from "@lens-protocol/client"; import { follow } from "@lens-protocol/client/actions"; const result = await follow(sessionClient, { account: evmAddress("0x1234") }); ``` ```ts filename="Follow on Custom Graph" import { evmAddress } from "@lens-protocol/client"; import { follow } from "@lens-protocol/client/actions"; const result = await follow(sessionClient, { account: evmAddress("0x1234…"), graph: evmAddress("0x5678…"), // the custom Graph address }); ``` The Lens SDK example here leverages a functional approach to chaining operations using the `Result` object. See the [Error Handling](../best-practices/error-handling) guide for more information. Use the `follow` mutation to submit a follow request. ```graphql filename="Follow on Global Graph" mutation { follow(request: { account: "0x1234…" }) { ... on FollowResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on AccountFollowOperationValidationFailed { reason } ... on TransactionWillFail { reason } } } ``` ```graphql filename="Follow on Custom Graph" mutation { follow( request: { account: "0x1234…" graph: "0x5678…" # the custom Graph address # any required Graph Rule execution params # graphRulesProcessingParams: [GraphRulesProcessingParams!] # any required Follow Rule execution params # followRuleProcessingParams: [AccountFollowRulesProcessingParams!] } ) { ... on FollowResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on AccountFollowOperationValidationFailed { reason } ... on TransactionWillFail { reason } } } ``` ```json filename="FollowResponse" { "data": { "follow": { "hash": "0x…" } } } ``` Coming soon ### Handle Result Finally, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,7" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await follow(sessionClient, { account: evmAddress("0x1234"), }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,7" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await follow(sessionClient, { account: evmAddress("0x1234"), }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Finally, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Coming soon ## Unfollow an Account To unfollow an Account, implement the following steps. You MUST be authenticated as Account Owner or Account Manager to follow an Account. ### Check Rules First, inspect the `account.operations` field to determine if you can unfollow an Account. ```ts filename="Account" type Account = { __typename: "Account"; address: EvmAddress; // … operations: LoggedInAccountOperations; }; ``` ```ts filename="LoggedInAccountOperations" type LoggedInAccountOperations = { __typename: "LoggedInAccountOperations"; // … canUnfollow: AccountFollowOperationValidationOutcome; isFollowedByMe: boolean; }; ``` ```ts filename="AccountFollowOperationValidationOutcome" type AccountFollowOperationValidationOutcome = | AccountFollowOperationValidationPassed | AccountFollowOperationValidationUnknown | AccountFollowOperationValidationFailed; ``` ```graphql filename="Account" fragment Account on Account { # ... operations { # defaults to the Global Graph canUnfollow isFollowedByMe # or use aliases for a specific Graph # canUnfollowOnMyGraph: canUnfollow(request: { graph: "0x123…" }) # isFollowedByMeOnMyGraph: isFollowedByMe(request: { graph: "0x123…" }) } } ``` If `true`, you can unfollow the account. If `false`, you might not be following them, in which case the `operation.isFollowedByMe` field will also be `false`. Where: - `AccountFollowOperationValidationPassed`: The logged-in Account can unfollow the target Account. - `AccountFollowOperationValidationFailed`: Following is not allowed. The `reason` field explains why. This could be for two reasons: - The logged-in Account does not follow the target Account. In this case, the `account.operation.isFollowedByMe` field will be `false`. - The logged-in Account does not meet the follow criteria for the target Account. In this case, `unsatisfiedRules` lists the unmet requirements. - `AccountFollowOperationValidationUnknown`: The custom Graph has one or more _unknown rules_ requiring ad-hoc verification. The `extraChecksRequired` field provides the addresses and configurations of these rules. Treat the `AccountFollowOperationValidationUnknown` as _failed_ unless you intend to support the specific rules. See [Follow Rules](./follow-rules) for more information. ### Submit Unfollow Request Then, if your unfollow request is allowed, you can proceed with submitting the request. Then, use the `unfollow` action with the target Account address. ```ts filename="Unfollow on Global Graph" import { evmAddress } from "@lens-protocol/client"; import { unfollow } from "@lens-protocol/client/actions"; const result = await unfollow(sessionClient, { account: evmAddress("0x1234…"), }); ``` ```ts filename="Unfollow on Custom Graph" import { evmAddress } from "@lens-protocol/client"; import { unfollow } from "@lens-protocol/client/actions"; const result = await unfollow(sessionClient, { account: evmAddress("0x1234…"), graph: evmAddress("0x5678…"), // the custom Graph address }); ``` The Lens SDK example here leverages a functional approach to chaining operations using the `Result` object. See the [Error Handling](../best-practices/error-handling) guide for more information. Use the `unfollow` mutation to submit an unfollow request. ```graphql filename="Unfollow on Global Graph" mutation { unfollow(request: { account: "0x1234…" }) { ... on UnfollowResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on AccountFollowOperationValidationFailed { reason } ... on TransactionWillFail { reason } } } ``` ```graphql filename="Unfollow on Custom Graph" mutation { unfollow( request: { account: "0x1234…" graph: "0x5678…" # the custom feed address # any required Graph Rule execution params # graphRulesProcessingParams: [GraphRulesProcessingParams!] } ) { ... on UnfollowResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on AccountFollowOperationValidationFailed { reason } ... on TransactionWillFail { reason } } } ``` ```json filename="UnfollowResponse" { "data": { "follow": { "hash": "0x…" } } } ``` Coming soon ### Handle Result Finally, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,7" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await unfollow(sessionClient, { account: evmAddress("0x1234"), }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,7" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await unfollow(sessionClient, { account: evmAddress("0x1234"), }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Finally, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Coming soon
That's it—you now know how to follow and unfollow an Account on Lens, allowing to manage your social graph. ================ File: src/pages/protocol/graphs/graph-rules.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Graph Rules This guide explains how to use Graph Rules and how to implement custom ones. --- Graph Rules allow administrators to add requirements or constraints for when accounts follow other accounts on a given Graph. ## Using Graph Rules Lens provides two built-in Graph rules: - `TokenGatedGraphRule` - Requires an account to hold a certain token to follow another account. - `GroupGatedGraphRule` - Requires an account to be a member of a certain Group to follow another account. It is also possible to use custom Graph Rules to extend the functionality of your Graph. ### Create a Graph with Rules As part of creating [Custom Graph](./custom-graphs), you can pass a `rules` object that defines the `required` rules and/or an `anyOf` set, where satisfying any one rule allows following on a given Graph. These rules can be built-in or custom. This section presumes you are familiar with the process of [creating a custom Graph](./custom-graphs) on Lens. ```ts filename="TokenGatedGraphRule" import { bigDecimal, evmAddress, uri, TokenStandard, } from "@lens-protocol/client"; import { createGraph } from "@lens-protocol/client/actions"; const result = await createGraph(sessionClient, { metadataUri: uri("lens://4f91c…"), rules: { required: [ { tokenGatedRule: { token: { currency: evmAddress("0x1234…"), standard: TokenStandard.Erc721, value: bigDecimal("1"), }, }, }, ], }, }); ``` ```ts filename="GroupGatedGraphRule" import { evmAddress, uri } from "@lens-protocol/client"; import { createGraph } from "@lens-protocol/client/actions"; const result = await createGraph(sessionClient, { metadataUri: uri("lens://4f91c…"), rules: { required: [ { groupGatedRule: { group: evmAddress("0x1234…"), }, }, ], }, }); ``` ```ts filename="Custom Graph Rule" import { blockchainData, evmAddress, uri } from "@lens-protocol/client"; import { createGraph } from "@lens-protocol/client/actions"; const result = await createGraph(sessionClient, { metadataUri: uri("lens://4f91c…"), rules: { required: [ { unknownRule: { address: evmAddress("0x1234…"), params: [ { raw: { // 32 bytes key (e.g., keccak(name)) key: blockchainData("0xac5f04…"), // an ABI encoded value data: blockchainData("0x00"), }, }, ], }, }, ], }, }); ``` ```graphql filename="TokenGatedGraphRule" mutation { createGraph( request: { metadataUri: "lens://4f91c…" rules: { required: [ { tokenGatedRule: { token: { standard: ERC721, currency: "0x1234…", value: "1" } } } ] } } ) { ... on CreateGraphResponse { hash } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ```graphql filename="GroupGatedGraphRule" mutation { createGraph( request: { metadataUri: "lens://4f91c…" rules: { required: [{ groupGatedRule: { group: "0x1234…" } }] } } ) { ... on CreateGraphResponse { hash } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ```graphql filename="Custom Graph Rule" mutation { createGraph( request: { metadataUri: "lens://4f91c…" rules: { required: [ { unknownRule: { address: "0x1234…" params: [ { raw: { key: "0x4f…" # 32-byte key (e.g., keccak(name)) data: "0x00" # ABI-encoded value } } ] } } ] } } ) { ... on CreateGraphResponse { hash } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` Coming soon ### Update Graph Rules To update Graph's rules configuration, follow these steps. You MUST be authenticated as [Builder](../authentication), [Account Manager](../authentication), or [Account Owner](../authentication) and be either the owner or an admin of the Graph you intend to configure. #### Identify Current Rules First, inspect the `graph.rules` field to know the current rules configuration. ```ts filename="GraphRules" type GraphRules = { required: GraphRule; anyOf: GraphRule; }; ``` ```ts filename="GraphRule" type GraphRule = { id: RuleId; type: GraphRuleType; address: EvmAddress; executesOn: FeedRuleExecuteOn[]; config: AnyKeyValue[]; }; ``` ```ts filename="AnyKeyValue" type AnyKeyValue = | IntKeyValue | IntNullableKeyValue | AddressKeyValue | StringKeyValue | BooleanKeyValue | RawKeyValue | BigDecimalKeyValue | DictionaryKeyValue | ArrayKeyValue; ``` ```ts filename="ArrayKeyValue" type ArrayKeyValue = { __typename: "ArrayKeyValue"; key: string; array: | IntKeyValue | IntNullableKeyValue | AddressKeyValue | StringKeyValue | BooleanKeyValue | RawKeyValue | BigDecimalKeyValue | DictionaryKeyValue; }; ``` ```ts filename="DictionaryKeyValue" type DictionaryKeyValue = { __typename: "DictionaryKeyValue"; key: string; dictionary: | IntKeyValue | IntNullableKeyValue | AddressKeyValue | StringKeyValue | BooleanKeyValue | RawKeyValue | BigDecimalKeyValue; }; ``` ```ts filename="Others" type IntKeyValue = { __typename: "IntKeyValue"; key: string; int: number; }; type IntNullableKeyValue = { __typename: "IntNullableKeyValue"; key: string; optionalInt: number | null; }; type AddressKeyValue = { __typename: "AddressKeyValue"; key: string; address: EvmAddress; }; type StringKeyValue = { __typename: "StringKeyValue"; key: string; string: string; }; type BooleanKeyValue = { __typename: "BooleanKeyValue"; key: string; boolean: boolean; }; type RawKeyValue = { __typename: "RawKeyValue"; key: string; data: BlockchainData; }; type BigDecimalKeyValue = { __typename: "BigDecimalKeyValue"; key: string; bigDecimal: BigDecimal; }; ``` ```graphql filename="GraphRules" type GraphRules { required: [GraphRule!]! anyOf: [GraphRule!]! } ``` ```graphql filename="GraphRule" type GraphRule { id: RuleId! type: GraphRuleType! address: EvmAddress! executesOn: [FeedRuleExecuteOn!]! config: [AnyKeyValue!]! } ``` ```graphql filename="AnyKeyValue" fragment AnyKeyValue on AnyKeyValue { ... on IntKeyValue { ...IntKeyValue } ... on IntNullableKeyValue { ...IntNullableKeyValue } ... on AddressKeyValue { ...AddressKeyValue } ... on StringKeyValue { ...StringKeyValue } ... on BooleanKeyValue { ...BooleanKeyValue } ... on RawKeyValue { ...RawKeyValue } ... on BigDecimalKeyValue { ...BigDecimalKeyValue } ... on DictionaryKeyValue { ...DictionaryKeyValue } ... on ArrayKeyValue { ...ArrayKeyValue } } ``` ```graphql filename="ArrayKeyValue" fragment ArrayKeyValue on ArrayKeyValue { __typename key array { ... on IntKeyValue { ...IntKeyValue } ... on IntNullableKeyValue { ...IntNullableKeyValue } ... on AddressKeyValue { ...AddressKeyValue } ... on StringKeyValue { ...StringKeyValue } ... on BooleanKeyValue { ...BooleanKeyValue } ... on RawKeyValue { ...RawKeyValue } ... on BigDecimalKeyValue { ...BigDecimalKeyValue } ... on DictionaryKeyValue { ...DictionaryKeyValue } } } ``` ```graphql filename="DictionaryKeyValue" fragment DictionaryKeyValue on DictionaryKeyValue { __typename key dictionary { ... on IntKeyValue { ...IntKeyValue } ... on IntNullableKeyValue { ...IntNullableKeyValue } ... on AddressKeyValue { ...AddressKeyValue } ... on StringKeyValue { ...StringKeyValue } ... on BooleanKeyValue { ...BooleanKeyValue } ... on RawKeyValue { ...RawKeyValue } ... on BigDecimalKeyValue { ...BigDecimalKeyValue } } } ``` ```graphql filename="Others" fragment IntKeyValue on IntKeyValue { __typename key int } fragment IntNullableKeyValue on IntNullableKeyValue { __typename key optionalInt } fragment AddressKeyValue on AddressKeyValue { __typename key address } fragment StringKeyValue on StringKeyValue { __typename key string } fragment BooleanKeyValue on BooleanKeyValue { __typename key boolean } fragment RawKeyValue on RawKeyValue { __typename key data } fragment BigDecimalKeyValue on BigDecimalKeyValue { __typename key bigDecimal } ``` The configuration for the built-in rules with one or more parameters is as follows. | Key | Type | Description | | --------------- | ------------ | --------------------------------------- | | `assetContract` | `EvmAddress` | Address of the ERC-20 token contract. | | `assetName` | `String` | Name of the ERC-20 token. | | `assetSymbol` | `String` | Symbol of the ERC-20 token. | | `amount` | `BigDecimal` | Payment required to follow the account. | | Key | Type | Description | | --------------- | ------------ | -------------------------------------------------------- | | `assetContract` | `EvmAddress` | Address of the token contract. | | `assetName` | `String` | Name of the token. | | `assetSymbol` | `String` | Symbol of the token. | | `amount` | `BigDecimal` | Minimum number of tokens required to follow the account. | Keep note of the Rule IDs you might want to remove. #### Update the Rules Configuration Next, update the rules configuration of the Graph as follows. Use the `updateGraphRules` action to update the rules configuration of a given graph. ```ts filename="Add Rules" import { bigDecimal, evmAddress, TokenStandard } from "@lens-protocol/client"; import { updateGraphRules } from "@lens-protocol/client/actions"; const result = await updateGraphRules(sessionClient, { graph: graph.address, toAdd: { required: [ { tokenGatedRule: { token: { standard: TokenStandard.Erc20, currency: evmAddress("0x5678…"), value: bigDecimal("1.5"), // Token value in its main unit }, }, }, ], }, }); ``` ```ts filename="Remove Rules" import { updateGraphRules } from "@lens-protocol/client/actions"; const result = await updateGraphRules(sessionClient, { graph: graph.address, toRemove: [graph.rules.required[0].id], }); ``` Use the `updateGraphRules` mutation to update the rules configuration of a given graph. ```graphql filename="Add Rules" mutation { updateGraphRules( request: { graph: "0x1234…" toAdd: { required: [ { tokenGatedRule: { token: { standard: ERC20, currency: "0x5678…", value: "1.5" } } } ] } } ) { ... on UpdateGraphRulesResponse { ...UpdateGraphRulesResponse } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ```graphql filename="Remove Rules" mutation { updateGraphRules(request: { graph: "0x1234…", toRemove: ["ej6g…"] }) { ... on UpdateGraphRulesResponse { ...UpdateGraphRulesResponse } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` #### Handle Result Then, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await updateGraphRules(sessionClient, { graph: evmAddress("0x1234…"), // … }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await updateGraphRules(sessionClient, { graph: evmAddress("0x1234…"), // … }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Then, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. --- ## Building a Graph Rule Let's illustrate the process with an example. We will build a custom Graph Rule that will require accounts to wait a certain amount of time after Following someone in order to be allowed to Follow again. To build a custom Graph Rule, you must implement the following `IGraphRule` interface: ```solidity interface IGraphRule { function configure(bytes32 configSalt, KeyValue[] calldata ruleParams) external; function processFollow( bytes32 configSalt, address originalMsgSender, address followerAccount, address accountToFollow, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external; function processUnfollow( bytes32 configSalt, address originalMsgSender, address followerAccount, address accountToUnfollow, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external; function processFollowRuleChanges( bytes32 configSalt, address account, RuleChange[] calldata ruleChanges, KeyValue[] calldata ruleParams ) external; } ``` Each function of this interface must assume to be invoked by the Graph contract. In other words, assume the `msg.sender` will be the Graph contract. A Lens dependency package with all relevant interfaces will be available soon. ### Implement the Configure Function First, implement the `configure` function. This function has the purpose of initializing any required state for the rule to work properly. It receives two parameters, a 32-byte configuration salt (`configSalt`), and an array of custom parameters as key-value pairs (`ruleParams`). The `configSalt` is there to allow the same rule contract to be used many times, with different configurations, for the same Graph. So, for a given Graph Rule implementation, the pair (Graph Address, Configuration Salt) should identify a rule configuration. For example, let's think about the `GroupGatedGraphRule`, we could want to achieve the restriction "To follow on this Graph, you must be a member of Group A or Group B". However, the `GroupGatedGraphRule` only allows to configure a single Group as a gate. So, instead of writing a whole new contract that receives two groups instead of one, we would just configure the `GroupGatedGraphRule` rule twice in the same Graph, once for Group A with some configuration salt, and once for Group B with another configuration salt. The `configure` function can be called multiple times by the same Graph passing the same configuration salt in order to update that rule configuration (i.e. reconfigure it). The `ruleParams` is an array of key-value pairs that can be used to pass any custom configuration parameters to the rule. Each key is `bytes32`, we put the hash of the parameter name there, and each value is `bytes`, we set the ABI-encoded parameter value there. Given that `ruleParams` is an array, this allows the rule to define which parameters are optional and which are required, acting accordingly when any of them are not present. In our example, we need to decode an integer parameter, which will represent the amount of time in seconds required to elapse until allowing an account to Follow again. Let's define a storage mapping to store this configuration: ```solidity contract FollowCooldownGraphRule is IGraphRule { mapping(address graph => mapping(bytes32 configSalt => uint256 cooldown)) internal _followCooldown; } ``` The configuration is stored in the mapping using the Graph contract address and the configuration salt as keys. With this setup, the same rule can be used by different Graphs, as well as be used by the same Graph many times. Now let's code the `configure` function itself, decoding the integer parameter and storing it in the mapping: ```solidity contract FollowCooldownGraphRule is IGraphRule { mapping(address graph => mapping(bytes32 configSalt => uint256 cooldown)) internal _followCooldown; function configure(bytes32 configSalt, KeyValue[] calldata ruleParams) external override { uint256 cooldown = 1 days; for (uint256 i = 0; i < ruleParams.length; i++) { if (ruleParams[i].key == keccak256("lens.param.followCooldown")) { cooldown = abi.decode(ruleParams[i].value, (uint256)); break; } } _followCooldown[msg.sender][configSalt] = cooldown; } } ``` As you can see, we treated the cooldown parameter as optional, setting a default cooldown time value of 1 day in case the parameter was not found in the `ruleParams`. However, if we want the parameter to be required, we could have initialized it with a value of 0 and then reverted if it is still set to 0 after the search for-loop. ### Implement the Process Follow function Next, implement the `processFollow` function. This function is invoked by the Graph contract every time a Follow is executed, so then our custom logic can be applied to shape under which conditions this operation can succeed. The function receives the configuration salt (`configSalt`), so we know which configuration to use, the address that initiated the follow transaction in the Graph (`originalMsgSender`), the account acting as the follower (`followerAccount`), the account being followed (`accountToFollow`), an array of key-value pairs with the custom parameters passed to the Graph (`primitiveParams`), and an array of key-value pairs in case the rule requires additional parameters to work (`ruleParams`). The function must revert in case of not meeting the requirements imposed by the rule. We need to introduce another mapping, which will store the timestamp of the last Follow performed by a given Follower account in a given Graph: ```solidity contract FollowCooldownGraphRule is IGraphRule { mapping(address graph => mapping(bytes32 configSalt => uint256 cooldown)) internal _followCooldown; mapping(address graph => mapping(bytes32 configSalt => (mapping(address account => uint256 timestamp)))) internal _lastFollowTimestamp; } ``` Now we can get the time elapsed since the last Follow executed by the Follower account in this Graph: ```solidity contract FollowCooldownGraphRule is IGraphRule { mapping(address graph => mapping(bytes32 configSalt => uint256 cooldown)) internal _followCooldown; mapping(address graph => mapping(bytes32 configSalt => (mapping(address account => uint256 timestamp)))) internal _lastFollowTimestamp; // . . . function processFollow( bytes32 configSalt, address originalMsgSender, address followerAccount, address accountToFollow, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external override { // We get the time elapsed since the last Follow for the given configuration uint256 timeElapsedSinceLastFollow = block.timestamp - _lastFollowTimestamp[msg.sender][configSalt][followerAccount]; } } ``` Next we require the elapsed time to be greater than the configured Follow cooldown period: ```solidity contract FollowCooldownGraphRule is IGraphRule { mapping(address graph => mapping(bytes32 configSalt => uint256 cooldown)) internal _followCooldown; mapping(address graph => mapping(bytes32 configSalt => (mapping(address account => uint256 timestamp)))) internal _lastFollowTimestamp; // . . . function processFollow( bytes32 configSalt, address originalMsgSender, address followerAccount, address accountToFollow, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external override { uint256 timeElapsedSinceLastFollow = block.timestamp - _lastFollowTimestamp[msg.sender][configSalt][followerAccount]; // We require the elapsed time to be greater than the configured follow cooldown period require(timeElapsedSinceLastFollow > _followCooldown[msg.sender][configSalt]); } } ``` And to finish the implementation of this function, we update the last Follow timestamp for this Follower account: ```solidity contract FollowCooldownGraphRule is IGraphRule { mapping(address graph => mapping(bytes32 configSalt => uint256 cooldown)) internal _followCooldown; mapping(address graph => mapping(bytes32 configSalt => (mapping(address account => uint256 timestamp)))) internal _lastFollowTimestamp; // . . . function processFollow( bytes32 configSalt, address originalMsgSender, address followerAccount, address accountToFollow, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external override { uint256 timeElapsedSinceLastFollow = block.timestamp - _lastFollowTimestamp[msg.sender][configSalt][followerAccount]; require(timeElapsedSinceLastFollow > _followCooldown[msg.sender][configSalt]); // We update the last Follow timestamp for this Follower account _lastFollowTimestamp[followerAccount][configSalt][accountToFollow] = block.timestamp; } } ``` ### Implement the Process Unfollow function Next, implement the `processUnfollow` function. This function is invoked by the Graph contract every time an unfollow is executed, so that our custom logic can be applied to determine under which conditions this operation can succeed. The function receives the configuration salt (`configSalt`), so we know which configuration to use, the address that initiated the unfollow transaction in the Graph (`originalMsgSender`), the account unfollowing (`followerAccount`), the account about to be unfollowed (`accountToUnfollow`), an array of key-value pairs with the custom parameters passed to the Graph (`primitiveParams`), and an array of key-value pairs in case the rule requires additional parameters to work (`ruleParams`). The function must revert in case of not meeting the requirements imposed by the rule. In this case, we do not want to impose any restriction on the unfollow operation. As a good practice, we revert for unimplemented rule functions, as it is safer in case the rule becomes accidentally applied. ```solidity contract FollowCooldownGraphRule is IGraphRule { // . . . function processUnfollow( bytes32 configSalt, address originalMsgSender, address followerAccount, address accountToUnfollow, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external override { revert Errors.NotImplemented(); } ``` ### Implement the Process Follow Rule Changes function Finally, implement the `processFollowRuleChanges` function. This function is invoked by the Graph contract every time an account makes a change on its [Follow Rules](./follow-rules), so that our rule can define whether this change must be accepted or not. The function receives the configuration salt (`configSalt`), so we know which configuration to use, the `account` changing its [Follow Rules](./follow-rules), the array of rules changes (`ruleChanges`), and an array of key-value pairs in case the rule requires additional parameters to work (`ruleParams`). The function must revert in case of not meeting the requirements imposed by the rule. In this case, the rule is not focused on controlling Follow Rules changes, so we revert as a good practice to avoid unintended effects on if the rule gets applied accidentally. ```solidity contract FollowCooldownGraphRule is IGraphRule { // . . . function processFollowRuleChanges( bytes32 configSalt, address account, RuleChange[] calldata ruleChanges, KeyValue[] calldata ruleParams ) external override { revert Errors.NotImplemented(); } } ``` Now the `FollowCooldownGraphRule` is ready to be applied into any Graph. See the full code below: ```solidity contract FollowCooldownGraphRule is IGraphRule { mapping(address graph => mapping(bytes32 configSalt => uint256 cooldown)) internal _followCooldown; mapping(address graph => mapping(bytes32 configSalt => (mapping(address account => uint256 timestamp)))) internal _lastFollowTimestamp; function configure(bytes32 configSalt, KeyValue[] calldata ruleParams) external override { uint256 cooldown = 1 days; for (uint256 i = 0; i < ruleParams.length; i++) { if (ruleParams[i].key == keccak256("lens.param.followCooldown")) { cooldown = abi.decode(ruleParams[i].value, (uint256)); break; } } _followCooldown[msg.sender][configSalt] = cooldown; } function processFollow( bytes32 configSalt, address originalMsgSender, address followerAccount, address accountToFollow, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external override { // We get the time elapsed since the last Follow for the given configuration uint256 timeElapsedSinceLastFollow = block.timestamp - _lastFollowTimestamp[msg.sender][configSalt][followerAccount]; // We require the elapsed time to be greater than the configured Follow cooldown period require(timeElapsedSinceLastFollow > _followCooldown[msg.sender][configSalt]); // We update the last Follow timestamp for this Follower account _lastFollowTimestamp[followerAccount][configSalt][accountToFollow] = block.timestamp; } function processUnfollow( bytes32 configSalt, address originalMsgSender, address followerAccount, address accountToUnfollow, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external override { revert Errors.NotImplemented(); } function processFollowRuleChanges( bytes32 configSalt, address account, RuleChange[] calldata ruleChanges, KeyValue[] calldata ruleParams ) external override { revert Errors.NotImplemented(); } } ``` Stay tuned for API integration of rules and more guides! ================ File: src/pages/protocol/graphs/relationships.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Follow Relationships This guide explains how to fetch followers and followings on Lens. --- ## List Followers Use the paginated `fetchFollowers` action to list followers of an Account. ```ts filename="Any Graph" import { evmAddress } from "@lens-protocol/client"; import { fetchFollowers } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchFollowers(client, { account: evmAddress("0x1234…"), }); if (result.isErr()) { return console.error(result.error); } // items: Array: [{follower: Account, followedOn: DateTime}, …] const { items, pageInfo } = result.value; ``` ```ts filename="Global Graph" import { evmAddress } from "@lens-protocol/client"; import { fetchFollowers } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchFollowers(client, { account: evmAddress("0x1234…"), filter: { graphs: [{ globalGraph: true }], }, }); if (result.isErr()) { return console.error(result.error); } // items: Array: [{follower: Account, followedOn: DateTime}, …] const { items, pageInfo } = result.value; ``` ```ts filename="App Graph" import { evmAddress } from "@lens-protocol/client"; import { fetchFollowers } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchFollowers(client, { account: evmAddress("0x1234…"), filter: { graphs: [{ app: evmAddress("0x5678…") }], }, }); if (result.isErr()) { return console.error(result.error); } // items: Array: [{follower: Account, followedOn: DateTime}, …] const { items, pageInfo } = result.value; ``` ```ts filename="Custom Graph" import { evmAddress } from "@lens-protocol/client"; import { fetchFollowers } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchFollowers(client, { account: evmAddress("0x1234…"), filter: { graphs: [{ graph: evmAddress("0x5678…") }], }, }); if (result.isErr()) { return console.error(result.error); } // items: Array: [{follower: Account, followedOn: DateTime}, …] const { items, pageInfo } = result.value; ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; export const client = PublicClient.create({ environment: mainnet, }); ``` Use the paginated `followers` query to list followers of an Account. ```graphql filename="Query" query { followers( request: { account: "0x5E647e6197fa5C6aA814E11C3504BE232a3D671a" # optional filter: { # optional, filter by graphs graphs: [ # optional, filter by global graph { globalGraph: true } # and/or, filter by graph address # { # graph: EvmAddress # } # and/or, filter by graph associated w/ an app address # { # app: EvmAddress # } ] } # optional, defaults to ACCOUNT_SCORE # orderBy: FollowersOrderBy } ) { items { ...Account } pageInfo { prev next } } } ``` ```json filename="Response" { "data": { "followers": { "items": [ { "address": "0x1234…", "username": { "value": "lens/bob" }, "metadata": { "name": "Bob", "picture": "https://example.com/bob.jpg" } } ], "pageInfo": { "prev": null, "next": null } } } } ``` Coming soon See the [Pagination](../best-practices/pagination) guide for more information on how to handle paginated results. ## List Following Use the paginated `fetchFollowing` action to list followings of an Account. ```ts filename="Any Graph" import { evmAddress } from "@lens-protocol/client"; import { fetchFollowing } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchFollowing(client, { account: evmAddress("0x1234…"), }); if (result.isErr()) { return console.error(result.error); } // items: Array: [{following: Account, followedOn: DateTime}, …] const { items, pageInfo } = result.value; ``` ```ts filename="Global Graph" import { evmAddress } from "@lens-protocol/client"; import { fetchFollowing } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchFollowing(client, { account: evmAddress("0x1234…"), filter: { graphs: [{ globalGraph: true }], }, }); if (result.isErr()) { return console.error(result.error); } // items: Array: [{following: Account, followedOn: DateTime}, …] const { items, pageInfo } = result.value; ``` ```ts filename="App Graph" import { evmAddress } from "@lens-protocol/client"; import { fetchFollowing } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchFollowing(client, { account: evmAddress("0x1234…"), filter: { graphs: [{ app: evmAddress("0x5678…") }], }, }); if (result.isErr()) { return console.error(result.error); } // items: Array: [{following: Account, followedOn: DateTime}, …] const { items, pageInfo } = result.value; ``` ```ts filename="Custom Graph" import { evmAddress } from "@lens-protocol/client"; import { fetchFollowing } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchFollowing(client, { account: evmAddress("0x1234…"), filter: { graphs: [{ graph: evmAddress("0x5678…") }], }, }); if (result.isErr()) { return console.error(result.error); } // items: Array: [{following: Account, followedOn: DateTime}, …] const { items, pageInfo } = result.value; ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; export const client = PublicClient.create({ environment: mainnet, }); ``` Use the paginated `following` query to list followings of an Account. ```graphql filename="Query" query { following( request: { account: "0x5E647e6197fa5C6aA814E11C3504BE232a3D671a" # optional filter: { # optional, filter by graphs graphs: [ # optional, filter by global graph { globalGraph: true } # and/or, filter by graph address # { # graph: EvmAddress # } # and/or, filter by graph associated w/ an app address # { # app: EvmAddress # } ] } # optional, defaults to ACCOUNT_SCORE # orderBy: FollowersOrderBy } ) { items { ...Account } pageInfo { prev next } } } ``` ```json filename="Response" { "data": { "following": { "items": [ { "address": "0x1234…", "username": { "value": "lens/bob" }, "metadata": { "name": "Bob", "picture": "https://example.com/bob.jpg" } } ], "pageInfo": { "prev": null, "next": null } } } } ``` Coming soon See the [Pagination](../best-practices/pagination) guide for more information on how to handle paginated results. ## List Followers You Know You can also cross-reference the followers of an Account that are also the followings of another Account. This could be a powerful hint to suggest potential connections between two Accounts. Use the paginated `fetchFollowersYouKnow` action to list the followings of a _target_ Account that are followed by an _observer_ Account. ```ts filename="Any Graph" import { evmAddress } from "@lens-protocol/client"; import { fetchFollowersYouKnow } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchFollowersYouKnow(client, { observer: evmAddress("0x1234…"), target: evmAddress("0x5678…"), }); if (result.isErr()) { return console.error(result.error); } // items: Array: [{follower: Account, followedOn: DateTime}, …] const { items, pageInfo } = result.value; ``` ```ts filename="Global Graph" import { evmAddress } from "@lens-protocol/client"; import { fetchFollowersYouKnow } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchFollowersYouKnow(client, { observer: evmAddress("0x1234…"), target: evmAddress("0x5678…"), filter: { graphs: [{ globalGraph: true }], }, }); if (result.isErr()) { return console.error(result.error); } // items: Array: [{follower: Account, followedOn: DateTime}, …] const { items, pageInfo } = result.value; ``` ```ts filename="Custom Graph" import { evmAddress } from "@lens-protocol/client"; import { fetchFollowersYouKnow } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchFollowersYouKnow(client, { observer: evmAddress("0x1234…"), target: evmAddress("0x5678…"), filter: { graphs: [{ graph: evmAddress("0x9012…") }], }, }); if (result.isErr()) { return console.error(result.error); } // items: Array: [{follower: Account, followedOn: DateTime}, …] const { items, pageInfo } = result.value; ``` ```ts filename="App Graph" import { evmAddress } from "@lens-protocol/client"; import { fetchFollowersYouKnow } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchFollowersYouKnow(client, { observer: evmAddress("0x1234…"), target: evmAddress("0x5678…"), filter: { graphs: [{ app: evmAddress("0x9012…") }], }, }); if (result.isErr()) { return console.error(result.error); } // items: Array: [{follower: Account, followedOn: DateTime}, …] const { items, pageInfo } = result.value; ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; export const client = PublicClient.create({ environment: mainnet, }); ``` Use the paginated `followersYouKnow` query to list the followings of a _target_ Account that are followed by an _observer_ Account. ```graphql filename="Query" query { followersYouKnow( request: { target: "0x5E647e6197fa5C6aA814E11C3504BE232a3D671a" observer: "0x1234567890abcdef1234567890abcdef12345678" # optional filter: { # optional, filter by graphs graphs: [ # optional, filter by global graph { globalGraph: true } # and/or, filter by graph address # { # graph: EvmAddress # } # and/or, filter by graph associated w/ an app address # { # app: EvmAddress # } ] } # optional, defaults to ACCOUNT_SCORE # orderBy: FollowersYouKnowOrderBy } ) { items { ...Account } pageInfo { prev next } } } ``` ```json filename="Response" { "data": { "followersYouKnow": { "items": [ { "address": "0x1234…", "username": { "value": "lens/bob" }, "metadata": { "name": "Bob", "picture": "https://example.com/bob.jpg" } } ], "pageInfo": { "prev": null, "next": null } } } } ``` Coming soon See the [Pagination](../best-practices/pagination) guide for more information on how to handle paginated results. ## Inspect Follow Status Given 2 or more Accounts, you might need to know what is their relative following relationship. Use the `fetchFollowStatus` action to verify the follow status of a list of target-observer pairs. ```ts filename="Default Graph" import { evmAddress } from "@lens-protocol/client"; import { fetchFollowStatus } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchFollowStatus(client, { pairs: [ { account: evmAddress("0x1234…"), follower: evmAddress("0x5678…"), }, ], }); if (result.isErr()) { return console.error(result.error); } // status: Array: [{graph: "0x1234", follower: "0x1234", account: "0x1234", isFollowing: {...}}, …] const status = result.value; ``` ```ts filename="Custom Graph" import { evmAddress } from "@lens-protocol/client"; import { fetchFollowStatus } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchFollowStatus(client, { pairs: [ { graph: evmAddress("0x1234…"), account: evmAddress("0x0912…"), follower: evmAddress("0x5678…"), }, ], }); if (result.isErr()) { return console.error(result.error); } // status: Array: [{graph: "0x1234", follower: "0x1234", account: "0x1234", isFollowing: {...}}, …] const status = result.value; ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; export const client = PublicClient.create({ environment: mainnet, }); ``` Use the `followStatus` query to verify the follow status of a list of target-observer pairs. **Pro-tip**: Include the same pair twice, reversing the target and observer, to determine if they follow each other in a single request. ```graphql filename="Query" query { followStatus( request: [ { follower: "0xb0b…" account: "0xa1ece…" # optional, defaults to Global Graph # graph: EvmAddress } { follower: "0xa1ece…" account: "0xb0b…" # optional, defaults to Global Graph # graph: EvmAddress } ] ) { graph follower account isFollowing { ...BooleanValue } } } ``` ```graphql filename="BooleanValue" fragment BooleanValue on BooleanValue { optimistic # true if the operation is optimistically assumed to be done onChain # true if the operation is settled on-chain } ``` ```json filename="Response" { "data": { "followStatus": [ { "graph": "0x1234…", "follower": "0xb0b…", "account": "0xa1ece…", "isFollowing": { "optimistic": false, "onChain": true } }, { "graph": "0x1234…", "follower": "0xa1ece…", "account": "0xb0b…", "isFollowing": { "optimistic": false, "onChain": true } } ] } } ``` Coming soon ================ File: src/pages/protocol/groups/banned-accounts.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Banned Accounts This guide explain how to ban and unban accounts from a group. --- Group owner or admins can ban accounts from joining the Group. Banning an account after it has already joined the Group does not remove it. See the [Remove Group Members](./manage#remove-group-members) guide to remove and ban a member. ## Ban Accounts To ban one or mores accounts from joining a Group, follow these steps. You MUST be authenticated as [Builder](../authentication), [Account Manager](../authentication), or [Account Owner](../authentication) and be the Group owner or an admin to perform this action. ### Ban the Accounts Use the `banGroupAccounts` action to ban one or more accounts from joining a given Group. ```ts filename="Ban Accounts" import { evmAddress } from "@lens-protocol/client"; import { banGroupAccounts } from "@lens-protocol/client/actions"; const result = await banGroupAccounts(sessionClient, { group: evmAddress("0xe2f…"), accounts: [evmAddress("0x4f91…"), evmAddress("0x4f92…")], }); if (result.isErr()) { return console.error(result.error); } ``` Use the `banGroupAccounts` mutation to ban one or more accounts from joining a given Group. ```graphql filename="Mutation" mutation { banGroupAccounts( request: { group: "0xe2f…", accounts: ["0x4f91…", "0x4f92…"] } ) { ... on BanGroupAccountsResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` Coming soon ### Handle Result Then, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await banGroupAccounts(sessionClient, { group: evmAddress("0xe2f…"), accounts: [evmAddress("0x4f91…"), evmAddress("0x4f92…")], }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await banGroupAccounts(sessionClient, { group: evmAddress("0xe2f…"), accounts: [evmAddress("0x4f91…"), evmAddress("0x4f92…")], }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Then, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Coming soon That's it—these accounts are now banned from joining the Group. ## List Banned Accounts Use the paginated `fetchGroupBannedAccounts` action to list all banned accounts for a given Group. ```ts filename="All members of a group" import { evmAddress } from "@lens-protocol/client"; import { fetchGroupBannedAccounts } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchGroupBannedAccounts(client, { group: evmAddress("0x1234…"), }); if (result.isErr()) { return console.error(result.error); } // items: Array: [{ account: Account, bannedAt: DateTime, bannedBy: Account, …}, …] const { items, pageInfo } = result.value; ``` Use the paginated `groupBannedAccounts` query to list all banned accounts for a given Group. ```graphql filename="Query" query { groupBannedAccounts(request: { group: "0x1234…" }) { items { ruleId lastActiveAt bannedAt bannedBy { ...Account } account { ...Account } } pageInfo { prev next } } } ``` ```graphql filename="Account" fragment Account on Account { address username { value } metadata { name picture } } ``` ```json filename="Response" { "data": { "groupBannedAccounts": [ { "ruleId": "0x1234…", "lastActiveAt": "2021-09-01T00:00:00Z", "bannedAt": "2021-09-01T00:00:00Z", "bannedBy": { "address": "0x1234…", "username": { "value": "Alice" }, "metadata": { "name": "Alice", "picture": "https://example.com/alice.jpg" } }, "account": { "address": "0x5678…", "username": { "value": "Bob" }, "metadata": { "name": "Bob", "picture": "https://example.com/bob.jpg" } } } ] } } ``` Coming soon See the [Pagination](../best-practices/pagination) guide for more information on how to handle paginated results. ## Unban Accounts To revert bans and allow accounts to join a Group again, follow these steps. You MUST be authenticated as [Builder](../authentication), [Account Manager](../authentication), or [Account Owner](../authentication) and be either the Group owner or an admin to perform this action. ### Unban the Accounts Use the `unbanGroupAccounts` action to unban one or more accounts for a given Group. ```ts filename="Ban Accounts" import { evmAddress } from "@lens-protocol/client"; import { unbanGroupAccounts } from "@lens-protocol/client/actions"; const result = await unbanGroupAccounts(sessionClient, { group: evmAddress("0xe2f…"), accounts: [evmAddress("0x4f91…"), evmAddress("0x4f92…")], }); if (result.isErr()) { return console.error(result.error); } ``` Use the `unbanGroupAccounts` mutation to unban one or more accounts for a given Group. ```graphql filename="Mutation" mutation { unbanGroupAccounts( request: { group: "0xe2f…", accounts: ["0x4f91…", "0x4f92…"] } ) { ... on UnbanGroupAccountsResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` Coming soon ### Handle Result Then, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await unbanGroupAccounts(sessionClient, { group: evmAddress("0xe2f…"), accounts: [evmAddress("0x4f91…"), evmAddress("0x4f92…")], }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await unbanGroupAccounts(sessionClient, { group: evmAddress("0xe2f…"), accounts: [evmAddress("0x4f91…"), evmAddress("0x4f92…")], }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Then, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Coming soon That's it—these accounts are now banned from joining the Group. ================ File: src/pages/protocol/groups/create.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Create a Group This guide will help you create a Group on Lens. --- To create an Group, follow these steps. You MUST be authenticated as [Builder](../authentication), [Account Manager](../authentication), or [Account Owner](../authentication) to create a Group. ## Create Group Metadata First, construct a Group Metadata object. Use the `@lens-protocol/metadata` package to construct a valid `GroupMetadata` object: ```ts filename="Example" import { group } from "@lens-protocol/metadata"; const metadata = group({ name: "XYZ", description: "My group description", icon: "lens://BsdfA…", }); ``` If you opted for manually create Metadata objects, make sure it conform to the [Group Metadata JSON Schema](https://json-schemas.lens.dev/group/1.0.0.json). ```json filename="Example" { "$schema": "https://json-schemas.lens.dev/group/1.0.0.json", "lens": { "id": "6418053f-89ae-4f9e-8362-8d1b6c0cdafb", "name": "XYZ", "description": "My group description", "icon": "lens://BsdfA…" } } ``` ## Upload Group Metadata Next, upload the Group Metadata object to a public URI. ```ts import { storageClient } from "./storage-client"; const { uri } = await storageClient.uploadAsJson(metadata); console.log(uri); // e.g., lens://4f91ca… ``` This example uses [Grove storage](../../storage) to host the Metadata object. See the [Lens Metadata Standards](../best-practices/metadata-standards#host-metadata-objects) guide for more information on hosting Metadata objects. ## Deploy Group Contract Next, deploy the Group smart contract. Use the `createGroup` action to deploy the Lens Group smart contract. ```ts filename="Simple Group" import { uri } from "@lens-protocol/client"; import { createGroup } from "@lens-protocol/client/actions"; const result = await createGroup(sessionClient, { metadataUri: uri("lens://4f91c…"), }); ``` ```ts filename="Group with Owner" import { uri } from "@lens-protocol/client"; import { createGroup } from "@lens-protocol/client/actions"; const result = await createGroup(sessionClient, { metadataUri: uri("lens://4f91c…"), owner: evmAddress("0x1234…"), }); ``` ```ts filename="Group with Admins" import { evmAddress, uri } from "@lens-protocol/client"; import { createGroup } from "@lens-protocol/client/actions"; const result = await createGroup(sessionClient, { admins: [evmAddress("0x1234…")], metadataUri: uri("lens://4f91c…"), }); ``` ```ts filename="Group with Custom Feed" import { evmAddress, uri } from "@lens-protocol/client"; import { createGroup } from "@lens-protocol/client/actions"; const result = await createGroup(sessionClient, { feed: { metadataUri: uri("lens://fer64…"), }, metadataUri: uri("lens://4f91c…"), }); ``` ```ts filename="Group with Rules" import { bigDecimal, evmAddress, uri, TokenStandard, } from "@lens-protocol/client"; import { createGroup } from "@lens-protocol/client/actions"; const result = await createGroup(sessionClient, { metadataUri: uri("lens://4f91c…"), rules: { required: [ { tokenGatedRule: { token: { currency: evmAddress("0x1234…"), standard: TokenStandard.Erc721, value: bigDecimal("1"), }, }, }, ], }, }); ``` Use the `createGroup` mutation to deploy the Lens Feed smart contract. ```graphql filename="Mutation" mutation { createGroup( request: { metadataUri: "lens://4f91cab87ab5e4f5066f878b72…" # optional owner # owner: EvmAddress! # optional list of admins # admins: [EvmAddress!] # optional rules for the group (see Group Rules guide) # rules: { # required: [GroupRule!], # anyOf: [GroupRule!], # } # optional feed parameters # feed: { # metadataUri: "lens://87ab5e4f5066f878b72…" # } } ) { ... on CreateGroupResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ```json filename="CreateGroupResponse" { "data": { "createGroup": { "hash": "0x…" } } } ``` Coming soon To learn more about how to use Group Rules, see the [Group Rules](./rules) guide. ## Handle Result Next, handle the result using the adapter for the library of your choice and wait for it to be indexed. ```ts filename="viem" highlight="1,8,9" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await createGroup(sessionClient, { metadataUri: uri("lens://4f91ca…"), }) .andThen(handleOperationWith(walletClient)) .andThen(sessionClient.waitForTransaction); ``` ```ts filename="ethers" highlight="1,8,9" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await createGroup(sessionClient, { metadataUri: uri("lens://4f91ca…"), }) .andThen(handleOperationWith(signer)) .andThen(sessionClient.waitForTransaction); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Next, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Coming soon ### Fetch New Group Finally, fetch the newly created Group using the `fetchGroup` action. ```ts filename="viem" highlight="1,10" import { fetchGroup } from "@lens-protocol/client/actions"; // … const result = await createGroup(sessionClient, { metadataUri: uri("lens://4f91…"), // the URI from the previous step }) .andThen(handleOperationWith(walletClientOrSigner)) .andThen(sessionClient.waitForTransaction) .andThen((txHash) => fetchGroup(sessionClient, { txHash })); if (result.isErr()) { return console.error(result.error); } // group: Group | null const group = result.value; ``` Finally, fetch the newly created Group using the `group` query. ```graphql filename="Query" query { group(request: { txHash: "0x1234…" }) { address timestamp owner metadata { description name } } } ``` ```json filename="Response" { "data": { "group": { "address": "0x1234…", "timestamp": "2021-09-01T00:00:00Z", "owner": "0x9012…", "metadata": { "description": "A group for builders", "name": "Builders" } } } } ``` That's it—you have successfully created a Group on Lens! ================ File: src/pages/protocol/groups/fetch-members.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Fetch Members This guide will help you with fetching Group members from Lens API. --- ## Fetch Groups Members Use the paginated `fetchGroupMembers` action to fetch a list of group members based on the provided group address. ```ts filename="All members of a group" import { evmAddress } from "@lens-protocol/client"; import { fetchGroupMembers } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchGroupMembers(client, { group: evmAddress("0x1234…"), }); if (result.isErr()) { return console.error(result.error); } // items: Array: [{account: Account, joinedAt: DateTime, lastActiveAt: DateTime}, …] const { items, pageInfo } = result.value; ``` ```ts filename="Filter Members By LocalName" import { evmAddress } from "@lens-protocol/client"; import { fetchGroupMembers } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchGroupMembers(client, { group: evmAddress("0x1234…"), filter: { searchBy: { localNameQuery: "user", }, }, }); if (result.isErr()) { return console.error(result.error); } // items: Array: [{account: Account, joinedAt: DateTime, lastActiveAt: DateTime}, …] const { items, pageInfo } = result.value; ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; export const client = PublicClient.create({ environment: mainnet, }); ``` Use the paginated `groupMembers` query to fetch a list of group members based on the provided group address. The [Fetch Account](../accounts/fetch) guide provides a more detailed explanation of how to fetch an account. ```graphql filename="Query" query { groupMembers( request: { # the group to fetch members for group: "0x1234…" } ) { items { address username { value } metadata { name picture } } pageInfo { prev next } } } ``` ```json filename="Response" { "data": { "accounts": [ { "address": "0x1234…", "username": { "value": "lens/wagmi" }, "metadata": { "name": "WAGMI", "picture": "https://example.com/wagmi.jpg" } }, { "address": "0x5678…", "username": { "value": "lens/ape" }, "metadata": { "name": "APE", "picture": "https://example.com/bob.jpg" } } ] } } ``` Coming soon See the [Pagination](../best-practices/pagination) guide for more information on how to handle paginated results. ================ File: src/pages/protocol/groups/fetch.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Fetch Groups This guide will help you with fetching Groups from Lens API. --- ## Get a Group Use the `useGroup` hook to fetch a single Group. Returns `null` if no group is found. ```tsx filename="With Loading" const { data, loading, error } = useGroup(request); ``` ```tsx filename="With Suspense" const { data, error } = useGroup({ suspense: true, ...request }); ``` A Group can be fetched by its address or by transaction hash. ```ts filename="By Group Address" import { evmAddress, useGroup } from "@lens-protocol/react"; // … const { data, loading, error } = useGroup({ group: evmAddress("0x1234…"), }); if (loading) { return

Loading…

; } if (error) { return

{error.message}

; } // data: Group | null ``` ```ts filename="By Tx Hash" import { txHash, useGroup } from "@lens-protocol/react"; // … const { data, loading, error } = useGroup({ txHash: txHash("0x1234…"), }); if (loading) { return

Loading…

; } if (error) { return

{error.message}

; } // data: Group | null ```
Use the `fetchGroup` function to fetch a single Group by address or by transaction hash. Fetching a Group by transaction hash is extremely useful when building a user experience where a user creates a Group and needs it presented back to them. ```ts filename="By Group Address" import { evmAddress } from "@lens-protocol/client"; import { fetchGroup } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchGroup(client, { group: evmAddress("0x1234…"), }); if (result.isErr()) { return console.error(result.error); } const group = result.value; ``` ```ts filename="By Tx Hash" import { txHash } from "@lens-protocol/client"; import { fetchGroup } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchGroup(client, { txHash: txHash("0x1234…"), }); if (result.isErr()) { return console.error(result.error); } const group = result.value; ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; export const client = PublicClient.create({ environment: mainnet, }); ``` Use the `group` query to fetch a single Group by address or by transaction hash. Fetching a Group by transaction hash is extremely useful when building a user experience where a user creates a Group and needs it presented back to them. ```graphql filename="Query" query { group( request: { group: "0x1234…" # OR # txHash: TxHash! } ) { address timestamp feed owner banningEnabled membershipApprovalEnabled metadata { description id icon name coverPicture } } } ``` ```json filename="Response" { "data": { "group": { "address": "0x1234…", "timestamp": "2021-09-01T00:00:00Z", "feed": "0x5678…", "owner": "0x9012…", "banningEnabled": true, "membershipApprovalEnabled": true, "metadata": { "id": "1234…", "description": "A group for builders", "icon": "https://example.com/icon.png", "name": "Builders", "coverPicture": "https://example.com/cover.png" } } } } ```
## List Groups Use the `useGroups` hook to fetch a list of Groups. ```tsx filename="With Loading" const { data, loading, error } = useGroups(request); ``` ```tsx filename="With Suspense" const { data, error } = useGroups({ suspense: true, ...request }); ``` Groups can be fetched by search query, app address, managedBy or member. ```ts filename="Search by Group Name" import { useGroups } from "@lens-protocol/react"; // … const { data, loading, error } = useGroups({ filter: { searchQuery: "group", }, }); if (loading) { return

Loading…

; } if (error) { return

{error.message}

; } // data: Array ``` ```ts filename="By App" import { evmAddress, useGroups } from "@lens-protocol/react"; // … const { data, loading, error } = useGroups({ filter: { app: evmAddress("0x1234…"), }, }); if (loading) { return

Loading…

; } if (error) { return

{error.message}

; } // data: Array ``` ```ts filename="Managed by an Address" import { evmAddress, useGroups } from "@lens-protocol/react"; // … const { data, loading, error } = useGroups({ filter: { managedBy: { address: evmAddress("0x1234…"), }, }, }); if (loading) { return

Loading…

; } if (error) { return

{error.message}

; } // data: Array ``` ```ts filename="By Member Address" import { evmAddress, useGroups } from "@lens-protocol/react"; // … const { data, loading, error } = useGroups({ filter: { member: evmAddress("0x1234…"), }, }); if (loading) { return

Loading…

; } if (error) { return

{error.message}

; } // data: Array ```
Use the paginated `fetchGroups` function to fetch a list of Groups based on the provided filters. ```ts filename="Search by Group Name" import { fetchGroups } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchGroups(client, { filter: { searchQuery: "group", }, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="Managed by an Address" import { evmAddress } from "@lens-protocol/client"; import { fetchGroups } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchGroups(client, { filter: { managedBy: { address: evmAddress("0x1234…"), }, }, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="By Member Address" import { evmAddress } from "@lens-protocol/client"; import { fetchGroups } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchGroups(client, { filter: { member: evmAddress("0x1234…"), }, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="By App" import { evmAddress } from "@lens-protocol/client"; import { fetchGroups } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchGroups(client, { filter: { app: evmAddress("0x1234…"), }, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; export const client = PublicClient.create({ environment: mainnet, }); ``` Use the paginated `groups` query to fetch a list of Groups based on the provided filters. ```graphql filename="Query" query { groups( request: { filter: { # the groups this account is a member of member: "0x1234…" # OR a search query # searchQuery: String # OR managed by a specific address # managedBy: ManagedBy # OR app # app: "0x1234…" } orderBy: ALPHABETICAL # other options: LATEST_FIRST, OLDEST_FIRST } ) { items { address timestamp feed owner banningEnabled membershipApprovalEnabled metadata { description id icon name coverPicture } } pageInfo { prev next } } } ``` ```json filename="Response" { "data": { "groups": { "items": [ { "address": "0x1234…", "timestamp": "2021-09-01T00:00:00Z", "feed": "0x5678…", "owner": "0x9012…", "banningEnabled": true, "membershipApprovalEnabled": true, "metadata": { "id": "1234…", "description": "A group for builders", "icon": "https://example.com/icon.png", "name": "Builders", "coverPicture": "https://example.com/cover.png" } } ], "pageInfo": { "prev": null, "next": null } } } } ```
See the [Pagination](../best-practices/pagination) guide for more information on how to handle paginated results. ================ File: src/pages/protocol/groups/join.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Join Groups This guide will help you manage Group membership on Lens. --- ## Join a Group To join a Group, follow these steps. You MUST be authenticated as [Account Owner or Account Manager](../authentication) to join a Group. ### Check Group Rules First, inspect the `group.operations.canJoin` field to determine whether the logged-in Account is allowed to join. Some Groups may have restrictions on who can join them. ```ts filename="Check Rules" switch (group.operations.canJoin.__typename) { case "GroupOperationValidationPassed": // Joining the group is allowed break; case "GroupOperationValidationFailed": // Joinin the group is not allowed console.log(group.operations.canJoin.reason); break; case "GroupOperationValidationUnknown": // Validation outcome is unknown break; } ``` Where: - `GroupOperationValidationPassed`: The logged-in Account can join the Group. - `GroupOperationValidationFailed`: Joining the Group is not allowed. The `reason` field explains why, and `unsatisfiedRules` lists the unmet requirements. - `GroupOperationValidationUnknown`: The Group has one or more _unknown rules_ requiring ad-hoc verification. The `extraChecksRequired` field provides the addresses and configurations of these rules. Treat the `GroupOperationValidationUnknown` as _failed_ unless you intend to support the specific rules. See [Group Rules](./rules) for more information. ### Join the Group Next, if allowed, join the Group. Use the `joinGroup` action to join a Group with the logged-in account. You MUST be authenticated as [Account Owner or Account Manager](../authentication) to make this request. ```ts filename="Example" import { evmAddress } from "@lens-protocol/client"; import { joinGroup } from "@lens-protocol/client/actions"; const result = await joinGroup(sessionClient, { group: evmAddress("0x1234") }); ``` Use the `joinGroup` mutation to join a Group with the logged-in account. ```graphql filename="Mutation" mutation { joinGroup(request: { group: "0x1234…" }) { ... on JoinGroupResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on GroupOperationValidationFailed { reason } ... on TransactionWillFail { reason } } } ``` ```json filename="JoinGroupResponse" { "data": { "joinGroup": { "hash": "0x…" } } } ``` Coming soon ### Handle Result Finally, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,7" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await joinGroup(sessionClient, { group: evmAddress("0x1234"), }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,7" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await joinGroup(sessionClient, { group: evmAddress("0x1234"), }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Finally, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Coming soon ## Leave a Group To leave a Group, follow these steps. You MUST be authenticated as [Account Owner or Account Manager](../authentication) to leave a Group. ### Check Group Rules First, inspect the `group.operations.canLeave` field to determine whether the logged-in Account is allowed to leave. Some Groups may have restrictions on who can leave them. ```ts filename="Check Rules" switch (group.operations.canLeave.__typename) { case "GroupOperationValidationPassed": // Leaving the group is allowed break; case "GroupOperationValidationFailed": // Leaving the group is not allowed console.log(group.operations.canLeave.reason); break; case "GroupOperationValidationUnknown": // Validation outcome is unknown break; } ``` Where: - `GroupOperationValidationPassed`: The logged-in Account can leave the Group. - `GroupOperationValidationFailed`: Leaving the Group is not allowed. The `reason` field explains why, and `unsatisfiedRules` lists the unmet requirements. - `GroupOperationValidationUnknown`: The Group has one or more _unknown rules_ requiring ad-hoc verification. The `extraChecksRequired` field provides the addresses and configurations of these rules. Treat the `GroupOperationValidationUnknown` as _failed_ unless you intend to support the specific rules. See [Group Rules](./rules) for more information. ### Leave a Group Next, if allowed, leave the Group. Use the `leaveGroup` action to leave a Group with the logged-in account. You MUST be authenticated as [Account Owner or Account Manager](../authentication) to make this request. ```ts filename="Example" import { evmAddress } from "@lens-protocol/client"; import { leaveGroup } from "@lens-protocol/client/actions"; const result = await leaveGroup(sessionClient, { group: evmAddress("0x1234") }); ``` Use the `leaveGroup` mutation to leave a Group with the logged-in account. ```graphql filename="Mutation" mutation { leaveGroup(request: { group: "0x1234…" }) { ... on LeaveGroupResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on GroupOperationValidationFailed { reason } ... on TransactionWillFail { reason } } } ``` ```json filename="LeaveGroupResponse" { "data": { "leaveGroup": { "hash": "0x…" } } } ``` Coming soon ### Handle Result Finally, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,7" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await leaveGroup(sessionClient, { account: evmAddress("0x1234"), }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,7" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await leaveGroup(sessionClient, { account: evmAddress("0x1234"), }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Finally, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Coming soon ================ File: src/pages/protocol/groups/manage.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Manage Groups This guide explains how to manage Groups on Lens. --- ## Update Group Metadata To update a Group Metadata, follow these steps. You MUST be authenticated as [Builder](../authentication), [Account Manager](../authentication), or [Account Owner](../authentication) and be either the owner or an admin of the Group you intend to update. ### Create New Metadata First, create a new Group Metadata object with the updated details. It's developer responsability to copy over any existing data that should be retained. The process is similar to the one in the [Create a Group](./create) guide, so we will keep this example brief. ```ts filename="Example" import { group } from "@lens-protocol/metadata"; const metadata = group({ name: "XYZ", description: "My group description", icon: "lens://BsdfA…", }); ``` ### Upload Metadata Next, upload the Group Metadata object to a public URI. ```ts filename="Upload Metadata" import { storageClient } from "./storage-client"; // … const { uri } = await storageClient.uploadAsJson(metadata); console.log(uri); // e.g., lens://4f91ca… ``` If [Grove storage](../../storage) was used you can also decide to edit the file at the existing URI. See [Editing Content](../../storage/usage/edit) guide for more information. ### Update Metadata URI Next, update the Group metadata URI with the new URI. Use the `setGroupMetadata` action to update the Group Metadata URI. ```ts import { uri } from "@lens-protocol/client"; import { setGroupMetadata } from "@lens-protocol/client/actions"; const result = await setGroupMetadata(sessionClient, { group: group.id, metadataUri: uri("lens://4f91ca…"), }); if (result.isErr()) { return console.error(result.error); } ``` Use the `setGroupMetadata` mutation to update the Group Metadata URI. ```graphql filename="Mutation" mutation { setGroupMetadata( request: { group: "0x1234…", contentUri: "lens://4f91ca…" } ) { ... on SetGroupMetadataResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` Coming soon ### Handle Result Finally, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await setGroupMetadata(sessionClient, { group: group.id, metadataUri: uri("lens://4f91ca…"), }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await setGroupMetadata(sessionClient, { group: group.id, metadataUri: uri("lens://4f91ca…"), }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Finally, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Coming soon ## Remove Group Member To remove a member from a Group and optionally prevent them from rejoining by setting the `ban` parameter to `true`, follow these steps: You MUST be authenticated as [Builder](../authentication), [Account Manager](../authentication), or [Account Owner](../authentication) and be either the owner or an admin of the Group you intend to remove members from. ### Remove Group Members Use the `removeGroupMembers` action to remove a members from a Group. ```ts filename="Example" import { evmAddress } from "@lens-protocol/client"; import { removeGroupMembers } from "@lens-protocol/client/actions"; const result = await removeGroupMembers(sessionClient, { group: evmAddress("0x1234…"), members: [evmAddress("0x5678…"), evmAddress("0x9012…")], ban: true, // Ban the members from joining the Group (optional parameter) }); if (result.isErr()) { return console.error(result.error); } ``` Use the `removeGroupMembers` mutation to remove a members from a Group. ```graphql filename="Mutation" mutation { removeGroupMembers(request: { group: "0x1234…", members: ["0x5678…", "0x9012…"], ban: true }) { ... on RemoveGroupMembersResponse { hash } ...on GroupOperationValidationFailed { ...GroupOperationValidationFailed } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ```json filename="RemoveGroupMembersResponse" { "data": { "removeGroupMembers": { "hash": "0x…" } } } ``` Coming soon ### Handle Result Finally, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,9" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await removeGroupMembers(sessionClient, { group: evmAddress("0x1234…"), members: [evmAddress("0x5678…"), evmAddress("0x9012…")], ban: true, }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,9" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await removeGroupMembers(sessionClient, { group: evmAddress("0x1234…"), members: [evmAddress("0x5678…"), evmAddress("0x9012…")], ban: true, }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Finally, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Coming soon ## Access Control The Group contract supports two roles: _Owner_ and _Administrator_. Administrators can: - Update the Group Metadata - Update the Group Rules - Update the Group Extra Data The Owner can do everything the administrators can do, plus transfer ownership of the Group to another address. See the [Team Management](../best-practices/team-management) guide for more information on how to manage these roles. ================ File: src/pages/protocol/groups/membership-approvals.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Membership Approvals This guide explains how to use the Membership Approval Group Rule to manage Group memberships. --- A Group configured with the [Membership Approval Group Rule](./rules#using-group-rules-to-manage-memberships) allows to create a Group where accounts requesting to join must be approved by the Group owner or an admin. >Group: Submit Join Request Z->>Group: Submit Join Request Y->>Group: Submit Join Request Y->>Group: Cancel Join Request note over X, Group: … Admin->>Group: List Join Requests Group-->>Admin: Pending Requests Admin->>Group: Approve X Request Group-->>Admin: Request Approved Admin->>Group: Reject Z Request Group-->>Admin: Request Rejected note over X, Group: … X->>Group: Can interact with Group `} /> ## Submit Join Request To submit a join request to a Group, follow these steps. You MUST be authenticated as [Account Manager or Account Owner](../authentication) to perform this operation. ### Requires Approval First, inspect the `group.membershipApprovalEnabled` flag to figure out which groups require approval for joining. ### Check Other Rules Next, inspect the `group.operations.canJoin` field to determine whether the logged-in Account is allowed to join. Some Groups may have restrictions on who can join them. ```ts filename="Check Rules" switch (group.operations.canJoin.__typename) { case "GroupOperationValidationPassed": // Joining the group is allowed break; case "GroupOperationValidationFailed": // Joinin the group is not allowed console.log(group.operations.canJoin.reason); break; case "GroupOperationValidationUnknown": // Validation outcome is unknown break; } ``` Where: - `GroupOperationValidationPassed`: The logged-in Account can join the Group. - `GroupOperationValidationFailed`: Joining the Group is not allowed. The `reason` field explains why, and `unsatisfiedRules` lists the unmet requirements. - `GroupOperationValidationUnknown`: The Group has one or more _unknown rules_ requiring ad-hoc verification. The `extraChecksRequired` field provides the addresses and configurations of these rules. Treat the `GroupOperationValidationUnknown` as _failed_ unless you intend to support the specific rules. See [Group Rules](./rules) for more information. ### Request to Join Next, if allowed, request to join the Group. Use the `requestGroupMembership` action to submit a join request to a Group. ```ts filename="Join Request" import { evmAddress } from "@lens-protocol/client"; import { requestGroupMembership } from "@lens-protocol/client/actions"; const result = await requestGroupMembership(sessionClient, { group: evmAddress("0xe2f…"), }); if (result.isErr()) { return console.error(result.error); } ``` Use the `requestGroupMembership` mutation to submit a join request to a Group. ```graphql filename="Mutation" mutation { requestGroupMembership(request: { group: "0xe2f…" }) { ... on RequestGroupMembershipResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` Coming soon ### Handle Result Then, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,7" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await requestGroupMembership(sessionClient, { group: evmAddress("0xe2f…"), }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,7" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await requestGroupMembership(sessionClient, { group: evmAddress("0xe2f…"), }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Then, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Coming soon That's it—these accounts are now banned from joining the Group. ## Cancel Join Request You can cancel a pending join request at any time before it is approved. You MUST be authenticated as [Account Manager or Account Owner](../authentication) to perform this operation. ### Cancel the Request Use the `cancelGroupMembershipRequest` action to cancel a pending join request to a Group. ```ts filename="Cancel Request" import { evmAddress } from "@lens-protocol/client"; import { cancelGroupMembershipRequest } from "@lens-protocol/client/actions"; const result = await cancelGroupMembershipRequest(sessionClient, { group: evmAddress("0xe2f…"), }); if (result.isErr()) { return console.error(result.error); } ``` Use the `cancelGroupMembershipRequest` mutation to cancel a pending join request to a Group. ```graphql filename="Mutation" mutation { cancelGroupMembershipRequest(request: { group: "0xe2f…" }) { ... on CancelGroupMembershipRequestResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` Coming soon ### Handle Result Then, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,7" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await cancelGroupMembershipRequest(sessionClient, { group: evmAddress("0xe2f…"), }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,7" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await cancelGroupMembershipRequest(sessionClient, { group: evmAddress("0xe2f…"), }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Then, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Coming soon That's it—these accounts are now banned from joining the Group. ## Manage Join Requests A Group owner or admin can list, approve, or reject pending join requests for a Group. You MUST be authenticated as [Builder](../authentication), [Account Manager](../authentication) or [Account Owner](../authentication) and be either the Group owner or an admin to perform this action. ### List Join Requests Use the paginated `fetchGroupMembershipRequests` action to list all pending join requests for a given Group. ```ts filename="List Requests" import { evmAddress } from "@lens-protocol/client"; import { fetchGroupMembershipRequests } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchGroupMembershipRequests(client, { group: evmAddress("0x1234…"), }); if (result.isErr()) { return console.error(result.error); } // items: Array: [{ account: Account, requestedAt: DateTime, lastActiveAt: DateTime, …}, …] const { items, pageInfo } = result.value; ``` Use the paginated `groupMembershipRequests` query to list all pending join requests for a given Group. ```graphql filename="Query" query { groupMembershipRequests(request: { group: "0x1234…" }) { items { ruleId requestedAt lastActiveAt account { ...Account } } pageInfo { prev next } } } ``` ```graphql filename="Account" fragment Account on Account { address username { value } metadata { name picture } } ``` ```json filename="Response" { "data": { "groupMembershipRequests": [ { "ruleId": "0x1234…", "requestedAt": "2022-01-01T00:00:00Z", "lastActiveAt": "2022-01-01T00:00:00Z", "account": { "address": "0x1234…", "username": { "value": "alice" }, "metadata": { "name": "Alice", "picture": "https://example.com/alice.jpg" } } } ] } } ``` Coming soon See the [Pagination](../best-practices/pagination) guide for more information on how to handle paginated results. ### Approve Requests Use the `approveGroupMembershipRequests` action to approve join requests for a Group. ```ts filename="Approve Requests" import { evmAddress } from "@lens-protocol/client"; import { approveGroupMembershipRequests } from "@lens-protocol/client/actions"; const result = await approveGroupMembershipRequests(sessionClient, { group: evmAddress("0xe2f…"), accounts: [evmAddress("0x4f91…"), evmAddress("0x8765…")], }); if (result.isErr()) { return console.error(result.error); } ``` And, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await approveGroupMembershipRequests(sessionClient, { group: evmAddress("0xe2f…"), accounts: [evmAddress("0x4f91…"), evmAddress("0x8765…")], }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await approveGroupMembershipRequests(sessionClient, { group: evmAddress("0xe2f…"), accounts: [evmAddress("0x4f91…"), evmAddress("0x8765…")], }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Use the `approveGroupMembershipRequests` mutation to approve join requests for a Group. ```graphql filename="Mutation" mutation { approveGroupMembershipRequests( request: { group: "0xe2f…", accounts: ["0x4f91…", "0x8765…"] } ) { ... on ApproveGroupMembershipRequestsResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` And, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Coming soon ### Reject Requests Use the `rejectGroupMembershipRequests` action to reject join requests for a Group. ```ts filename="Approve Requests" import { evmAddress } from "@lens-protocol/client"; import { rejectGroupMembershipRequests } from "@lens-protocol/client/actions"; const result = await rejectGroupMembershipRequests(sessionClient, { group: evmAddress("0xe2f…"), accounts: [evmAddress("0x4f91…"), evmAddress("0x8765…")], }); if (result.isErr()) { return console.error(result.error); } ``` And, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await rejectGroupMembershipRequests(sessionClient, { group: evmAddress("0xe2f…"), accounts: [evmAddress("0x4f91…"), evmAddress("0x8765…")], }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await rejectGroupMembershipRequests(sessionClient, { group: evmAddress("0xe2f…"), accounts: [evmAddress("0x4f91…"), evmAddress("0x8765…")], }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Use the `rejectGroupMembershipRequests` mutation to reject join requests for a Group. ```graphql filename="Mutation" mutation { rejectGroupMembershipRequests( request: { group: "0xe2f…", accounts: ["0x4f91…", "0x8765…"] } ) { ... on RejectGroupMembershipRequestsResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` And, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Coming soon ================ File: src/pages/protocol/groups/rules.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Group Rules This guide explains how to use Group Rules and how to implement custom ones. --- Group Rules allow administrators to add requirements or constraints for joining or leaving a Group. Lens provides four built-in Group rules: - `SimplePaymentGroupRule` - Requires an ERC-20 payment to join the Group. - `TokenGatedGroupRule` - Requires an account to hold a certain token to join the Group. - `MembershipApprovalGroupRule` - Requires approval by owner or administrators of the Group to join. For the `SimplePaymentGroupRule`, a **1.5%** Lens treasury fee is deducted from the payment before the remaining amount is transferred to the designated recipient. It is also possible to use custom Group Rules to extend the functionality of your Group. ## Using Group Rules This section presumes you are familiar with the process of [creating a Group](./create) on Lens. ### Simple Payment Group Rule This rule requires a native GHO (GRASS on the Lens Testnet) or ERC-20 payment to join a Group. Configuration includes the token address, the payment amount, and the recipient address. ```ts filename="ERC-20 Payment" import { bigDecimal, evmAddress, uri } from "@lens-protocol/client"; import { createGroup } from "@lens-protocol/client/actions"; const result = await createGroup(sessionClient, { metadataUri: uri("lens://4f91c…"), rules: { required: [ { simplePaymentRule: { erc20: { currency: evmAddress("0x5678…"), value: bigDecimal("1.0"), }, recipient: evmAddress("0x9012…"), }, }, ], }, }); ``` ```ts filename="Native Payment" import { bigDecimal, evmAddress, uri } from "@lens-protocol/client"; import { createGroup } from "@lens-protocol/client/actions"; const result = await createGroup(sessionClient, { metadataUri: uri("lens://4f91c…"), rules: { required: [ { simplePaymentRule: { native: bigDecimal("1.0"), recipient: evmAddress("0x9012…"), }, }, ], }, }); ``` ```graphql filename="Mutation" mutation { createGroup( request: { metadataUri: "lens://4f91c…" rules: { required: [ { simplePaymentRule: { erc20: { currency: "0x5678…", value: "1.0" } # or # native: "1.0", recipient: "0x9012…" } } ] } } ) { ... on CreateGroupResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` Coming soon ### Token Gated Group Rule This rule requires holding a certain balance of a token (fungible or non-fungible) to join a Group. Configuration includes the token address, the token standard (ERC-20, ERC-721, or ERC-1155), and the required token amount. For ERC-1155 tokens, an additional token type ID is required. ```ts filename="Token Gated Group Rule" import { bigDecimal, evmAddress, uri, TokenStandard, } from "@lens-protocol/client"; import { createGroup } from "@lens-protocol/client/actions"; const result = await createGroup(sessionClient, { metadataUri: uri("lens://4f91c…"), rules: { required: [ { tokenGatedRule: { token: { currency: evmAddress("0x1234…"), standard: TokenStandard.Erc721, value: bigDecimal("1"), }, }, }, ], }, }); ``` ```graphql filename="Mutation" mutation { createGroup( request: { metadataUri: "lens://4f91c…" rules: { required: [ { tokenGatedRule: { token: { standard: ERC721, currency: "0x1234…", value: "1" } } } ] } } ) { ... on CreateGroupResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` Coming soon ### Membership Approval Group Rule This rule requires approval by the owner or administrators to join a Group. ```ts filename="Membership Approval Group Rule" import { uri } from "@lens-protocol/client"; import { createGroup } from "@lens-protocol/client/actions"; const result = await createGroup(sessionClient, { metadataUri: uri("lens://4f91c…"), rules: { required: [{ membershipApprovalRule: { enable: true } }], }, }); ``` ```graphql filename="Mutation" mutation { createGroup( request: { metadataUri: "lens://4f91c…" rules: { required: [{ membershipApprovalRule: { enable: true } }] } } ) { ... on CreateGroupResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` Coming soon See the [Membership Approvals](./membership-approvals) guide for more information on how to handle membership approval. ### Custom Group Rules You can also use custom rules by specifying the rule contract address, when it applies, and the configuration parameters as key-value pairs. ```ts filename="Custom Group Rule" import { blockchainData, evmAddress, GroupRuleExecuteOn, uri, } from "@lens-protocol/client"; import { createGroup } from "@lens-protocol/client/actions"; const result = await createGroup(sessionClient, { metadataUri: uri("lens://4f91c…"), rules: { required: [ { unknownRule: { address: evmAddress("0x1234…"), executeOn: [GroupRuleExecuteOn.Joining], params: [ { raw: { // 32 bytes key (e.g., keccak(name)) key: blockchainData("0xac5f04…"), // an ABI encoded value data: blockchainData("0x00"), }, }, ], }, }, ], }, }); ``` ```graphql filename="Mutation" mutation { createGroup( request: { metadataUri: "lens://4f91c…" rules: { required: [ { unknownRule: { address: "0x1234…" executeOn: [JOINING] params: [ { raw: { key: "0x4f…" # 32-byte key (e.g., keccak(name)) data: "0x00" # ABI-encoded value } } ] } } ] } } ) { ... on CreateGroupResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` Coming soon ### Combining Rules Additionally, multiple rules can be combined: ```ts filename="Combining Rules" import { bigDecimal, evmAddress, TokenStandard, uri, } from "@lens-protocol/client"; import { createGroup } from "@lens-protocol/client/actions"; const result = await createGroup(sessionClient, { metadataUri: uri("lens://4f91c…"), rules: { required: [ { simplePaymentRule: { erc20: { currency: evmAddress("0x5678…"), value: bigDecimal("10.42"), }, recipient: evmAddress("0x9012…"), }, }, ], anyOf: [ { tokenGatedRule: { token: { currency: evmAddress("0x1234…"), standard: TokenStandard.Erc721, value: bigDecimal("1"), }, }, }, { tokenGatedRule: { token: { currency: evmAddress("0x3456…"), standard: TokenStandard.Erc721, value: bigDecimal("1"), }, }, }, ], }, }); ``` ```graphql filename="Mutation" mutation { createGroup( request: { metadataUri: "lens://4f91c…" rules: { required: [ { simplePaymentRule: { erc20: { currency: "0x5678…", value: "1.0" } # or # native: "1.0", recipient: "0x9012…" } } ] anyOf: [ { tokenGatedRule: { token: { standard: ERC721, currency: "0x1234…", value: "1" } } } { tokenGatedRule: { token: { standard: ERC721, currency: "0x3456…", value: "1" } } } ] } } ) { ... on CreateGroupResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` Coming soon ### Update a Group Rules To update a Group rules configuration, follow these steps. You MUST be authenticated as [Builder](../authentication), [Account Manager](../authentication), or [Account Owner](../authentication) and be either the owner or admin of the Group to update its Rules. #### Identify Current Rules First, inspect the: - `group.membershipApprovalEnabled` flag to know if the Group has the `MembershipApprovalGroupRule` enabled. - `group.rules` field to know details on all rules configuration of the Group. ```ts filename="GroupRules" type GroupRules = { __typename: "GroupRules"; required: GroupRule[]; anyOf: GroupRule[]; }; ``` ```ts filename="GroupRule" type GroupRule = { __typename: "GroupRule"; id: RuleId; type: GroupRuleType; address: EvmAddress; executesOn: GroupRuleExecuteOn[]; config: AnyKeyValue[]; }; ``` ```ts filename="AnyKeyValue" type AnyKeyValue = | IntKeyValue | IntNullableKeyValue | AddressKeyValue | StringKeyValue | BooleanKeyValue | RawKeyValue | BigDecimalKeyValue | DictionaryKeyValue | ArrayKeyValue; ``` ```ts filename="ArrayKeyValue" type ArrayKeyValue = { __typename: "ArrayKeyValue"; key: string; array: | IntKeyValue | IntNullableKeyValue | AddressKeyValue | StringKeyValue | BooleanKeyValue | RawKeyValue | BigDecimalKeyValue | DictionaryKeyValue; }; ``` ```ts filename="DictionaryKeyValue" type DictionaryKeyValue = { __typename: "DictionaryKeyValue"; key: string; dictionary: | IntKeyValue | IntNullableKeyValue | AddressKeyValue | StringKeyValue | BooleanKeyValue | RawKeyValue | BigDecimalKeyValue; }; ``` ```ts filename="Others" type IntKeyValue = { __typename: "IntKeyValue"; key: string; int: number; }; type IntNullableKeyValue = { __typename: "IntNullableKeyValue"; key: string; optionalInt: number | null; }; type AddressKeyValue = { __typename: "AddressKeyValue"; key: string; address: EvmAddress; }; type StringKeyValue = { __typename: "StringKeyValue"; key: string; string: string; }; type BooleanKeyValue = { __typename: "BooleanKeyValue"; key: string; boolean: boolean; }; type RawKeyValue = { __typename: "RawKeyValue"; key: string; data: BlockchainData; }; type BigDecimalKeyValue = { __typename: "BigDecimalKeyValue"; key: string; bigDecimal: BigDecimal; }; ``` ```graphql filename="GroupRules" type GroupRules { required: [GroupRule!]! anyOf: [GroupRule!]! } ```` ```graphql filename="GroupRule" type GroupRule { id: RuleId! type: GroupRuleType! address: EvmAddress! executesOn: [GroupRuleExecuteOn!]! config: [AnyKeyValue!]! } ``` ```graphql filename="AnyKeyValue" fragment AnyKeyValue on AnyKeyValue { ... on IntKeyValue { ...IntKeyValue } ... on IntNullableKeyValue { ...IntNullableKeyValue } ... on AddressKeyValue { ...AddressKeyValue } ... on StringKeyValue { ...StringKeyValue } ... on BooleanKeyValue { ...BooleanKeyValue } ... on RawKeyValue { ...RawKeyValue } ... on BigDecimalKeyValue { ...BigDecimalKeyValue } ... on DictionaryKeyValue { ...DictionaryKeyValue } ... on ArrayKeyValue { ...ArrayKeyValue } } ``` ```graphql filename="ArrayKeyValue" fragment ArrayKeyValue on ArrayKeyValue { __typename key array { ... on IntKeyValue { ...IntKeyValue } ... on IntNullableKeyValue { ...IntNullableKeyValue } ... on AddressKeyValue { ...AddressKeyValue } ... on StringKeyValue { ...StringKeyValue } ... on BooleanKeyValue { ...BooleanKeyValue } ... on RawKeyValue { ...RawKeyValue } ... on BigDecimalKeyValue { ...BigDecimalKeyValue } ... on DictionaryKeyValue { ...DictionaryKeyValue } } } ``` ```graphql filename="DictionaryKeyValue" fragment DictionaryKeyValue on DictionaryKeyValue { __typename key dictionary { ... on IntKeyValue { ...IntKeyValue } ... on IntNullableKeyValue { ...IntNullableKeyValue } ... on AddressKeyValue { ...AddressKeyValue } ... on StringKeyValue { ...StringKeyValue } ... on BooleanKeyValue { ...BooleanKeyValue } ... on RawKeyValue { ...RawKeyValue } ... on BigDecimalKeyValue { ...BigDecimalKeyValue } } } ``` ```graphql filename="Others" fragment IntKeyValue on IntKeyValue { __typename key int } fragment IntNullableKeyValue on IntNullableKeyValue { __typename key optionalInt } fragment AddressKeyValue on AddressKeyValue { __typename key address } fragment StringKeyValue on StringKeyValue { __typename key string } fragment BooleanKeyValue on BooleanKeyValue { __typename key boolean } fragment RawKeyValue on RawKeyValue { __typename key data } fragment BigDecimalKeyValue on BigDecimalKeyValue { __typename key bigDecimal } ``` The configuration for the built-in rules with one or more parameters is as follows. | Key | Type | Description | | --------------- | ------------ | ----------------------------------------------- | | `assetContract` | `EvmAddress` | Address of the ERC-20 or native token contract. | | `assetName` | `String` | Name of the ERC-20 or native token. | | `assetSymbol` | `String` | Symbol of the ERC-20 or native token. | | `amount` | `BigDecimal` | Payment required to join. | | Key | Type | Description | | --------------- | ------------ | ------------------------------------------ | | `assetContract` | `EvmAddress` | Address of the token contract. | | `assetName` | `String` | Name of the token. | | `assetSymbol` | `String` | Symbol of the token. | | `amount` | `BigDecimal` | Minimum number of tokens required to join. | Keep note of the Rule IDs you might want to remove. #### Update the Rules Configuration Next, update the rules configuration of the Group as follows. Use the `updateGroupRules` action to update the rules configuration of a given group. ```ts filename="Add Rules" import { bigDecimal, evmAddress, TokenStandard } from "@lens-protocol/client"; import { updateGroupRules } from "@lens-protocol/client/actions"; const result = await updateGroupRules(sessionClient, { group: group.address, toAdd: { required: [ { tokenGatedRule: { token: { currency: evmAddress("0x1234…"), standard: TokenStandard.Erc721, value: bigDecimal("1"), }, }, }, ], }, }); ``` ```ts filename="Remove Rules" import { updateGroupRules } from "@lens-protocol/client/actions"; const result = await updateGroupRules(sessionClient, { group: group.address, toRemove: [group.rules.required[0].id], }); ``` Use the `updateGroupRules` mutation to update the rules configuration of a given group. ```graphql filename="Add Rules" mutation { updateGroupRules( request: { group: "0x1234…" toAdd: { required: [ { tokenGatedRule: { token: { standard: ERC20, currency: "0x5678…", value: "1.5" } } } ] } } ) { ... on UpdateGroupRulesResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ```graphql filename="Remove Rules" mutation { updateGroupRules(request: { group: "0x1234…", toRemove: ["ej6g…"] }) { ... on UpdateGroupRulesResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` Coming soon #### Handle Result Then, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,9,10" import { handleOperationWith } from "@l ens-protocol/client/viem"; // … const result = await updateGroupRules(sessionClient, { group: group.address, // … }) .andThen(handleOperationWith(walletClient)) .andThen(sessionClient.waitForTransaction); ``` ```ts filename="ethers" highlight="1,9,10" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await updateGroupRules(sessionClient, { group: group.address, // … }) .andThen(handleOperationWith(signer)) .andThen(sessionClient.waitForTransaction); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Then, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Coming soon --- ## Building a Group Rule Let's illustrate the process with an example. We will build a custom Group Rule that requires accounts to have more than a certain amount of Followers in order to join the Group. To build a custom Group Rule, you must implement the following `IGroupRule` interface: ```solidity interface IGroupRule { function configure(bytes32 configSalt, KeyValue[] calldata ruleParams) external; function processAddition( bytes32 configSalt, address originalMsgSender, address account, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external; function processRemoval( bytes32 configSalt, address originalMsgSender, address account, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external; function processJoining( bytes32 configSalt, address account, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external; function processLeaving( bytes32 configSalt, address account, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external; } ``` Each function of this interface must assume to be invoked by the Group contract. In other words, assume the `msg.sender` will be the Group contract. A Lens dependency package with all relevant interfaces will be available soon. ### Implement the Configure Function First, implement the `configure` function. This function has the purpose of initializing any required state for the rule to work properly. It receives two parameters, a 32-byte configuration salt (`configSalt`), and an array of custom parameters as key-value pairs (`ruleParams`). The `configSalt` is there to allow the same rule contract to be used many times, with different configurations, for the same Group. So, for a given Group Rule implementation, the pair (Group Address, Configuration Salt) should identify a rule configuration. For example, let's think about the `TokenGatedGroupRule`, we could want to achieve the restriction "To join this Group, you must hold 10 WETH and 1 WBTC". However, the `TokenGatedGroupRule` only allows to configure a single token as a gate. So, instead of writing a whole new contract that receives two tokens instead of one, we would just configure the `TokenGatedGroupRule` rule twice in the same Group, once for WETH with some configuration salt, and once for WBTC with another configuration salt. The `configure` function can be called multiple times by the same Group passing the same configuration salt in order to update that rule configuration (i.e. reconfigure it). The `ruleParams` is an array of key-value pairs that can be used to pass any custom configuration parameters to the rule. Each key is `bytes32`, we put the hash of the parameter name there, and each value is `bytes`, we set the ABI-encoded parameter value there. Given that `ruleParams` is an array, this allows the rule to define which parameters are optional and which are required, acting accordingly when any of them are not present. In our example, we need to decode two parameters: an address, representing the Graph where to perform the amount of followers check, and an integer, which will represent the minimum amount of Followers an account must have in order to become a member of the Group. Let's define a storage mapping to store this configuration: ```solidity contract FollowerCountGatedGroupRule is IGroupRule { mapping(address group => mapping(bytes32 configSalt => address graph)) internal _graphToCheck; mapping(address group => mapping(bytes32 configSalt => uint256 minFollowers)) internal _minFollowers; } ``` The configuration is stored in the mapping using the Group contract address and the configuration salt as keys. With this setup, the same rule can be used by different Groups, as well as be used by the same Group many times. Now let's code the `configure` function itself, decoding the parameters and putting them into the storage: ```solidity contract FollowerCountGatedGroupRule is IGroupRule { mapping(address group => mapping(bytes32 configSalt => address graph)) internal _graphToCheck; mapping(address group => mapping(bytes32 configSalt => uint256 minFollowers)) internal _minFollowers; function configure(bytes32 configSalt, KeyValue[] calldata ruleParams) external override { address graphToCheck; uint256 minFollowers = 100; for (uint256 i = 0; i < ruleParams.length; i++) { if (ruleParams[i].key == keccak256("lens.param,graph")) { graphToCheck = abi.decode(ruleParams[i].value, (address)); } else if (ruleParams[i].key == keccak256("lens.param,minFollowers")) { minFollowers = abi.decode(ruleParams[i].value, (uint256)); } } // Aims to check if the graph is a valid Graph contract IGraph(graphToCheck).getFollowersCount(address(this)); _graphToCheck[msg.sender][configSalt] = graphToCheck; _minFollowers[msg.sender][configSalt] = minFollowers; } } ``` As you can see, we treated the minimum followers parameter as optional, setting a default value of 100 followers for it in case that the parameter was not found in the `ruleParams`. On the other hand, the graph parameter is treated as required, without default value, and needing to be found as a valid graph address in order for the `getFollowersCount` call to not revert. ### Implement the Process Joining function Next, implement the `processJoining` function. This function is invoked by the Group contract every time someone tries to join the Group, so then our custom logic can be applied to shape under which conditions the operation can succeed. The function receives the configuration salt (`configSalt`), so we know which configuration to use, the account attempting to join the Group (`account`), an array of key-value pairs with the custom parameters passed to the Group (`primitiveParams`), and an array of key-value pairs in case the rule requires additional parameters to work (`ruleParams`). The function must revert in case of not meeting the requirements imposed by the rule. We first get the configured Graph where to perform the check: ```solidity contract FollowerCountGatedGroupRule is IGroupRule { mapping(address group => mapping(bytes32 configSalt => address graph)) internal _graphToCheck; mapping(address group => mapping(bytes32 configSalt => uint256 minFollowers)) internal _minFollowers; // . . . function processJoining( bytes32 configSalt, address account, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external override { // We get the Graph where to check the followers count IGraph graph = IGraph(_graphToCheck[msg.sender][configSalt]); } } ``` Next we require that `account` has at least the amount of followers required by the rule configuration. The `IGraph` interface contains this function in order to check the amount of followers of a given account: ```solidity function getFollowersCount(address account) external view returns (uint256); ``` So, let's add the requirement check and we are done with this function: ```solidity contract FollowerCountGatedGroupRule is IGroupRule { mapping(address group => mapping(bytes32 configSalt => address graph)) internal _graphToCheck; mapping(address group => mapping(bytes32 configSalt => uint256 minFollowers)) internal _minFollowers; // . . . function processJoining( bytes32 configSalt, address account, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external override { IGraph graph = IGraph(_graphToCheck[msg.sender][configSalt]); // We check if the account has the min required amount of followers require(graph.getFollowersCount(account) >= _minFollowers[msg.sender][configSalt]); } } ``` ### Implement the Process Leaving function Next, implement the `processLeaving` function. This function is invoked by the Group contract every time someone tries to leave the Group, so then our custom logic can be applied to shape under which conditions the operation can succeed. The parameters are the same as in the `processJoining` function except that the `account` is trying to leave the Group instead of joining it. Given that we do not want to apply any restriction to the leaving operation, we can just revert the function with a `NotImplemented` error, which is the safest approach in case someone accidentally applies this rule: ```solidity contract FollowerCountGatedGroupRule is IGroupRule { // . . . function processLeaving( bytes32 configSalt, address account, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external override { revert Errors.NotImplemented(); } } ``` ### Implement the Process Removal function Next, implement the `processRemoval` function. This function is invoked by the Group contract every time a Group member is trying to be removed, so then our rule can define if this operation must succeed or not. The function receives the configuration salt (`configSalt`), so we know which configuration to use, the address that is trying to remove the member (`originalMsgSender`), the account trying to be removed of the Group (`account`), an array of key-value pairs with the custom parameters passed to the Group (`primitiveParams`), and an array of key-value pairs in case the rule requires additional parameters to work (`ruleParams`). The function must revert in case of not meeting the requirements imposed by the rule. In this case, we do not want to impose any restriction on the removal operation. As a good practice, we revert for unimplemented rule functions, as it is safer in case the rule becomes accidentally applied. ```solidity contract FollowerCountGatedGroupRule is IGroupRule { // . . . function processRemoval( bytes32 configSalt, address originalMsgSender, address account, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external override { revert Errors.NotImplemented(); } } ``` ### Implement the Process Addition function Finally, implement the `processAddition` function. This function is invoked by the Group contract every time a Group member is trying to be added, so then our rule can define if this operation must succeed or not. The parameters are the same as in the `processRemoval` function except that the `account` is trying to be added to the Group instead of removed from it. Same as in the `processRemoval`, here in the `processAddition` we do not want to impose any restriction, so we revert the function with a `NotImplemented` error: ```solidity contract FollowerCountGatedGroupRule is IGroupRule { // . . . function processAddition( bytes32 configSalt, address originalMsgSender, address account, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external override { revert Errors.NotImplemented(); } } ``` Now the `FollowerCountGatedGroupRule` is ready to be applied into any Group. See the full code below: ```solidity contract FollowerCountGatedGroupRule is IGroupRule { mapping(address group => mapping(bytes32 configSalt => address graph)) internal _graphToCheck; mapping(address group => mapping(bytes32 configSalt => uint256 minFollowers)) internal _minFollowers; function configure(bytes32 configSalt, KeyValue[] calldata ruleParams) external override { address graphToCheck; uint256 minFollowers = 100; for (uint256 i = 0; i < ruleParams.length; i++) { if (ruleParams[i].key == keccak256("lens.param,graph")) { graphToCheck = abi.decode(ruleParams[i].value, (address)); } else if (ruleParams[i].key == keccak256("lens.param,minFollowers")) { minFollowers = abi.decode(ruleParams[i].value, (uint256)); } } // Aims to check if the graph is a valid Graph contract IGraph(graphToCheck).getFollowersCount(address(this)); _graphToCheck[msg.sender][configSalt] = graphToCheck; _minFollowers[msg.sender][configSalt] = minFollowers; } function processAddition( bytes32 configSalt, address originalMsgSender, address account, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external override { revert Errors.NotImplemented(); } function processRemoval( bytes32 configSalt, address originalMsgSender, address account, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external override { revert Errors.NotImplemented(); } function processJoining( bytes32 configSalt, address account, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external override { // We get the Graph where to check the followers count IGraph graph = IGraph(_graphToCheck[msg.sender][configSalt]); // We check if the account has the min required amount of followers require(graph.getFollowersCount(account) >= _minFollowers[msg.sender][configSalt]); } function processLeaving( bytes32 configSalt, address account, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external override { revert Errors.NotImplemented(); } } ``` Stay tuned for API integration of rules and more guides! ================ File: src/pages/protocol/migration/api.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # API Migration This guide will walk you through the necessary steps to upgrade to the latest versions of the API. --- This guide assumes familiarity with Lens Protocol v2 and the Lens API v2. ## Changed API Calls This guide below will list old Lens v2 GraphQL queries and mutations mapped to the new Lens v3 ones. As GraphQL is a type system we will not describe if the request object has changed as you can easily see this on the GraphQL playground. Note we have not integrated actions and rules yet in the API and indexer so they are missing from here for now. ### Queries #### Restructured Only listing changes from v2 to v3 and not any new queries. - `challenge` > now a mutation - `verify` > [use RSA keys to verify](docs/features/authentication#advanced-topics-authentication-tokens) - `approvedAuthentications` > `authenticatedSessions` - `currentSession` > `currentSession` - `ownedHandles` > `usernames` - `handleToAddress` > `account` - `feed` > `timeline` - `feedHighlights` > `timelineHighlights` - `mutualFollowers` > `followersYouKnow` - `followStatusBulk` > `followStatus` - `profiles` > `accountsBulk` - `profile` > `account` - `profileInterestsOptions` > it will be moved to metadata - `whoHaveBlocked` > `accountsBlocked` - `lastLoggedInProfile` > `lastLoggedInAccount` - `profileManagers` > `accountManagers` - `profilesManaged` > `accountsAvailable` - `profileRecommendations` > `mlAccountRecommendations` - `searchProfiles` > `accounts` - `publications` > `posts` and `postReferences` - `publication` > `post` - `publicationsTags` > `postTags` - `publicationBookmarks` > `postBookmarks` - `whoReactedPublication` > `postReactions` - `didReactOnPublication` > `postReactionsStatus` - `explorePublications` > `mlPostsExplore` - `validatePublicationMetadata` > `debugMetadata` - `forYou` > `mlPostsForYou` - `searchPublications` > `posts` - `userRateLimit` > `me` (under `SponsorshipAllowance`) - `lensTransactionStatus` > `transactionStatus` - `whoActedOnPublication` > `whoExecutedActionOnPost` - `supportedOpenActionModules` > `postActionContracts` - `supportedFollowModules` > Not complete yet - `moduleMetadata` > it's inlined in the relevant GQL notes (e.g. `UnknownAction.metadata`) #### Deprecated - `approvedModuleAllowanceAmount` - `canClaim` - `claimableProfiles` - `claimableStatus` - `claimableTokens` - `claimTokens` - `defaultProfile` - `exploreProfiles` - `followRevenues` - `generateLensAPIRelayAddress` - `generateModuleCurrencyApprovalData` - `invitedProfiles` - `latestPaidActions` - `lensAPIOwnedEOAs` - `lensProtocolVersion` - `momokaSubmitters` - `momokaSummary` - `momokaTransaction` - `momokaTransactions` - `mutualNftCollections` - `mutualPoaps` - `nftCollectionOwners` - `nftCollections` - `nftGalleries` - `nfts` - `poapEvent` - `poapHolders` - `poaps` - `popularNftCollections` - `profileActionHistory` - `profileAlreadyInvited` - `relayQueues` - `revenueFromPublication` - `revenueFromPublications` - `setDefaultProfile` - `txIdToTxHash` - `userSigNonces` ### Mutations #### Restructured Only listing changes from v2 to v3 and not any new mutations. - `walletAuthenticationToProfileAuthentication` > `switchAccount` - `linkHandleToProfile` > `assignUsernameToAccount` - `unlinkHandleFromProfile` > `unassignUsernameFromAccount` - `createLinkHandleToProfileTypedData` > `assignUsernameToAccount` - `createUnlinkHandleFromProfileTypedData` > `unassignUsernameFromAccount` - `createFollowTypedData` > `follow` - `createUnfollowTypedData` > `unfollow` - `setFollowModule` > `setAccountFollowRule` - `createSetFollowModuleTypedData` > `setAccountFollowRule` - `postOnMomoka` > `post` - `commentOnMomoka` > `post` - `quoteOnMomoka` > `post` - `mirrorOnMomoka` > `post` - `createMomokaQuoteTypedData` > `post` - `createMomokaPostTypedData` > `post` - `createMomokaCommentTypedData` > `post` - `createMomokaMirrorTypedData` > `post` - `addProfileInterests` > `setAccountMetadata` - `removeProfileInterests` > `setAccountMetadata` - `dismissRecommendedProfiles` > `mlDismissRecommendedAccounts` - `reportProfile` > `reportAccount` - `peerToPeerRecommend` > `recommendAccount` - `peerToPeerUnrecommend` > `undoRecommendedAccount` - `hideManagedProfile` > `hideManagedAccount` - `unhideManagedProfile` > `unhideManagedAccount` - `setProfileMetadata` > `setAccountMetadata` - `createOnchainSetProfileMetadataTypedData` > `setAccountMetadata` - `createChangeProfileManagersTypedData` > `addAccountManager`, `removeAccountManager` and `updateAccountManager` - `createBlockProfilesTypedData` > `block` - `createUnblockProfilesTypedData` > `unblock` - `hidePublication` > `deletePost` - `hideComment` > `hideReply` - `unhideComment` > `unhideReply` - `addPublicationNotInterested` > `mlAddPostNotInterested` - `undoPublicationNotInterested` > `mlUndoPostNotInterested` - `addPublicationBookmark` > `bookmarkPost` - `removePublicationBookmark` > `undoBookmarkPost` - `removeReaction` > `undoReaction` - `reportPublication` > `reportPost` - `postOnchain` > `post` - `commentOnchain` > `post` - `quoteOnchain` > `post` - `mirrorOnchain` > `post` - `refreshPublicationMetadata` > `editPost` - `createOnchainPostTypedData` > `post` - `createOnchainCommentTypedData` > `post` - `createOnchainQuoteTypedData` > `post` - `createOnchainMirrorTypedData` > `post` - `actOnOpenAction` > Not complete yet - `createActOnOpenActionTypedData` > Not complete yet #### Deprecated - `broadcastOnMomoka` > use `post` for everything - `broadcastOnchain` - `legacyCollect` - `createLegacyCollectTypedData` - `createNftGallery` - `updateNftGalleryInfo` - `updateNftGalleryOrder` - `updateNftGalleryItems` - `deleteNftGallery` - `nftOwnershipChallenge` - `claimProfileWithHandle` - `invite` - `idKitPhoneVerifyWebhook` ## Processes - Typed data does not exist anymore its handled all in the mutation response union - Momoka does not exist anymore - Tx Id does not exist anymore ### Seamless Authentication Rollover In order to provide a seamless transition for users, we have implemented a new authentication mechanism that allows you to refresh tokens from Refresh Token issued by the Lens API v2. You can call the `legacyRolloverRefresh` mutation to acquire new authentication tokens. ```graphql filename="Mutation" mutation { legacyRolloverRefresh(request: { refreshToken: "" }) { ... on AuthenticationTokens { accessToken refreshToken idToken } ... on ForbiddenError { reason } } } ``` ```json filename="AuthenticationTokens" { "data": { "legacyRolloverRefresh": { "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6…", "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Ij…", "idToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Ij…" } } } ``` The provided Refresh Token must still be valid. Since they last for 7 days from the time they are issued, this rollover mechanism is a short-term solution to allow for a seamless transition. If you think that most of your app's users will have their Refresh Token expired by the time they try to log-in into your Lens v3 app, you probably can omit this integration and just force users to re-authenticate. ## New Features Explore the Lens v3 documentation for all the new features. ================ File: src/pages/protocol/migration/database.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Database Migration This guide will help you navigate schema changes and table updates from Lens v2 to v3. --- The Lens V3 schema has been changed on reflection from all our learning on the past protocols. If you were using Public Big Query this will show you want the names of the tables where and what they are now. This guide will only highlight V2 tables any new ones you can look on the Public Big Query. Note we have not integrated actions and rules yet in the API and indexer so they are missing from here for now. ## Key differences - You will see on the schema a more less strict primary key and foreign key structure, with Lens V3 we optimised for speed and indexing blockchain data can be a lot faster if you process things concurrently. - Database size has been optimised so we are storing binary format for all the hex values which makes each value 2x less in size alongside makes queries faster. ## Moved Tables Note the new tables may also have different columns. - `app_stats.profile` > `app.account_post_summary` - `app_stats.profile_reacted` > `app.account_reacted_summary` - `app_stats.profile_reaction` > `app.account_reaction_summary` - `app_stats.publication` > `app.post_summary` - `app_stats.publication_reaction` > `app.post_reaction_summary` - `app_stats.publication_tag` > `app.post_tag_summary` - `curation.profile` > `curation.account` - `curation.profile_tag` > `curation.account_tag` - `enabled.currency` > `currencies.record` - `global_stats.profile` > `account.post_summary` - `global_stats.profile_follower` > `account.follower_summary` - `global_stats.profile_reacted` > `account.reacted_summary` - `global_stats.profile_reaction` > `account.reaction_summary` - `global_stats.publication` > `post.summary` - `global_stats.publication_reaction` > `post.reaction_summary` - `global_stats.publication_tag` > `post.tag_summary` - `machine_learning.for_you_global_feed` > `ml.for_you_global_timeline` - `machine_learning.popularity_trending_feed` > `ml.popularity_trending_timeline` - `machine_learning.profile_boosting` > `ml.account_boosting` - `machine_learning.quality_profiles` > `ml.account_score` - `machine_learning.reply_ranking` > `ml.reply_ranking` - `namespace.handle` > `username.record` - `namespace.record` > `username.namespace_record` - `namespace.handle_link` > `account.username_assigned` - `notification.record` > `account.notification` - `personalisation.bookmarked_publication` > `account.bookmarked_post` - `personalisation.not_interested_publication` > `account.not_interested_post` - `personalisation.wtf_recommendation_dismissed` > `ml.who_to_follow_dismissed` - `personalisation.wtf_recommendation_dismissed` > `ml.who_to_follow_dismissed` - `profile.follow_module` > `account.follow_rule` - `profile.last_logged_in` > `account.last_logged_in` - `profile.follower` > `account.follower` - `profile.ownership_history` > `account.record_owner_history` - `profile.peer_to_peer_recommendation` > `account.peer_to_peer_recommendation` - `publication.mention` > `post.mention` - `profile.record` > `account.record` - `profile.reported` > `account.reported` - `publication.hashtag` > `post.hashtag` - `publication.open_action_module` > `post.action` - `publication.open_action_module_acted_record` > `account.acted` - `publication.metadata` > `post.metadata` - `publication.open_action_module_collect_nft` > `post.action` column `collect_nft_address` - `publication.open_action_module_multirecipient` > `post.action` column `recipients` - `publication.reaction` > `post.reaction` - `publication.reaction_type` > `post.reaction_type` - `publication.record` > `post.record` - `publication.reported` > `post.reported` - `publication.tag` > `post.tag` - `profile.blocked` > `account.blocked` - `profile.manager` > `account.manager` - `profile.metadata` > `account.metadata` - `profile.metadata_failed` > `metadata.failed` - `profile.metadata_pending` > `metadata.pending` - `publication.failed` > `metadata.failed` - `publication.pending` > `metadata.pending` - `publication.type` > `post.type` - `enabled.follow_module` > Not completed yet - `enable.reference_module` > Not completed yet - `enabled.open_action_module `> Not completed yet - `profile.follow_module_record` > Not completed yet - `publication.reference_module` > Not completed yet - `publication.referrer` > Not completed yet - `publication.open_action_module_acted_record_referrer` > Not completed yet ## Deprecated Tables These tables have been killed - `app.onboarding_access` - `app.onboarding_handle` - `app.onboarding_profile` - `app.profile_revenue` - `app.profile_revenue` - `app.profile_revenue_record` - `app.public_key` - `app.publication_revenue` - `app.publication_revenue_open_action` - `app.publication_revenue_record` - `app_stats.hashtag` - `app_stats.mention` - `app_stats.mention_handle` - `app_stats.profile_open_action` - `app_stats.publication_content_warning` - `app_stats.publication_locale` - `app_stats.publication_main_content_focus` - `app_stats.publication_open_action` - `app_stats.publication_tagged` - `curation.profile_interest` - `enabled.currency_history` - `enabled.follow_module_history` - `enabled.open_action_module_history` - `enable.profile_creator` - `enable.profile_creator_history` - `enable.reference_module_history` - `ens` - `fiat` - `global_stats.hashtag` - `global_stats.mention` - `global_stats.mention_handle` - `global_stats.profile_manager` - `global_stats.profile_open_action` - `global_stats.publication_content_warning` - `global_stats.publication_locale` - `global_stats.publication_main_content_focus` - `global_stats.publication_open_action` - `global_stats.publication_tagged` - `machine_learning.profile_boosting_history` - `machine_learning.proof_of_human` - `media.livepeer_mapping` - `momoka.*` - `namespace.handle_guardian` - `namespace.handle_guardian_history` - `namespace.handle_history` - `namespace.handle_link_history` - `nft.*` - `notification.type` - `personalisation.bookmarked_publication_history` - `personalisation.not_interested_publication_history` - `poap.*` - `profile.action_history` - `profile.blocked_history` - `profile.default` - `profile.follow_module_history` - `profile.follow_nft` - `profile.follower_history` - `profile.gallery` - `profile.gallery_history` - `profile.guardian` - `profile.guardian_history` - `profile.interest` - `profile.interest_history` - `profile.manager_active_config_number` - `profile.manager_active_config_number_history` - `profile.manager_all_config` - `profile.manager_all_config_history` - `profile.metadata_history` - `profile.nft_picture` - `profile.nft_picture_history` - `profile.revenue` - `profile.revenue_record` - `profile.unfollow_audit_log` - `proof_of_humanity.*` - `protocol.*` - `publication.hashtag_history` - `publication.id` - `publication.mention_history` - `publication.metadata_history` - `publication.open_action_module_collect_nft_ownership` - `publication.open_action_module_collect_nft_ownership_history` - `publication.open_action_module_history` - `publication.reaction_history` - `publication.reference_module_history` - `publication.revenue` - `publication.revenue_open_action` - `publication.revenue_record` - `publication.secured_metadata_id_executed` - `publication.tag_history` - `sybil_dot_org.*` - `worldcoin.*` ================ File: src/pages/protocol/migration/from-polygon.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Migration Plan From Lens V2 This guide will walk you through how data will be migrated from Lens v2 on Polygon to Lens v3. --- Lens V2 is currently live on Polygon and we will be migrating all the data for Lens V3 onto Lens Chain. We want to apply the migration on the initial creation of Lens Chain mainnet and in turn make it automatic and seemless as possible for everyone. Below we walk through all the data we will migrate and how we are going to approach it please note that these migration plans are still work in progress and can change as we get feedback from people. ## Profiles > Accounts Lens V2 Profiles which are now called accounts on Lens V3 will be migrated automatically, the big difference between Profiles and Accounts is that Profiles are an NFT and accounts on Lens V3 are a smart wallet. The flow of the migration will be this: ### The Profile is owned by an EOA If the Profile is owned by an EOA then we will deploy a new Account and give ownership to that EOA. ### The Profile is owned by a Safe If the Safe has a 1/1 signer and that signer is EOA we will deploy a new Safe and assign the same 1/1 signer up, we will then deploy a new Account and give ownership to that Account to the Safe. ### The Profile is owned by an unknown Contract If this is the case we will mint the Account on your behalf and have way via the Lens API you can claim it by signing if possible, if not possible we can work with you to prove ownership then change ownership of the Account over to you. ## Handles > Usernames Lens V2 Handles which are now called Usernames on Lens V3 will be migrated automatically. We will follow a similar process for how we will go about migrating Profiles onto Lens V3. Usernames can have many namespaces so all of the below will happen on the lens namespace. ### The Handle is owned by an EOA If the Handle is owned by an EOA then we will deploy a new Account and give ownership to that EOA. If that EOA has deployed an Account we will send the username to the Account smart wallet. ### The Handle is owned by a Safe If the Safe has a 1/1 signer and that signer is EOA we will deploy a new Safe and assign the same 1/1 signer up, we will then mint the username and give ownership to that username to the Safe. If the Safe has already been deployed when migrating the Account we will send the username to the Account smart wallet. ### The Handle is owned by an unknown Contract If this is the case we will mint the Username on your behalf and have way via the Lens API you can claim it by signing if possible, if not possible we can work with you to prove ownership then send the Username over to you. ## Profiles Linked To Handles > Username Linked To Accounts On Lens V2 Profiles were also linked to Handles we will automatically apply this link if exists. ## Profile Managers > Account Managers On Lens V2 we had Profile managers which could do stuff onbehalf of the Profile, in Lens V3 we have Account Managers which can control aspects on the Account. We also have Lens API Profile managers who enabled signless for the user. ### The Profile manager is owned by Lens API In this case we will generate a new Lens API dispatcher so you can still do signless on Lens V3. With Lens V3 dispatchers are now 1 of 1 this means each dispatcher is not shared with anyone else but you. ### The Profile manager is an EOA We will assign this EOA as Account manager ### The Profile manager is a Contract or Safe If the current Profile manager is a contract or a safe we will not assign it and will need to be added again. ## Blocked Profiles > Blocked Accounts Any Profiles you blocked on Lens V2 will be applied to Lens V3. ## Follow connections On Lens V3 we now have multiple graphs so we will migrate all the follows on Lens V2 and apply them on the global graph automatically. ## Profile Follow Modules > Follow Rules On Lens V2 you could set paid to follow if that is the case we will enable that follow rule on your account but it will be applied as GHO and be the exchange rate at the time of migration. ## Publications > Posts On Lens V3 we now have multiple feeds so we will migrate all the publications to posts on the global feed. ## Post/Account Metadata Storage We will honour the metadata it is stored on and advise people to look at the storage nodes for future uploads. ## Publication Actions > Post Actions We will not auto set any Post Actions for the Account, the Lens V3 supports editing Posts so if you want to enable that action on Lens V3 you can edit and set it up. This includes collect actions. ## Collects Collects are NFTs which live on within the network itself in this case Polygon so we will not be deploying any collections from Lens V2. ## Centralised Data All centralised data like reactions, reports, recommendations will be migrated. ## ML trainned algos All machine learning modals will be migrated and supported on Lens V3. ## Future of Lens V2 After the launch of Lens Chain and the migration of Lens V2 data onto Lens V3 we will slowly start deprecating Lens V2 protocol infrastructure support for the protocol which we run including The Lens API. Below is a list of actions we will do, timelines of us deprecating this is open, we want to support the apps migrating over and one of the main reasons we have done a dev preview first. ### Gas Sponsorship As the Lens API pays for most the gas costs for the users on behalf of the apps we firstly will bring this limit down slowly to 0. This includes Momoka publications which we will slowly also bring down the amount of limits people can post using it. The slow down of the gas paying will start instantly on mainnet launch. ### Lens API and Indexers The Lens API and indexers powers most of the applications built in Lens in some way, on launch of Lens Chain we will decrease the server sizes for the API which in turn will mean it will be slower then it is now for queries. Over time we will completely turn this off. ### Public Big Query The Lens indexers publishes all the indexed data to public big query to allow anyone to query it. This will be supported until we turn off the Lens API and indexers. ### SNS SNS powers some apps by doing push notifications when events are indexed. This will be supported until we turn off the Lens API and indexers. ### Direct DB access If you have a read replica of the Lens DB this will also be scaled down on launch of Lens Chain and revoked access after a short period of time. We can work with the apps to work out the timescale which makes sense for them. ### General Bug Fixing / Support On launch of the mainnet for Lens Chain with Lens V3 we will not maintain or support bugs and give developer support on the old protocol, we will also not add any new features. The only way we will react is if the bug is a critical and users funds/identities are at risk. ================ File: src/pages/protocol/migration/overview.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Migration Overview This guide provides a high-level overview of the paradigm shifts between Lens v2 and Lens v3. --- ## Data Migration Most of the Lens v2 data was migrated from Polygon to Lens v3 on the Lens Chain Mainnet. The migrated data included: - Profiles were migrated into Accounts - Signless configuration - Profile Managers were migrated into Account Managers - Handles were migrated into Usernames - App IDs defined in metadata were migrated as onchain App Primitives - Follow relationships into the Lens Global Graph - Publications: - All root posts were migrated. - Comments were migrated up to 4 levels deep — comments on a comment on a comment on a comment were included. - Only quotes of root posts were migrated; quotes of comments were excluded. As of **April 3, 2025**, migration data from Lens v2 to Lens v3 has been synchronized up to **Polygon block [69,837,653](https://polygonscan.com/block/69837653)**. ## Exiting Concepts - **Profile → Account** – In Lens v2, Profiles were stored as state within a single smart contract. In Lens v3, each Account is its own smart contract. - **Profile Manager → Account Manager** – In v3, management is built directly into the Account contract itself. - **Handle → Username** – Handles are now called usernames. Users can have multiple usernames across different namespaces. - **Follow Modules → Follow Rules** – Follow modules are now referred to as follow rules. - **Reference Modules → Post Rules** – Similarly, reference modules are now called post rules. - **App ID → Onchain App Primitives** – In v3, apps are fully onchain, unlocking powerful new capabilities. - **Gasless → Sponsorships** – Apps now create sponsorships for their users and decide who receives them. Lens V3 automatically migrated accounts and usernames for users. If a Safe was used, it was deployed on the user's behalf. However, if the account or username was owned by contract on Polygon, it could not be transferred automatically. If this applies to you, please contact us — we can help transfer ownership once verification is complete. ## New Concepts You can explore the full documentation to learn about the new concepts introduced in Lens v3. We recommend starting with the [Overview](../index). ## Deprecated Concepts - **Profile Guardian** – In v3, Accounts have owners in a more standard smart wallet pattern. - **Handle Guardian** – There is no equivalent concept in v3. ================ File: src/pages/protocol/migration/sdk.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # SDK Migration This guide will show you how to migrate from Lens SDK v2 to v3. --- ## React SDK The Lens React SDK is currently in development and will be released soon. ## TypeScript SDK ### Authentication - `client.authentication.authenticate()` -> `anyClient.login(request)` - `client.authentication.generateChallenge()` -> `anyClient.login(request)` - `client.authentication.authenticateWith()` -> `sessionClient.resumeSession()` - `client.authentication.fetch()` -> `currentSession(sessionClient)` - `client.authentication.fetchAll()` -> `fetchAuthenticatedSessions(sessionClient)` - `client.authentication.getAccessToken()` -> `sessionClient.getCredentials()` - `client.authentication.getRefreshToken()` -> `sessionClient.getCredentials()` - `client.authentication.getAuthorizationId()` -> `sessionClient.getCredentials()` - `client.authentication.getIdentityToken()` -> `sessionClient.getAuthenticatedUser()` - `client.authentication.getProfileId()` -> `sessionClient.getAuthenticatedUser()`: address property of the AuthenticatedUser when logged-in as Account Owner or Account Manager. See [Authentication](../authentication) guide. - `client.authentication.getWalletAddress()` -> `sessionClient.getAuthenticatedUser()` - `client.authentication.isAuthenticated()` -> `currentSession(sessionClient)` - `client.authentication.logout()` -> `sessionClient.logout()` - `client.authentication.revoke(args)` -> `revokeAuthentication(sessionClient, request)` - `client.authentication.upgradeCredentials()` -> `sessionClient.switchAccount(request)` - `client.authentication.verify()` -> `sessionClient.getAuthenticatedUser()` ### Explore - `client.explore.profiles()` -> **Coming Soon** - `client.explore.publications()` -> **Coming Soon** ### Feed - `client.feed.fetch(args)` -> `fetchTimeline(anyClient, request)` - `client.feed.highlights(args)` -> `fetchTimelineHighlights(anyClient, request)` ### Frames Coming soon. ### Handle Handles are renamed in Lens v3 to `username`. - `client.handle.resolveAddress(args)` -> **Coming soon** ### Invites No more invites in Lens v3, the protocol is now permissionless. ### Modules The modules functionality has been reorganized into rules and actions. - `client.modules.approvedAllowanceAmount(...)` -> **Deprecated** - `client.modules.fetchCurrencies()` -> **Deprecated** - `client.modules.fetchMetadata(...)` -> **Deprecated** - `client.modules.supportedFollowModules()` -> **Deprecated** - `client.modules.supportedOpenActionCollectModules()` -> **Deprecated** - `client.modules.supportedOpenActionModules()` -> **Deprecated** - `client.modules.supportedReferenceModules()` -> **Deprecated** ### Momoka In Lens v3 ALL posts are on-chain, so we will no longer support Momoka publications. ### NFTs This is no longer supported in Lens v3. ### Notifications - `client.notifications.fetch()` -> `fetchNotifications(sessionClient)` ### Profile - `client.profile.actionHistory()` -> **Deprecated** - `client.profile.addInterests(args)` -> **Coming soon** - `client.profile.createBlockProfilesTypedData(args)` -> **Deprecated** - `client.profile.createChangeProfileManagersTypedData(args)` -> **Deprecated** - `client.profile.createFollowTypedData(args)` -> **Deprecated** - `client.profile.createLinkHandleTypedData(args)` -> **Deprecated** - `client.profile.createSetFollowModuleTypedData(args)` -> **Deprecated** - `client.profile.createSetProfileMetadataTypedData(args)` -> **Deprecated** - `client.profile.createUnblockProfilesTypedData(args)` -> **Deprecated** - `client.profile.createUnfollowTypedData(args)` -> **Deprecated** - `client.profile.createUnlinkHandleTypedData(args)` -> **Deprecated** - `client.profile.dismissRecommended(args)` -> **Coming soon** - `client.profile.fetch(args)` -> `fetchAccount(anyClient, request)` - `client.profile.fetchAll(args)` -> `fetchAccounts(anyClient, request)` - `client.profile.fetchDefault()` -> `fetchAccount(anyClient, request)` - `client.profile.follow(args)` -> `follow(sessionClient, request)` - `client.profile.followStatusBulk(args)` -> `fetchFollowStatus(anyClient, request)` - `client.profile.followers(args)` -> `fetchFollowers(anyClient, request)` - `client.profile.following(args)` -> `fetchFollowing(anyClient, request)` - `client.profile.unfollow(args)` -> `unfollow(sessionClient, request)` - `client.profile.managers(args)` -> `fetchAccountManagers(sessionClient, request)` - `client.profile.mutualFollowers(args)` -> `fetchFollowersYouKnow(anyClient, request)` - `client.profile.recommend(args)` -> `recommendAccount(sessionClient, request)` - `client.profile.unrecommend(args)` -> `undoRecommendAccount(sessionClient, request)` - `client.profile.recommendations(args)` -> **Coming soon** - `client.profile.removeInterests(args)` -> **Coming soon** - `client.profile.report(args)` -> `reportAccount(sessionClient, request)` - `client.profile.setDefault()` -> **Deprecated** - `client.profile.setFollowModule(args)` -> **Coming soon** - `client.profile.setProfileMetadata(args)` -> `setAccountMetadata(sessionClient, request)` - `client.profile.block(args)` -> `blockAccount(request)` - `client.profile.unblock(args)` -> `unblockAccount(sessionClient, request)` - `client.profile.linkHandle(args)` -> `assignUsernameToAccount(sessionClient, request)` - `client.profile.unlinkHandle(args)` -> `unassignUsernameFromAccount(sessionClient, request)` - `client.profile.whoActedOnPublication(args)` -> `fetchWhoActedOnPost(anyClient, request)` - `client.profile.whoHaveBeenBlocked()` -> `fetchBlockedAccounts(sessionClient, request)` ### Publication - `client.publication.commentOnMomoka(args)` -> **Deprecated** - `client.publication.createLegacyCollectTypedData(args)` -> **Deprecated** - `client.publication.createMomokaCommentTypedData(args)` -> **Deprecated** - `client.publication.createMomokaMirrorTypedData(args)` -> **Deprecated** - `client.publication.createMomokaPostTypedData(args)` -> **Deprecated** - `client.publication.createMomokaQuoteTypedData(args)` -> **Deprecated** - `client.publication.createOnchainCommentTypedData(args)` -> **Deprecated** - `client.publication.createOnchainMirrorTypedData(args)` -> **Deprecated** - `client.publication.createOnchainPostTypedData(args)` -> **Deprecated** - `client.publication.createOnchainQuoteTypedData(args)` -> **Deprecated** - `client.publication.fetch(args)` -> `fetchPost(anyClient, request)` - `client.publication.fetchAll(args)` -> `fetchPosts(anyClient, request)` - `client.publication.legacyCollect(args)` -> **Deprecated** - `client.publication.mirrorOnMomoka(args)` -> **Deprecated** - `client.publication.mirrorOnchain(args)` -> `repost(sessionClient, request)` - `client.publication.postOnMomoka(args)` -> **Deprecated** - `client.publication.predictNextOnChainPublicationId(args)` -> **Deprecated** - `client.publication.quoteOnMomoka(args)` -> **Deprecated** - `client.publication.commentOnchain(args)` -> `post(sessionClient, request)` - `client.publication.postOnchain(args)` -> `post(sessionClient, request)` - `client.publication.quoteOnchain(args)` -> `post(sessionClient, request)` - `client.publication.refreshMetadata(args)` -> `editPost(sessionClient, request)` - `client.publication.report(args)` -> `reportPost(sessionClient, request)` - `client.publication.tags(args)` -> **Coming soon** - `client.publication.hide(args)` -> Now you can delete a post by calling `deletePost(sessionClient, request)` - `client.publication.hideComment(args)` -> `hideReply(sessionClient, request)` - `client.publication.unhideComment(args)` -> `unhideReply(sessionClient, request)` - `client.publication.validateMetadata(args)` -> **Deprecated** #### Actions - `client.publication.actions.actOn(args)` -> **Coming soon** - `client.publication.actions.createActOnTypedData(args)` -> **Deprecated** #### Bookmarks - `client.publication.bookmarks.add(args)` -> `bookmarkPost(sessionClient, request)` - `client.publication.bookmarks.remove(args)` -> `undoBookmarkPost(sessionClient, request)` - `client.publication.bookmarks.fetch(args)` -> `fetchPostBookmarks(sessionClient, request)` #### NotInterested - `client.publication.notInterested.add(args)` -> **Coming soon** - `client.publication.notInterested.undo(args)` -> **Coming soon** #### Reactions - `client.publication.reactions.add(args)` -> `addReaction(sessionClient, request)` - `client.publication.reactions.remove(args)` -> `undoReaction(sessionClient, request)` - `client.publication.reactions.fetch(args)` -> `fetchPostReactions(sessionClient, request)` ### Revenue This is no longer supported in Lens v3. ### Search - `client.search.profiles(args)` -> `fetchAccounts(sessionClient, request)` - `client.search.publications(args)` -> `fetchPosts(sessionClient, request)` ### Transaction - `client.transaction.broadcastOnMomoka(args)` -> **Deprecated** - `client.transaction.broadcastOnchain(args)` -> **Deprecated** - `client.transaction.generateLensAPIRelayAddress()` -> **Deprecated** - `client.transaction.relayQueues()` -> **Deprecated** - `client.transaction.status(args)` -> `transactionStatus(anyClient, request)` - `client.transaction.txIdToTxHash(args)` -> **Deprecated** - `client.transaction.waitUntilComplete(args)` -> `sessionClient.waitForTransaction(request)` ### Wallet - `client.wallet.claimProfile(args)` -> **Deprecated** - `client.wallet.claimableProfiles(args)` -> **Deprecated** - `client.wallet.createProfile(args)` -> `createAccountWithUsername(sessionClient, request)` - `client.wallet.createProfileWithHandle(args)` -> `createAccountWithUsername(sessionClient, request)` - `client.wallet.hideManagedProfile(args)` -> `hideManagedAccount(sessionClient, request)` - `client.wallet.lastLoggedInProfile(args)` -> `lastLoggedInAccount(sessionClient, request)` - `client.wallet.ownedHandles(args)` -> `fetchUsernames(anyClient, request)`: ```ts const result = await fetchUsernames(client, { filter: { owned: evmAddress("0x1234…") }, }); ``` - `client.wallet.profilesManaged(args)` -> `fetchAccountsAvailable(sessionClient, request)` - `client.wallet.rateLimits(args)` -> **Coming soon** - `client.wallet.sigNonces(args)` -> **Deprecated** - `client.wallet.unhideManagedProfile(args)` -> `unhideManagedAccount(sessionClient, request)` ================ File: src/pages/protocol/resources/contracts.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; export default ({ children }) => {children}; # Lens Contracts This page list useful resources for interacting with the Lens Protocol smart contracts. --- ## Deployed Contracts The Lens protocol smart contracts are deployed at the following addresses on the corresponding networks. ### Lens Mainnet | **Factories** | **Address** | | ---------------------------------------------------------------------------------------------------- | -------------------------------------------- | | [AccessControlFactory](https://explorer.lens.xyz/address/0x0d028419c270C2d366929f459418a4905D1B778F) | `0x0d028419c270C2d366929f459418a4905D1B778F` | | [AccountFactory](https://explorer.lens.xyz/address/0x26C7fd63B06deb4F9E4B5955D540767b9Ac7bbaa) | `0x26C7fd63B06deb4F9E4B5955D540767b9Ac7bbaa` | | [AppFactory](https://explorer.lens.xyz/address/0xB3b7502C47E16a1E3c6d660b73006f45Ec327B0B) | `0xB3b7502C47E16a1E3c6d660b73006f45Ec327B0B` | | [FeedFactory](https://explorer.lens.xyz/address/0x591c6e036a6bC92C6bF0d1dB991D06E74C2B9a6A) | `0x591c6e036a6bC92C6bF0d1dB991D06E74C2B9a6A` | | [GraphFactory](https://explorer.lens.xyz/address/0x837E95c3A69Cd6efa3eCDE87A3a07801AAB25Ba0) | `0x837E95c3A69Cd6efa3eCDE87A3a07801AAB25Ba0` | | [GroupFactory](https://explorer.lens.xyz/address/0x9810C41e805164f30b58395b2Af976B3229b0CE6) | `0x9810C41e805164f30b58395b2Af976B3229b0CE6` | | [NamespaceFactory](https://explorer.lens.xyz/address/0x3155ccbeefbA266a4B6060fB1F9d4b8591d1De3F) | `0x3155ccbeefbA266a4B6060fB1F9d4b8591d1De3F` | | [LensFactory](https://explorer.lens.xyz/address/0x1fa75D26819Ac733bf7B1C1B36C3F8aEF32d2Cc0) | `0x1fa75D26819Ac733bf7B1C1B36C3F8aEF32d2Cc0` | | **Global Instances** | **Address** | | ---------------------------------------------------------------------------------------------- | -------------------------------------------- | | [Global Feed](https://explorer.lens.xyz/address/0xcB5E109FFC0E15565082d78E68dDDf2573703580) | `0xcB5E109FFC0E15565082d78E68dDDf2573703580` | | [Global Graph](https://explorer.lens.xyz/address/0x433025d9718302E7B2e1853D712d96F00764513F) | `0x433025d9718302E7B2e1853D712d96F00764513F` | | [Lens Namespace](https://explorer.lens.xyz/address/0x1aA55B9042f08f45825dC4b651B64c9F98Af4615) | `0x1aA55B9042f08f45825dC4b651B64c9F98Af4615` | | [Test App](https://explorer.lens.xyz/address/0x8A5Cc31180c37078e1EbA2A23c861Acf351a97cE) | `0x8A5Cc31180c37078e1EbA2A23c861Acf351a97cE` | | **Auxiliary** | **Address** | | ----------------------------------------------------------------------------------------- | -------------------------------------------- | | [ActionHub](https://explorer.lens.xyz/address/0xc6d57ee750ef2ee017a9e985a0c4198bed16a802) | `0xc6d57ee750ef2ee017a9e985a0c4198bed16a802` | | **Actions** | **Address** | | ---------------------------------------------------------------------------------------------------- | -------------------------------------------- | | [TippingAccountAction](https://explorer.lens.xyz/address/0x20170f1e53851df4d9ea236a28399493c5b152c0) | `0x20170f1e53851df4d9ea236a28399493c5b152c0` | | [TippingPostAction](https://explorer.lens.xyz/address/0x4984ec4ffd17e64c8f91691d829bd5aea287e47b) | `0x4984ec4ffd17e64c8f91691d829bd5aea287e47b` | | [SimpleCollectAction](https://explorer.lens.xyz/address/0x1cee1cd464c4e44e80acdb0b0e33f88849070f6e) | `0x1cee1cd464c4e44e80acdb0b0e33f88849070f6e` | | **Graph Rules** | Address | | --------------------------------------------------------------------------------------------------- | -------------------------------------------- | | [GroupGatedGraphRule](https://explorer.lens.xyz/address/0x754839c5917a063eb923e83f1194e6737bbb451c) | `0x754839c5917a063eb923e83f1194e6737bbb451c` | | [TokenGatedGraphRule](https://explorer.lens.xyz/address/0x24779f9c251cc5c2ac0ae5c9f274666224e78035) | `0x24779f9c251cc5c2ac0ae5c9f274666224e78035` | | **Group Rules** | Address | | ----------------------------------------------------------------------------------------------------------- | -------------------------------------------- | | [BanMemberGroupRule](https://explorer.lens.xyz/address/0xe12543e5f917ada5aef92b26bc08e1925ec9f53f) | `0xe12543e5f917ada5aef92b26bc08e1925ec9f53f` | | [MembershipApprovalGroupRule](https://explorer.lens.xyz/address/0x353064b2ee992483398dab32267e1ad597e502b9) | `0x353064b2ee992483398dab32267e1ad597e502b9` | | [SimplePaymentGroupRule](https://explorer.lens.xyz/address/0x6d2251d69fba6d7e761c72d55cf478d741cb4ac1) | `0x6d2251d69fba6d7e761c72d55cf478d741cb4ac1` | | [TokenGatedGroupRule](https://explorer.lens.xyz/address/0x0740653858863e8f4f0c734553c2bef0dc54bfa9) | `0x0740653858863e8f4f0c734553c2bef0dc54bfa9` | | **Feed Rules** | Address | | ----------------------------------------------------------------------------------------------------- | -------------------------------------------- | | [GroupGatedFeedRule](https://explorer.lens.xyz/address/0x40a2a352583b266097234f1260b5aafb7b129047) | `0x40a2a352583b266097234f1260b5aafb7b129047` | | [SimplePaymentFeedRule](https://explorer.lens.xyz/address/0xd79dfb3f8290c0da1899b91c3bbfe9ab56198004) | `0xd79dfb3f8290c0da1899b91c3bbfe9ab56198004` | | [TokenGatedFeedRule](https://explorer.lens.xyz/address/0xe320d45b21243771dc5a47909db2389abab81d5b) | `0xe320d45b21243771dc5a47909db2389abab81d5b` | | **Namespace Rules** | Address | | ------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | | [UsernameSimpleCharsetNamespaceRule](https://explorer.lens.xyz/address/0x5dbe2054903512ff26e336c0cbded6e0ddbeac4f) | `0x5dbe2054903512ff26e336c0cbded6e0ddbeac4f` | | [UsernameLengthNamespaceRule](https://explorer.lens.xyz/address/0xb541055222c87ee86a72558e8b582a9c0158a0d8) | `0xb541055222c87ee86a72558e8b582a9c0158a0d8` | | [UsernameReservedNamespaceRule](https://explorer.lens.xyz/address/0x0e8b9960f2a891a561f2d52f0cd98cca19cdf8c9) | `0x0e8b9960f2a891a561f2d52f0cd98cca19cdf8c9` | | [TokenGatedNamespaceRule](https://explorer.lens.xyz/address/0xd108e4215963f9cb13f47a4b08110d0ff51d52d8) | `0xd108e4215963f9cb13f47a4b08110d0ff51d52d8` | | [UsernamePricePerLengthNamespaceRule](https://explorer.lens.xyz/address/0xad917a20bca258020ff81590f62ff05366ebb180) | `0xad917a20bca258020ff81590f62ff05366ebb180` | | **Follow Rules** | Address | | ------------------------------------------------------------------------------------------------------- | -------------------------------------------- | | [SimplePaymentFollowRule](https://explorer.lens.xyz/address/0x10e044f026bd51f855a10f2277e35ed4c896db7e) | `0x10e044f026bd51f855a10f2277e35ed4c896db7e` | | [TokenGatedFollowRule](https://explorer.lens.xyz/address/0x8b39e5e2b7a4ce8fcd8f4601ca1a43486a9d7ca4) | `0x8b39e5e2b7a4ce8fcd8f4601ca1a43486a9d7ca4` | | **Post Rules** | Address | | ----------------------------------------------------------------------------------------------------- | -------------------------------------------- | | [FollowersOnlyPostRule](https://explorer.lens.xyz/address/0x4f573ed906cf23cb43f86ad461d10e43e29802ce) | `0x4f573ed906cf23cb43f86ad461d10e43e29802ce` | | **Global Rules** | Address | | --------------------------------------------------------------------------------------------------- | -------------------------------------------- | | [AccountBlockingRule](https://explorer.lens.xyz/address/0x3b766408f14141f4b567681a1c29cfb58d1c1574) | `0x3b766408f14141f4b567681a1c29cfb58d1c1574` | ### Lens Testnet | **Factories** | **Address** | | ------------------------------------------------------------------------------------------------------------ | -------------------------------------------- | | [AccessControlFactory](https://explorer.testnet.lens.xyz/address/0x5eb740362F17815Ae67EBcA6420Cbb350f714C3E) | `0x5eb740362F17815Ae67EBcA6420Cbb350f714C3E` | | [AccountFactory](https://explorer.testnet.lens.xyz/address/0xE55C2154d1766a9C6319dBD989C89867b0457358) | `0xE55C2154d1766a9C6319dBD989C89867b0457358` | | [AppFactory](https://explorer.testnet.lens.xyz/address/0xc650f3CcfF7801F5e95a99B99AAbD2f6319d38ed) | `0xc650f3CcfF7801F5e95a99B99AAbD2f6319d38ed` | | [FeedFactory](https://explorer.testnet.lens.xyz/address/0xb8169FB0FaB6a699854fd4fD2457b990988E1372) | `0xb8169FB0FaB6a699854fd4fD2457b990988E1372` | | [GraphFactory](https://explorer.testnet.lens.xyz/address/0x7cbB07bD2E80A27c59Ed707B79024cC5e54dEaF0) | `0x7cbB07bD2E80A27c59Ed707B79024cC5e54dEaF0` | | [GroupFactory](https://explorer.testnet.lens.xyz/address/0xEF51808f8a2399282CDd156E897473b282998a29) | `0xEF51808f8a2399282CDd156E897473b282998a29` | | [NamespaceFactory](https://explorer.testnet.lens.xyz/address/0xb69CBb69041a30216e2fe13E9700b32761b859C3) | `0xb69CBb69041a30216e2fe13E9700b32761b859C3` | | [LensFactory](https://explorer.testnet.lens.xyz/address/0x408BC8704Ce76DDcd00cf3a83Acd24de4101eE2D) | `0x408BC8704Ce76DDcd00cf3a83Acd24de4101eE2D` | | **Global Instances** | **Address** | | ------------------------------------------------------------------------------------------------------ | -------------------------------------------- | | [Global Feed](https://explorer.testnet.lens.xyz/address/0x31232Cb7dE0dce17949ffA58E9E38EEeB367C871) | `0x31232Cb7dE0dce17949ffA58E9E38EEeB367C871` | | [Global Graph](https://explorer.testnet.lens.xyz/address/0x4d97287FF1A0e030cA4604EcDa9be355dd8A8BaC) | `0x4d97287FF1A0e030cA4604EcDa9be355dd8A8BaC` | | [Lens Namespace](https://explorer.testnet.lens.xyz/address/0xFBEdC5C278cc01A843D161d5469202Fe4EDC99E4) | `0xFBEdC5C278cc01A843D161d5469202Fe4EDC99E4` | | [Test App](https://explorer.testnet.lens.xyz/address/0xC75A89145d765c396fd75CbD16380Eb184Bd2ca7) | `0xC75A89145d765c396fd75CbD16380Eb184Bd2ca7` | | **Auxiliary** | **Address** | | ------------------------------------------------------------------------------------------------- | -------------------------------------------- | | [ActionHub](https://explorer.testnet.lens.xyz/address/0x4A92a97Ff3a3604410945ae8CA25df4fBB2fDC11) | `0x4A92a97Ff3a3604410945ae8CA25df4fBB2fDC11` | | **Actions** | **Address** | | ------------------------------------------------------------------------------------------------------------ | -------------------------------------------- | | [TippingAccountAction](https://explorer.testnet.lens.xyz/address/0xda614A06972C70a8d50D494FB678d48cf536f769) | `0xda614A06972C70a8d50D494FB678d48cf536f769` | | [TippingPostAction](https://explorer.testnet.lens.xyz/address/0x34EF0F5e41cB6c7ad9438079c179d70C7567ae00) | `0x34EF0F5e41cB6c7ad9438079c179d70C7567ae00` | | [SimpleCollectAction](https://explorer.testnet.lens.xyz/address/0x17d5B3917Eab14Ab4923DEc597B39EF64863C830) | `0x17d5B3917Eab14Ab4923DEc597B39EF64863C830` | | **Graph Rules** | Address | | ----------------------------------------------------------------------------------------------------------- | -------------------------------------------- | | [GroupGatedGraphRule](https://explorer.testnet.lens.xyz/address/0x2Cb90d67d4396385060F4f18B036176005B21d56) | `0x2Cb90d67d4396385060F4f18B036176005B21d56` | | [TokenGatedGraphRule](https://explorer.testnet.lens.xyz/address/0x2662F99dC985d3dC710D3c13142e2D156874878d) | `0x2662F99dC985d3dC710D3c13142e2D156874878d` | | **Group Rules** | Address | | ------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | | [BanMemberGroupRule](https://explorer.testnet.lens.xyz/address/0xd12E1aD028d550F85F2a8d9130C46dB77A6A0a41) | `0xd12E1aD028d550F85F2a8d9130C46dB77A6A0a41` | | [MembershipApprovalGroupRule](https://explorer.testnet.lens.xyz/address/0x6d467E7f34e87C0D7185FAf692B43eD5792B86f5) | `0x6d467E7f34e87C0D7185FAf692B43eD5792B86f5` | | [SimplePaymentGroupRule](https://explorer.testnet.lens.xyz/address/0xC99b11687d91EC4f6e65EcFa205795101BbaB5B2) | `0xC99b11687d91EC4f6e65EcFa205795101BbaB5B2` | | [TokenGatedGroupRule](https://explorer.testnet.lens.xyz/address/0x3e3a35d2A67583975569c4a19761268AFB958cEF) | `0x3e3a35d2A67583975569c4a19761268AFB958cEF` | | **Feed Rules** | Address | | ------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | | [GroupGatedFeedRule](https://explorer.testnet.lens.xyz/address/0xbDE71d01eC6d6c49b2bcc9067EcA352a17D25A91) | `0xbDE71d01eC6d6c49b2bcc9067EcA352a17D25A91` | | [SimplePaymentFeedRule](https://explorer.testnet.lens.xyz/address/0x55efA60BE4fd711C114B853A5d251b95bdCC4F66) | `0x55efA60BE4fd711C114B853A5d251b95bdCC4F66` | | [TokenGatedFeedRule](https://explorer.testnet.lens.xyz/address/0x54649BfA8Ea33eDD90f93592Fe87627be6C76013) | `0x54649BfA8Ea33eDD90f93592Fe87627be6C76013` | | **Namespace Rules** | Address | | --------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | | [UsernameSimpleCharsetNamespaceRule](https://explorer.testnet.lens.xyz/address/0x1dB51f49DE4D266B2ab7D62656510083e0AACe44) | `0x1dB51f49DE4D266B2ab7D62656510083e0AACe44` | | [UsernameLengthNamespaceRule](https://explorer.testnet.lens.xyz/address/0x0F0Fe596bAfddbd2Eb4037Fc111b9C4aE5192C5C) | `0x0F0Fe596bAfddbd2Eb4037Fc111b9C4aE5192C5C` | | [UsernameReservedNamespaceRule](https://explorer.testnet.lens.xyz/address/0x9a8b0e3344f5ca5f6fc9FcEb8fF543FDeF5eb2b9) | `0x9a8b0e3344f5ca5f6fc9FcEb8fF543FDeF5eb2b9` | | [TokenGatedNamespaceRule](https://explorer.testnet.lens.xyz/address/0x87A69174530aA735768096c5F24a0F559553Dd84) | `0x87A69174530aA735768096c5F24a0F559553Dd84` | | [UsernamePricePerLengthNamespaceRule](https://explorer.testnet.lens.xyz/address/0x4aBdf719Bc6659e91233c62D4d08D6F4229989e8) | `0x4aBdf719Bc6659e91233c62D4d08D6F4229989e8` | | **Follow Rules** | Address | | --------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | | [SimplePaymentFollowRule](https://explorer.testnet.lens.xyz/address/0x7EA84D750E8C2b7D0EB5e8114f54111d78Eeb992) | `0x7EA84D750E8C2b7D0EB5e8114f54111d78Eeb992` | | [TokenGatedFollowRule](https://explorer.testnet.lens.xyz/address/0x51BB76bae8eb8f1B69B8F4c3e310d49423a9aF33) | `0x51BB76bae8eb8f1B69B8F4c3e310d49423a9aF33` | | **Post Rules** | Address | | ------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | | [FollowersOnlyPostRule](https://explorer.testnet.lens.xyz/address/0x8956af058dF5Cb3609Fc10B2ea293764f55F5B0c) | `0x8956af058dF5Cb3609Fc10B2ea293764f55F5B0c` | | **Global Rules** | Address | | ----------------------------------------------------------------------------------------------------------- | -------------------------------------------- | | [AccountBlockingRule](https://explorer.testnet.lens.xyz/address/0xf3de16e99679243E36BB449CADEA247Cf61450e1) | `0xf3de16e99679243E36BB449CADEA247Cf61450e1` | --- ## Lens Contracts You can find the source code for the Lens smart contracts on [GitHub](https://github.com/lens-protocol/lens-v3/tree/latest-testnet). ================ File: src/pages/protocol/sponsorships/fetch.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Fetch Sponsorship This guide will show you how to fetch Sponsorship data in different ways. --- Lens Sponsorship data has a rich structure that includes the following information: - Addresses of the primitive contract - Sponsorship Metadata content - Time of creation - Owner of the Sponsorship - Information about status of the sponsorship (pause or unpause) To illustrate how to fetch sponsorships, we will use the following fragments: ```graphql filename="Sponsorship" fragment Sponsorship on Sponsorship { __typename address isPaused allowsLensAccess createdAt metadata { ...SponsorshipMetadata } limits { __typename global { ...SponsorshipRateLimit } user { ...SponsorshipRateLimit } } owner } ``` ```graphql filename="SponsorshipMetadata" fragment SponsorshipMetadata on SponsorshipMetadata { __typename id name description } ``` ```graphql filename="SponsorshipRateLimit" fragment SponsorshipRateLimit on SponsorshipRateLimit { __typename id limit window } ``` ## Get a Sponsorship Use the `fetchSponsorship` action to fetch a single Sponsorship by address or by transaction hash. ```ts filename="By Address" import { evmAddress } from "@lens-protocol/client"; import { fetchSponsorship } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchSponsorship(client, { address: evmAddress("0x1234…"), }); if (result.isErr()) { return console.error(result.error); } const sponsorship = result.value; ``` ```ts filename="By Tx Hash" import { txHash } from "@lens-protocol/client"; import { fetchSponsorship } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchSponsorship(client, { txHash: txHash("0x1234…"), }); if (result.isErr()) { return console.error(result.error); } const fetchSponsorship = result.value; ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; export const client = PublicClient.create({ environment: mainnet, }); ``` Use the `sponsorship` query to fetch a single Sponsorship by address or by transaction hash. ```graphql filename="Query" query { app( request: { address: "0x1234…" # OR # txHash: "TxHash!" } ) { address isPaused allowsLensAccess createdAt metadata { id name description } limits { __typename global { limit window } user { limit window } } owner } } ``` ```json filename="Response" { "data": { "sponsorship": { "address": "0x1234…", "isPaused": false, "allowsLensAccess": true, "createdAt": "2024-12-22T21:14:53+00:00", "owner": "0x1234…", "metadata": { "id": "0x1234…", "name": "Lens Sponsorship", "description": "Lens Sponsorship Description" }, "limits": { "global": { "limit": 1000, "window": "DAY" }, "user": { "limit": 100, "window": "DAY" } } } } } ``` ## List Sponsorships Use the paginated `fetchSponsorships` action to fetch a list of Sponsorships based on the provided filters. ```ts filename="Managed By" import { evmAddress } from "@lens-protocol/client"; import { fetchSponsorships } from "@lens-protocol/client/actions"; import { client } from "./client"; const posts = await fetchSponsorships(client, { filter: { managedBy: { address: evmAddress("0x1234…"), }, }, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; export const client = PublicClient.create({ environment: mainnet, }); ``` Use the paginated `sponsorships` query to fetch a list of Sponsorships based on the provided filters. ```graphql filename="Query" query { sponsorships( request: { filter: { # optional, filter by the account owner managedBy: { address: "0x1234…" } } # optional, order of the results (default: ALPHABETICAL) orderBy: ALPHABETICAL # other options: LATEST_FIRST, OLDEST_FIRST # optional, number of items per page (default: FIFTY) pageSize: TEN # other option is FIFTY } ) { items { address isPaused allowsLensAccess createdAt metadata { id name description } limits { __typename global { limit window } user { limit window } } owner } pageInfo { prev next } } } ``` ```json filename="Response" { "data": { "apps": { "items": [ { "address": "0x1234…", "isPaused": false, "allowsLensAccess": true, "createdAt": "2024-12-22T21:14:53+00:00", "owner": "0x1234…", "metadata": { "id": "0x1234…", "name": "Lens Sponsorship", "description": "Lens Sponsorship Description" }, "limits": { "global": { "limit": 1000, "window": "DAY" }, "user": { "limit": 100, "window": "DAY" } } } ], "pageInfo": { "prev": null, "next": null } } } } ``` See the [Pagination](../best-practices/pagination) guide for more information on how to handle paginated results. ================ File: src/pages/protocol/sponsorships/funding.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Funding Sponsorships This guide covers how to fund your Lens Sponsorship contract. --- To sponsor transactions for your users, you need to periodically fund your Lens Sponsorship. You can do this by sending native tokens (e.g., _$GRASS_ on Testnet) to the Lens Sponsorship contract address. This can be done either through a wallet or programmatically via code. ```ts filename="Example" import { ethers } from "ethers"; import { wallet } from "./wallet"; const response = await wallet.sendTransaction({ to: "", value: ethers.parseEther("100"), // Amount in native tokens }); const receipt = await response.wait(); // funded ``` ```ts filename="wallet.ts" import { getDefaultProvider, Network, Wallet } from "@lens-chain/sdk/ethers"; const lensProvider = getDefaultProvider(Network.Mainnet); export const wallet = new Wallet( process.env.PRIVATE_KEY as String, lensProvider ); ``` Refer to the Lens Chain [integration guide](../../chain/integrations/viem) for more options on how to integrate with the Lens Chain. ================ File: src/pages/protocol/sponsorships/managing.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Managing Sponsorships This guide covers how to manage your Lens Sponsorship contract. --- ## Rate Limiting Lens Sponsorships allow you to scale your app while protecting it from abuse. Sponsorships can be configured with app-wide as well as per-user rate limits with configurable reset windows. This gives the developer full visibility and control over the usage of their app. ```graphql filename="SponsorshipRateLimits" input SponsorshipRateLimits { """ The global rate limit. """ global: SponsorshipRateLimit """ The user rate limit. """ user: SponsorshipRateLimit } ``` ```graphql filename="SponsorshipRateLimit" input SponsorshipRateLimit { """ The limit time window. """ window: SponsorshipRateLimitWindow! """ The limit value. """ limit: Int! } ``` ```graphql filename="SponsorshipRateLimitWindow" enum SponsorshipRateLimitWindow { HOUR DAY WEEK MONTH } ``` ### Configure Limits You can provide rate limits when deploying the Sponsorship contract as well as update them later. #### Update Limits First, create the transaction request to update the rate limits of a Sponsorship. You MUST be authenticated as [Builder](../authentication) and be either the owner or an admin of the Sponsorship you intend to configure. Use the `updateSponsorshipLimits` action to update the rate limits of the Lens Sponsorship smart contract. ```ts filename="Update Global and User Limits" import { evmAddress, SponsorshipRateLimitWindow } from "@lens-protocol/client"; import { updateSponsorshipLimits } from "@lens-protocol/client/actions"; // … const result = await updateSponsorshipLimits(sessionClient, { sponsorship: evmAddress("0xe2f2a5C287993345a840db3B0845fbc70f5935a5"), rateLimits: { user: { window: SponsorshipRateLimitWindow.Hour, limit: 100, }, global: { window: SponsorshipRateLimitWindow.Day, limit: 1_000_000, }, }, }); ``` ```ts filename="Selective Update" import { evmAddress, SponsorshipRateLimitWindow } from "@lens-protocol/client"; import { updateSponsorshipLimits } from "@lens-protocol/client/actions"; // … const result = await updateSponsorshipLimits(sessionClient, { sponsorship: evmAddress("0xe2f2a5C287993345a840db3B0845fbc70f5935a5"), rateLimits: { user: null, global: { window: SponsorshipRateLimitWindow.Day, limit: 1_000_000, }, }, }); ``` ```ts filename="Remove Limits" import { evmAddress } from "@lens-protocol/client"; import { updateSponsorshipLimits } from "@lens-protocol/client/actions"; // … const result = await updateSponsorshipLimits(sessionClient, { sponsorship: evmAddress("0xe2f2a5C287993345a840db3B0845fbc70f5935a5"), rateLimits: null, }); ``` Use the `updateSponsorshipLimits` mutation to update the rate limits of the Lens Sponsorship smart contract. ```graphql filename="Mutation" mutation { updateSponsorshipLimits( request: { sponsorship: "0xe2f2a5C287993345a840db3B0845fbc70f5935a5" rateLimits: { user: { window: HOUR, limit: 100 } global: { window: DAY, limit: 1000000 } } } ) { ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ```json filename="SponsoredTransactionRequest" { "data": { "updateSponsorshipLimits": { "raw": { "type": "71", "nonce": "0x1" // … } } } } ``` #### Handle Result Then, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,10" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await updateSponsorshipLimits(sessionClient, { sponsorship: evmAddress("0xe2f2a5C287993345a840db3B0845fbc70f5935a5"), rateLimits: { // … }, }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,10" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await updateSponsorshipLimits(sessionClient, { sponsorship: evmAddress("0xe2f2a5C287993345a840db3B0845fbc70f5935a5"), rateLimits: { // … }, }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Then, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. ### Exclusion List To enable certain use-cases such as trusted VIP/users, the rate-limiting feature can be optionally bypassed for given addresses by adding them to an exclusion list. #### Update Exclusion List #### Prepare the Request First, create the transaction request to update the exclusion list of a Sponsorship. You MUST be authenticated as [Builder](../authentication) and be either the owner or an admin of the Sponsorship you intend to configure. Use the `updateSponsorshipExclusionList` action to update the exclusions list of the Lens Sponsorship smart contract. You can add and remove entries as part of the same transaction. ```ts filename="Update Exclusion List" import { evmAddress, SponsorshipRateLimitWindow } from "@lens-protocol/client"; import { updateSponsorshipExclusionList } from "@lens-protocol/client/actions"; // … const result = await updateSponsorshipExclusionList(sessionClient, { sponsorship: evmAddress("0xe2f2a5C287993345a840db3B0845fbc70f5935a5"), toAdd: [ { address: evmAddress("0x1234…"), label: "Bob The Builder", }, ], toRemove: [evmAddress("0x5678…")], }); ``` Use the `updateSponsorshipExclusionList` mutation to update the exclusion list of the Lens Sponsorship smart contract. ```graphql filename="Mutation" mutation { updateSponsorshipExclusionList( request: { sponsorship: "0xe2f2a5C287993345a840db3B0845fbc70f5935a5" toAdd: [{ address: "0x1234…", label: "Bob The Builder" }] toRemove: ["0x5678…"] } ) { ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ```json filename="SponsoredTransactionRequest" { "data": { "updateSponsorshipExclusionList": { "raw": { "type": "71", "nonce": "0x2" // … } } } } ``` #### Handle Result Then, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,13" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await updateSponsorshipExclusionList(sessionClient, { sponsorship: evmAddress("0xe2f2a5C287993345a840db3B0845fbc70f5935a5"), toAdd: [ // … ], toRemove: [ // … ], }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,13" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await updateSponsorshipExclusionList(sessionClient, { sponsorship: evmAddress("0xe2f2a5C287993345a840db3B0845fbc70f5935a5"), toAdd: [ // … ], toRemove: [ // … ], }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Then, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. #### Fetch Exclusion List Use the paginated `fetchSponsorshipLimitExclusions` action to fetch a list of addresses that are excluded from the rate limits. ```ts filename="Example" import { evmAddress } from "@lens-protocol/client"; import { fetchSponsorshipLimitExclusions } from "@lens-protocol/client/actions"; import { client } from "./client"; const posts = await fetchSponsorshipLimitExclusions(client, { filter: { sponsorship evmAddress("0x1234…"), }, }); if (result.isErr()) { return console.error(result.error); } // items: Array<{ sponsorship: EvmAddress, label: string, address: EvmAddress, createdAt: DateTimeTime }> const { items, pageInfo } = result.value; ``` Use the paginated `sponsorshipLimitsExclusions` query to fetch a list of addresses that are excluded from the rate limits. ```graphql filename="Query" query { sponsorshipLimitsExclusions( request: { filter: { sponsorship: "0x1234…" } # optional, order of the results (default: ALPHABETICAL) orderBy: ALPHABETICAL # other options: LATEST_FIRST, OLDEST_FIRST # optional, number of items per page (default: FIFTY) pageSize: TEN # other option is FIFTY } ) { items { sponsorship label address createdAt } pageInfo { prev next } } } ``` ```json filename="Response" { "data": { "apps": { "items": [ { "address": "0x1234…", "sponsorship": "0x1234…", "label": "label", "createdAt": "2024-12-22T21:14:53+00:00" } ], "pageInfo": { "prev": null, "next": null } } } } ``` See the [Pagination](../best-practices/pagination) guide for more information on how to handle paginated results. ## Signers To ensure that sponsored transactions are only used by the intended users, the Sponsorship contract uses a list of authorized signers. These signers are one or more addresses that need to supply their signature to every transaction sent to the Sponsorship contract, indicating that the transaction originates from their app. This is the mechanism behind the `allowLensAccess` flag you encountered when [deploying the Sponsorship contract](./sponsoring-transactions#create-sponsorship-deploy-contract)—it allows the Lens API to sponsor transactions for users while they are logged into your app. ### Update Signers You can provide a list of signers when deploying the Sponsorship contract as well as update them later. #### Configure Signers First, create the transaction request to update the signers of a Sponsorship. You MUST be authenticated as [Builder](../authentication) and be either the owner or an admin of the Sponsorship you intend to configure. Use the `updateSponsorshipSigners` action to update the signers of the Lens Sponsorship smart contract. ```ts filename="Add Signers" import { evmAddress } from "@lens-protocol/client"; import { updateSponsorshipSigners } from "@lens-protocol/client/actions"; // … const result = await updateSponsorshipSigners(sessionClient, { sponsorship: evmAddress("0x1234…"), toAdd: [ { address: evmAddress("0x5678…"), label: "My Backend System", }, ], }); ``` ```ts filename="Remove Signers" import { evmAddress } from "@lens-protocol/client"; import { updateSponsorshipSigners } from "@lens-protocol/client/actions"; // … const result = await updateSponsorshipSigners(sessionClient, { sponsorship: evmAddress("0x1234…"), toRemove: [evmAddress("0x5678…")], }); ``` ```ts filename="Configure Lens Access" import { evmAddress } from "@lens-protocol/client"; import { updateSponsorshipSigners } from "@lens-protocol/client/actions"; // … const result = await updateSponsorshipSigners(sessionClient, { sponsorship: evmAddress("0x1234…"), allowLensAccess: true, }); ``` Use the `updateSponsorshipSigners` mutation to update the signers of the Lens Sponsorship smart contract. ```graphql filename="Mutation" mutation { updateSponsorshipSigners( request: { sponsorship: "0x5678…" toAdd: [{ address: "0x1234…", label: "Bob The Builder" }] toRemove: ["0xdad78…"] allowLensAccess: true } ) { ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` #### Handle Result Then, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await updateSponsorshipSigners(sessionClient, { sponsorship: evmAddress("0x1234…"), toRemove: [evmAddress("0x5678…")], }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,14" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await updateSponsorshipSigners(sessionClient, { sponsorship: evmAddress("0x1234…"), toAdd: [ { address: evmAddress("0x5678…"), label: "My Backend System", }, ], toRemove: [evmAddress("0x5678…")], }).andThen(handleOperationWith(signer)); ``` Then, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. ### Fetch Signers Use the paginated `fetchSponsorshipSigners` action to list the signers of a Sponsorship. ```ts filename="Example" import { evmAddress } from "@lens-protocol/client"; import { fetchSponsorshipSigners } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchSponsorshipSigners(client, { filter: { sponsorship: evmAddress("0x1234…"), }, }); if (result.isErr()) { return console.error(result.error); } // items: Array // const { items, pageInfo } = result.value; ``` Use the `sponsorshipSigners` query to list the signers of a Sponsorship. ```graphql filename="Query" query { sponsorshipSigners( request: { filter: { sponsorship: "0x1234…" } # optional, order of the results (default: ALPHABETICAL) # orderBy: ALPHABETICAL # other options: LATEST_FIRST, OLDEST_FIRST # optional, number of items per page (default: FIFTY) # pageSize: TEN # other option is FIFTY # optional, cursor to start fetching results from (default: null) # cursor: } ) { items { sponsorship label address createdAt } pageInfo { prev next } } } ``` ```json filename="Response" { "data": { "sponsorshipSigners": { "items": [ { "sponsorship": "0x1234…", "label": "label", "address": "0x1234…", "createdAt": "2024-12-22T21:14:53+00:00" } ], "pageInfo": { "prev": null, "next": null } } } } ``` See the [Pagination](../best-practices/pagination) guide for more information on how to handle paginated results. ## Pausing Sponsorships By default, Sponsorships are active when deployed and ready to use. You can pause a Sponsorship to stop it from being used to sponsor transactions. #### Prepare Transaction First, create the transaction request to pause or unpause a Sponsorship. You MUST be authenticated as [Builder](../authentication) and be either the owner or an admin of the Sponsorship you intend to configure. Use the `pauseSponsorship` or `unpauseSponsorship` action to pause or unpause a Sponsorship. ```ts filename="Pause" import { evmAddress } from "@lens-protocol/client"; import { pauseSponsorship } from "@lens-protocol/client/actions"; // … const result = await pauseSponsorship(sessionClient, { sponsorship: evmAddress("0x1234…"), }); ``` ```ts filename="Unpause" import { evmAddress } from "@lens-protocol/client"; import { unpauseSponsorship } from "@lens-protocol/client/actions"; // … const result = await unpauseSponsorship(sessionClient, { sponsorship: evmAddress("0x1234…"), }); ``` Use the `pauseSponsorship` or `unpauseSponsorship` mutation to pause or unpause a Sponsorship. ```graphql filename="Pause Mutation" mutation { pauseSponsorship(request: { sponsorship: "0x1234…" }) { ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ...on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ...on TransactionWillFail { ...TransactionWillFail } } } ``` ```graphql filename="Unpause Mutation" mutation { unpauseSponsorship(request: { sponsorship: "0x1234…" }) { ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ...on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ...on TransactionWillFail { ...TransactionWillFail } } } ``` #### Handle Result Then, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,7" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await pauseSponsorship(sessionClient, { sponsorship: evmAddress("0x1234…"), }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,7" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await pauseSponsorship(sessionClient, { sponsorship: evmAddress("0x1234…"), }).andThen(handleOperationWith(signer)); ``` Then, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. ## Update Sponsorship Metadata To update a Sponsorship Metadata, follow these steps. You MUST be authenticated as [Builder](../authentication) and be either the owner or an admin of the Sponsorship you intend to update. ### Create New Metadata First, create a new Sponsorship Metadata object with the updated details. It's developer responsibility to copy over any existing data that should be retained. {/* Enable when creation is implemented */} {/* The process is similar to the one in the [Create a Group](./create) guide, so we will keep this example brief. */} ```ts filename="Example" import { sponsorship } from "@lens-protocol/metadata"; const metadata = sponsorship({ name: "XYZ", description: "My group description", icon: "lens://BsdfA…", }); ``` ### Upload Metadata Next, upload the Sponsorship Metadata object to a public URI. ```ts filename="Upload Metadata" import { storageClient } from "./storage-client"; // … const { uri } = await storageClient.uploadAsJson(metadata); console.log(uri); // e.g., lens://4f91ca… ``` If [Grove storage](../../storage) was used you can also decide to edit the file at the existing URI. See [Editing Content](../../storage/usage/edit) guide for more information. ### Update Metadata URI Next, update the Sponsorship metadata URI with the new URI. Use the `setSponsorshipMetadata` action to update the Sponsorship Metadata URI. ```ts import { uri } from "@lens-protocol/client"; import { setSponsorshipMetadata } from "@lens-protocol/client/actions"; const result = await setSponsorshipMetadata(sessionClient, { sponsorship: sponsorship.address, metadataUri: uri("lens://4f91ca…"), }); if (result.isErr()) { return console.error(result.error); } ``` Use the `setSponsorshipMetadata` mutation to update the Sponsorship Metadata URI. ```graphql filename="Mutation" mutation { setSponsorshipMetadata( request: { sponsorship: "0x1234…", contentUri: "lens://4f91ca…" } ) { ... on SetSponsorshipMetadataResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` Coming soon ### Handle Result Finally, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await setSponsorshipMetadata(sessionClient, { sponsorship: sponsorship.address, metadataUri: uri("lens://4f91ca…"), }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await setSponsorshipMetadata(sessionClient, { sponsorship: sponsorship.address, metadataUri: uri("lens://4f91ca…"), }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Finally, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. ## Access Control The Sponsorship contract supports two roles: _Owner_ and _Administrators_. Administrators can: - Add and remove authorized signers - Add and remove addresses to the rate limit exclusion list - Update the rate limits - Pause and unpause the Sponsorship The Owner can do everything the administrators can do, plus: - Transfer ownership - Update the list of administrators - Withdraw the funds from the Sponsorship See the [Team Management](../best-practices/team-management) guide for more information on how to manage these roles. ================ File: src/pages/protocol/sponsorships/sponsoring-transactions.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Sponsoring Transactions Lens allows apps to offer a **free** user experience to their end-users through Sponsorships. --- Lens Sponsorship enables developers to provide a fully configurable, gasless experience for their users. It leverages the [ZKsync Paymaster](https://docs.zksync.io/zksync-era/guides/zksync-101/paymaster). After creating and funding a Sponsorship, it can be used to cover gas fees for Lens Protocol transactions, as well as any other transactions on the Lens Chain. ## Create Sponsorship To create a Sponsorship, follow these steps. You MUST be authenticated as a [Builder](../authentication) to create a Sponsorship. ### Create Metadata First, create the Sponsorship Metadata object. Use the `@lens-protocol/metadata` package to construct a valid `SponsorshipMetadata` object: ```ts filename="Example" import { sponsorship } from "@lens-protocol/metadata"; const metadata = sponsorship({ name: "GasPal", }); ``` If you opt to manually create Metadata objects, ensure they conform to the [Sponsorship Metadata JSON Schema](https://json-schemas.lens.dev/sponsorship/1.0.0.json). ```json filename="Example" { "$schema": "https://json-schemas.lens.dev/sponsorship/1.0.0.json", "lens": { "id": "6418053f-89ae-4f9e-8362-8d1b6c0cdafb", "name": "GasPal" } } ``` ### Upload Metadata Next, upload the Sponsorship Metadata object to a public URI. ```ts import { storageClient } from "./storage-client"; const { uri } = await storageClient.uploadAsJson(metadata); console.log(uri); // e.g., lens://4f91ca… ``` This example uses [Grove storage](../storage) to host the Metadata object. See the [Lens Metadata Standards](../best-practices/metadata-standards#host-metadata-objects) guide for more information on hosting Metadata objects. ### Deploy Contract Next, deploy the Lens Sponsorship smart contract. By setting the `allowLensAccess` flag to `true`, you are allowing the Lens API to use the Sponsorship to sponsor Lens transactions for users of your Lens App. Use the `createSponsorship` action to deploy the Lens Sponsorship smart contract. ```ts filename="deploy-sponsorship.ts" import { uri } from "@lens-protocol/client"; import { createSponsorship } from "@lens-protocol/client/actions"; // … const result = await createSponsorship(sessionClient, { metadataUri: uri("lens://4f91…"), // the URI from the previous step allowLensAccess: true, }); ``` Use the `createSponsorship` mutation to deploy the Lens Sponsorship smart contract. ```graphql filename="Mutation" mutation { createSponsorship( request: { metadataUri: "lens://4f91cab87ab5e4f5066f878b72…" allowLensAccess: true # optional list of admins # admins: [EvmAddress!] # optional rate limits # rateLimits: SponsorshipRateLimits # optional exclusion list # exclusionList: [SponsorshipRateLimitsExempt!]! # optional signers # signers: [SponsorshipSignerInput!] } ) { ... on CreateSponsorshipResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ```json filename="CreateSponsorshipResponse" { "data": { "createSponsorship": { "hash": "0x…" } } } ``` ### Handle Result Then, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await createSponsorship(sessionClient, { metadataUri: uri("lens://4f91…"), // the URI from the previous step allowLensAccess: true, }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await createSponsorship(sessionClient, { metadataUri: uri("lens://4f91…"), // the URI from the previous step allowLensAccess: true, }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Then, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. ## Fund Sponsorship To sponsor transactions for your users, you must periodically fund your Lens Sponsorship. This involves sending native GHO (_GRASS_ on Testnet) to the Lens Sponsorship contract address. You can accomplish this either manually through a wallet or programmatically using code. ```ts filename="Example" import { ethers } from "ethers"; import { wallet } from "./wallet"; const response = await wallet.sendTransaction({ to: "", value: ethers.parseEther("100"), // Amount in native tokens }); const receipt = await response.wait(); // funded ``` ```ts filename="wallet.ts" import { getDefaultProvider, Network, Wallet } from "@lens-chain/sdk/ethers"; const lensProvider = getDefaultProvider(Network.Mainnet); export const wallet = new Wallet( process.env.PRIVATE_KEY as String, lensProvider, ); ``` Refer to the Lens Chain [integration guide](../../chain/integrations/viem) for more options on how to integrate with the Lens Chain. ## Sponsor Lens Transactions To start using Lens Sponsorship to sponsor Lens transactions for your users, follow these steps. To simplify the development process on Testnet, if an app Sponsorship contract is not configured, **all transactions** are sponsored by Lens through a global Sponsorship contract. ### Set App Sponsorship First, configure your Lens [App](../apps/index.mdx) to use a Sponsorship you previously created. Use the `setAppSponsorship` action to set the Sponsorship for your App. ```ts filename="set-app-sponsorship.ts" import { evmAddress } from "@lens-protocol/client"; import { setAppSponsorship } from "@lens-protocol/client/actions"; // … const result = await setAppSponsorship(sessionClient, { app: evmAddress("0x1234…"), sponsorship: evmAddress("0x5678…"), }); ``` Use the `setAppSponsorship` mutation to set the Sponsorship for your App. ```graphql filename="Mutation" mutation { setAppSponsorship(request: { app: "0x1234…", sponsorship: "0x5678…" }) { ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ```json filename="SponsoredTransactionRequest" { "data": { "setAppSponsorship": { "raw": { "type": "71", "nonce": "0x42" // … } } } } ``` ### Handle Result Then, handle the result using the adapter for the library of your choice and wait for it to be indexed. ```ts filename="viem" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await setAppSponsorship(sessionClient, { app: evmAddress("0x1234…"), sponsorship: evmAddress("0x5678…"), }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await setAppSponsorship(sessionClient, { app: evmAddress("0x1234…"), sponsorship: evmAddress("0x5678…"), }).andThen(handleOperationWith(signer)); ``` And, ensure the transaction was successful: ```ts filename="Wait for Transaction" highlight="6" const result = await setAppSponsorship(sessionClient, { app: evmAddress("0x1234…"), sponsorship: evmAddress("0x5678…"), }) .andThen(handleOperationWith(signer)) .andThen(sessionClient.waitForTransaction); if (result.isErr()) { return console.error(result.error); } // The transaction was successful ``` Then, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. ### Authentication Workflow Finally, implement the [Authentication Workflow](../apps/authorization-workflows#implementation) to be able to authorize end-users for your sponsorship. If this is not implemented, the Lens API will require end-users to cover transaction fees by returning a [Self-Funded Transaction Request](../best-practices/transaction-lifecycle#tiered-transaction-model-social-operations-self-funded-transaction-request-fallback) for any operation involving a transaction. Since transactions on Testnet fall back to being sponsored by the Lens global Sponsorship if no app Sponsorship is configured, you might not notice any visible difference in the final user experience until deploying to Mainnet, where the full behavior is enforced. ## Sponsor Any Transaction To sponsor any transaction on the Lens Chain using funds from your Sponsorship, follow these steps. ### Sponsorship Signer First, generate a new private key for the address responsible for approving sponsorship requests (i.e., the signer). ```shell filename="Foundry (cast)" cast wallet new Successfully created new keypair. Address: 0x8711d4d6B7536D… Private key: 0x72433488d76ffec7a16b… ``` ```ts filename="viem" #!/usr/bin/env tsx import { privateKeyToAccount, generatePrivateKey } from "viem/accounts"; const privateKey = generatePrivateKey(); const account = privateKeyToAccount(privateKey); console.log("Private Key:", account.privateKey); console.log("Address:", account.address); ``` ```ts filename="ethers" #!/usr/bin/env tsx import { Wallet } from "ethers"; const wallet = Wallet.createRandom(); console.log("Private Key:", wallet.privateKey); console.log("Address:", wallet.address); ``` ### Add Signer Next, add the signer to your Sponsorship. You MUST be authenticated as a [Builder](../authentication) and be the owner or an admin for the Sponsorship you want to add the signer to. Use the `updateSponsorshipSigners` action to add the signer to your Sponsorship. ```ts filename="Add Sponsorship Signer" import { evmAddress } from "@lens-protocol/client"; import { updateSponsorshipSigners } from "@lens-protocol/client/actions"; // … const result = await updateSponsorshipSigners(sessionClient, { sponsorship: evmAddress("0xe2f2a5C287993345a840db3B0845fbc70f5935a5"), toAdd: [ { address: evmAddress("0x8711d4d6B7536D…"), label: "My Backend System", }, ], }); ``` Use the `updatedSponsorshipSigners` mutation to add the signer to your Sponsorship. ```graphql filename="Add Sponsorship Signer" mutation { updatedSponsorshipSigners( request: { sponsorship: "0xe2f2a5C287993345a840db3B0845fbc70f5935a5" toAdd: [{ address: "0x8711d4d6B7536D…", label: "My Backend System" }] } ) { ... on SponsoredTransactionRequest { SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ### Handle Result Then, handle the result using the adapter for the library of your choice and wait for it to be indexed. ```ts filename="viem" highlight="1,14,15" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await updateSponsorshipSigners(sessionClient, { sponsorship: evmAddress("0xe2f2a5C287993345a840db3B0845fbc70f5935a5"), toAdd: [ { address: evmAddress("0x8711d4d6B7536D…"), label: "My Backend System", }, ], }) .andThen(handleOperationWith(walletClient)) .andThen(sessionClient.waitForTransaction); ``` ```ts filename="ethers" highlight="1,14,15" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await updateSponsorshipSigners(sessionClient, { sponsorship: evmAddress("0xe2f2a5C287993345a840db3B0845fbc70f5935a5"), toAdd: [ { address: evmAddress("0x8711d4d6B7536D…"), label: "My Backend System", }, ], }) .andThen(handleOperationWith(signer)) .andThen(sessionClient.waitForTransaction); ``` Then, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide until the transaction is successful. ### Sponsorship Logic Finally, implement the logic to sponsor user's transaction that supports your use case. Here’s an example of a client-side application that sends a request to its backend to generate a sponsored transaction based on specific criteria. ```ts filename="client.ts" import { parseEip712Transaction, sendEip712Transaction } from "viem/zksync"; import { wallet } from "./wallet"; const request = { from: wallet.account.address, to: "0x567890abcdef1234567890abcdef1234567890ab", value: 100, }; const response = await fetch("http://localhost:3000/sponsor", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(request), }); const { serialized } = await response.json(); // send the transaction const transaction = parseEip712Transaction(serialized) as any; const hash = await sendEip712Transaction(wallet, transaction); ``` ```ts filename="wallet.ts" import "viem/window"; import { chains } from "@lens-chain/sdk/viem"; import { createWalletClient, custom } from "viem"; // hoist account const [account] = (await window.ethereum!.request({ method: "eth_requestAccounts", })) as [Address]; export const wallet = createWalletClient({ account, chain: chains.mainnet, transport: custom(window.ethereum!), }); // ensure the user's wallet is connected to the Lens Chain try { await wallet.switchChain({ id: chains.mainnet.id }); } catch { await wallet.addChain({ chain: chains.mainnet }); } ``` The backend server listens for incoming requests, utilizes the `SponsorshipApprovalSigner` to approve the transaction, and sends the approved transaction back to the client. ```ts filename="server.ts" import express from "express"; import type { Address } from "viem"; import { serializeTransaction } from "viem/zksync"; import { approver } from "./approver"; const app = express(); app.use(express.json()); app.post("/sponsor", async (req, res) => { try { const approved = await approver.approveSponsorship({ account: req.body.from as Address, to: req.body.to as Address, value: BigInt(req.body.value), }); res.json({ serialized: serializeTransaction(approved), }); } catch (err) { console.error(err); res.status(500).json({ error: String(err) }); } }); app.listen(3000, () => { console.log("Server listening on http://localhost:3000"); }); ``` ```ts filename="approver.ts" import { evmAddress } from "@lens-protocol/client"; import { SponsorshipApprovalSigner } from "@lens-protocol/client/viem"; import { signer } from "./signer"; export const approver = new SponsorshipApprovalSigner({ signer, sponsorship: evmAddress(process.env.SPONSORSHIP_ADDRESS), }); ``` ```ts filename="signer.ts" import { http, createWalletClient, type Hex } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { chains } from "@lens-chain/sdk/viem"; export const signer = createWalletClient({ account: privateKeyToAccount( process.env.SPONSORSHIP_SIGNER_PRIVATE_KEY as Hex, ), chain: chains.mainnet, transport: http(), }); ``` Here’s an example of a client-side application that sends a request to its backend to generate a sponsored transaction based on specific criteria. ```ts filename="client.ts" import { utils } from "zksync-ethers"; import { wallet } from "./wallet"; const request = { from: wallet.address, to: "0x567890abcdef1234567890abcdef1234567890ab", value: 100, }; const response = await fetch("http://localhost:3000/sponsor", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(request), }); const { serialized } = await response.json(); // send the transaction const transaction = utils.parseEip712(serialized); const response = await wallet.sendTransaction(transaction); await response.wait(); ``` ```ts filename="wallet.ts" import "@lens-chain/sdk/ethers/globals"; import { BrowserProvider, chains, Signer } from "@lens-chain/sdk/ethers"; import { Eip1193Provider } from "ethers"; const lensProvider = getDefaultProvider(Network.Mainnet); const browserProvider = new BrowserProvider(window.ethereum as Eip1193Provider); // ensure the user's wallet is connected to the Lens Chain await browserProvider.send("wallet_addEthereumChain", [chains.mainnet]); await browserProvider.send("wallet_switchEthereumChain", [ { chainId: chains.testnet.chainId }, ]); const network = await browserProvider.getNetwork(); export const wallet = Signer.from( await browserProvider.getSigner(), Number(network.chainId), lensProvider, ); ``` The backend server listens for incoming requests, utilizes the `SponsorshipApprovalSigner` to approve the transaction, and sends the approved transaction back to the client. ```ts filename="server.ts" import express from "express"; import type { Address } from "viem"; import { utils } from "zksync-ethers"; import { approver } from "./approver"; const app = express(); app.use(express.json()); app.post("/sponsor", async (req, res) => { try { const approved = await approver.approveSponsorship({ account: req.body.from, to: req.body.to, value: req.body.value, }); res.json({ serialized: utils.serializeEip712(approved), }); } catch (err) { console.error(err); res.status(500).json({ error: String(err) }); } }); app.listen(3000, () => { console.log("Server listening on http://localhost:3000"); }); ``` ```ts filename="approver.ts" import { evmAddress } from "@lens-protocol/client"; import { Network, Wallet, getDefaultProvider } from '@lens-chain/sdk/ethers'; import { SponsorshipApprovalSigner } from "@lens-protocol/client/ethers"; const signer = new Wallet( process.env.SPONSORSHIP_SIGNER_PRIVATE_KEY as string, getDefaultProvider(Network.Mainnet); ); export const approver = new SponsorshipApprovalSigner({ signer, sponsorship: evmAddress(process.env.SPONSORSHIP_ADDRESS), }); ``` ================ File: src/pages/protocol/tools/balances.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Balances This guide explains how to fetch native and ERC20 token balances. --- You MUST be authenticated as Account Owner or Account Manager to make this request. Use the `fetchBalancesBulk` action to fetch a finite number of balances for the specific address. ```ts filename="ERC-20 Balances" import { evmAddress } from "@lens-protocol/client"; import { fetchBalancesBulk } from "@lens-protocol/client/actions"; const result = await fetchBalancesBulk(sessionClient, { address: evmAddress("0x1234…"), tokens: [evmAddress("0x1234…"), evmAddress("0x5678…")], }); if (result.isErr()) { return console.error(result.error); } // Array const balances = result.value; ``` ```ts filename="Native Token Balance" import { fetchBalancesBulk } from "@lens-protocol/client/actions"; const result = await fetchBalancesBulk(sessionClient, { address: evmAddress("0x1234…"), includeNative: true, tokens: [], }); if (result.isErr()) { return console.error(result.error); } // Array const balances = result.value; ``` Use the `balancesBulk` query to fetch a finite number of balances for the specific address. ```graphql filename="Query" query { balancesBulk( request: { address: "0x1234…", tokens: ["0x1234…", "0x5678…"], includeNative: true } ) { ... on Erc20Amount { __typename asset { name symbol decimals contract { address chainId } } value } ... on NativeAmount { __typename asset { name symbol decimals } value } ... on Erc20BalanceError { __typename reason token } ... on NativeBalanceError { __typename reason } } } ``` ```json filename="Response" { "data": { "balancesBulk": [ { "asset": { "name": "Wrapped Ether", "symbol": "WETH", "decimals": 18, "contract": { "address": "0x1234…", "chainId": 1 } }, "value": "1000000000000000000" }, { "asset": { "name": "GHO", "symbol": "GHO", "decimals": 18 }, "value": "1000000000000000000" } ] } } ``` Coming soon This response differs slightly from others by providing a localized error for each balance that couldn’t be fetched. This enables more graceful error handling, allowing you to display some balances even if others fail. ```ts filename="Example" for (let balance of balances) { switch (balance.__typename) { case "Erc20Amount": console.log(`${balance.value} ${balance.asset.symbol}`); break; case "NativeAmount": console.log(`${balance.value} ${balance.asset.symbol}`); case "Erc20BalanceError": case "NativeBalanceError": console.error(balance.reason); } } ``` ================ File: src/pages/protocol/tools/s3.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # S3 Data Access This guide explains how to access and analyze Lens Protocol data stored in Amazon S3. The data is synchronized from PostgreSQL databases using AWS Database Migration Service (DMS). --- ## Configure AWS CLI 1. Install the AWS CLI by following the [official AWS CLI installation guide](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html#getting-started-install-instructions) 2. Test your access: ```bash aws s3 ls s3://lens-protocol-mainnet-data/ --no-sign-request ``` ```bash aws s3 ls s3://lens-protocol-testnet-data/ --no-sign-request ``` ## Access the Data 1. List available schemas: ```bash aws s3 ls s3://lens-protocol-mainnet-data/ --no-sign-request ``` ```bash aws s3 ls s3://lens-protocol-testnet-data/ --no-sign-request ``` 2. List available tables within a schema: ```bash aws s3 ls s3://lens-protocol-mainnet-data/{schema_name}/ --no-sign-request ``` ```bash aws s3 ls s3://lens-protocol-testnet-data/{schema_name}/ --no-sign-request ``` 3. Download data using the AWS CLI: ```bash aws s3 cp s3://lens-protocol-mainnet-data/schema/table/LOAD00000001.parquet . --no-sign-request ``` ```bash aws s3 cp s3://lens-protocol-testnet-data/schema/table/LOAD00000001.parquet . --no-sign-request ``` 4. Download data using HTTPS: ```bash curl -O https://lens-protocol-mainnet-data.s3.us-east-1.amazonaws.com/schema/table/LOAD00000001.parquet ``` ```bash curl -O https://lens-protocol-testnet-data.s3.us-east-1.amazonaws.com/schema/table/LOAD00000001.parquet ``` ## Data Organization The data in S3 is organized in the following structure: ```text s3://lens-protocol-mainnet-data/ ├── schema/ # e.g., account, post, etc. ├── table/ # e.g., metadata, record, etc. ├── LOAD[0-9,A-F]{8}.parquet # Initial data load └── YYYY/MM/DD/ # CDC changes by date └── YYYYMMDD-HHMMSSXXX.parquet ``` ```text s3://lens-protocol-testnet-data/ ├── schema/ # e.g., account, post, etc. ├── table/ # e.g., metadata, record, etc. ├── LOAD[0-9,A-F]{8}.parquet # Initial data load └── YYYY/MM/DD/ # CDC changes by date └── YYYYMMDD-HHMMSSXXX.parquet ``` ## File Types 1. **Initial Load Files** - Named: `LOAD[0-9,A-F]{8}.parquet` - Contains: Base data snapshot(s) - Format: Apache Parquet - Example: - Table with a single file (e.g., LOAD00000001.parquet) - Table with multiple files (e.g., LOAD00000001.parquet through LOAD0000000F.parquet) 2. **Change Data Capture (CDC) Files** - Named: `YYYYMMDD-HHMMSSXXX.parquet` - Contains: Incremental changes - Operations: - `I`: Insert (new record) - `U`: Update (modified record) - `D`: Delete (removed record) ## Example ### Install Python Dependencies 1. Create a virtual environment (optional but recommended): ```bash python -m venv lens-env source lens-env/bin/activate # On Unix/macOS ``` 2. Install required packages: ```bash pip install pandas>=2.0.0 pyarrow>=14.0.1 ``` Save the following code as `read_lens_data.py`: ```python #!/usr/bin/env python3 import pandas as pd import argparse from datetime import datetime import sys import binascii def format_timestamp(ts): """Convert timestamp to a readable format""" try: return pd.to_datetime(ts).strftime('%Y-%m-%d %H:%M:%S') except: return ts def format_binary(binary_data): """Convert binary data to a readable hex format""" try: if isinstance(binary_data, bytes): # Convert to hex and remove the '0x' prefix return binascii.hexlify(binary_data).decode('utf-8') return binary_data except: return str(binary_data) def read_parquet_file(url): """Read a parquet file from S3 and return as DataFrame""" try: df = pd.read_parquet(url) return df except Exception as e: print(f"Error reading parquet file: {e}") sys.exit(1) def display_data(df): """Display the data in a human-readable format""" # Create a copy of the dataframe for display display_df = df.copy() # Format timestamp columns timestamp_cols = [col for col in df.columns if 'timestamp' in col.lower() or 'time' in col.lower() or 'date' in col.lower()] for col in timestamp_cols: display_df[col] = display_df[col].apply(format_timestamp) # Format binary columns binary_cols = ['post', 'account', 'app'] # Known binary columns for col in binary_cols: if col in display_df.columns: display_df[col] = display_df[col].apply(format_binary) # Display basic information print("\n=== Dataset Information ===") print(f"Number of records: {len(df)}") print(f"Columns: {', '.join(df.columns)}") # Display column types print("\n=== Column Types ===") for col in df.columns: print(f"{col}: {df[col].dtype}") print("\n=== Sample Data (first 5 rows) ===") # Set display options for better readability pd.set_option('display.max_columns', None) pd.set_option('display.width', None) pd.set_option('display.max_colwidth', None) # Display the formatted data print(display_df.head().to_string()) # Display value counts for categorical columns categorical_cols = df.select_dtypes(include=['object', 'category']).columns for col in categorical_cols: if col not in binary_cols and df[col].nunique() < 10: print(f"\n=== {col} Distribution ===") print(df[col].value_counts().to_string()) def save_to_csv(df, output_path): """Save DataFrame to CSV with proper handling of binary data""" # Create a copy for saving save_df = df.copy() # Convert binary columns to hex binary_cols = ['post', 'account', 'app'] for col in binary_cols: if col in save_df.columns: save_df[col] = save_df[col].apply(format_binary) # Save to CSV save_df.to_csv(output_path, index=False) def main(): parser = argparse.ArgumentParser(description='Read Lens Protocol Parquet files') parser.add_argument('url', help='URL of the parquet file') parser.add_argument('--output', '-o', help='Output file path (optional)') args = parser.parse_args() print(f"Reading data from: {args.url}") df = read_parquet_file(args.url) display_data(df) if args.output: save_to_csv(df, args.output) print(f"\nData saved to: {args.output}") if __name__ == "__main__": main() ``` ### Running the Script 1. **Basic Usage** ```bash python3 read_lens_data.py "s3://lens-protocol-testnet-data/post/reaction/LOAD00000001.parquet" ``` 2. **Save to CSV** ```bash python3 read_lens_data.py "s3://lens-protocol-testnet-data/post/reaction/LOAD00000001.parquet" -o reactions.csv ``` ### Output ```text Reading data from: https://lens-protocol-testnet-data.s3.us-east-1.amazonaws.com/post/reaction/LOAD00000001.parquet === Dataset Information === Number of records: 354 Columns: timestamp, post, account, type, action_at, app === Column Types === timestamp: object post: object account: object type: object action_at: datetime64[us, UTC] app: object === Sample Data (first 5 rows) === timestamp post account type action_at app 0 2025-06-26 11:17:46 ab47c67b39b399a62aacdcc2d9b78f3460b627a9d6740564d0a0d1b7c1e3f01b 7479b233fb386ed4bcc889c9df8b522c972b09f2 UPVOTE 2025-04-14 20:52:13.656723+00:00 4abd67c2c42ff2b8003c642d0d0e562a3f900805 1 2025-06-26 11:17:46 33ca34ec9dc9da2b08931d97e092508f01a4e1f695f56e4822ab2491c98fe278 41bf9732b1e83f62d56f834ba090af3d79b21d83 UPVOTE 2025-04-14 22:43:20.331929+00:00 4abd67c2c42ff2b8003c642d0d0e562a3f900805 2 2025-06-26 11:17:46 33ca34ec9dc9da2b08931d97e092508f01a4e1f695f56e4822ab2491c98fe278 b9b0358d9f2461b2852255a07d3736a45300442b UPVOTE 2025-04-14 22:56:46.519339+00:00 4abd67c2c42ff2b8003c642d0d0e562a3f900805 3 2025-06-26 11:17:46 5c8a610bef5ea5576b863e1a48ef4078974d4aeff3bf22a7b1748aaad98b5b38 7479b233fb386ed4bcc889c9df8b522c972b09f2 UPVOTE 2025-04-14 23:11:54.549963+00:00 4abd67c2c42ff2b8003c642d0d0e562a3f900805 4 2025-06-26 11:17:46 204e761b3eb1493470bcdc839a102bc0df57d3a43539565dad449855676906fe 7754f6ffd9a8cbcbc3d59aa54cce76cc0f27902c UPVOTE 2025-04-15 11:42:47.021848+00:00 10651b74b26ac31aafe23615fc23872402086e85 ``` ## Best Practices 1. **Performance Optimization** - Use appropriate data types when reading Parquet files - Implement column filtering to read only needed data - Process data in batches for large datasets 2. **Error Handling** ```python from tenacity import retry, stop_after_attempt, wait_exponential @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10)) def read_parquet_with_retry(url): return pd.read_parquet(url) ``` 3. **Data Processing** - Handle binary data appropriately (convert to hex for readability) - Format timestamps for your timezone - Implement proper error handling for data type conversions ## Troubleshooting 1. **Common Issues** - Binary data handling: Use proper conversion for bytea columns - Timestamp parsing: Handle timezone information correctly - Memory management: Process large files in chunks 2. **Data Validation** - Verify data types match schema definitions - Check for missing or null values - Validate binary data lengths for addresses and hashes ## References - [Using Amazon S3 as a target for AWS DMS](https://docs.aws.amazon.com/dms/latest/userguide/CHAP_Target.S3.html) - [Using PostgreSQL as a source for AWS DMS](https://docs.aws.amazon.com/dms/latest/userguide/CHAP_Source.PostgreSQL.html) ================ File: src/pages/protocol/tutorials/post-an-image.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; import StorageIllustrationOne from "@/components/mdx/components/concepts/illustrations/StorageIllustrationOne"; import StorageIllustrationTwo from "@/components/mdx/components/concepts/illustrations/StorageIllustrationTwo"; export default ({ children }) => {children}; {/* Start of the page content */} # Post an Image on Lens This tutorial will show you how to post an image on Lens leveraging Grove storage. --- You MUST be [authenticated](../../features/authentication) as Account Owner or Account Manager to post content on Lens. ## Create a Form First, start by defining a form that allows users to upload an image file and a text. ```html filename="index.html"
``` With an event listener to handle the form submission. ```ts filename="index.ts" document.getElementById("post-form").addEventListener("submit", onSubmit); async function onSubmit(event: SubmitEvent) { // prevent the full page reload of a typical form submission event.preventDefault(); const input = event.currentTarget.elements["image"]; const textarea = event.currentTarget.elements["content"]; // … } ``` ## Create and Upload the Post Metadata Next, create an instance of the Lens `StorageClient`. ```ts filename="storage.ts" import { StorageClient } from "@lens-chain/storage-client"; export const storageClient = StorageClient.create(); ``` And upload the image and post content altogether as a single folder with the Post Metadata object as index file. ```ts filename="index.ts" import type { Resource } from "@lens-chain/storage-client"; import { image, MediaImageMimeType } from "@lens-protocol/metadata"; import { storageClient } from "./storage"; // … async function onSubmit(event: SubmitEvent) { // … const { folder, files } = await storage.uploadFolder(input.files, { index: (resources: Resource[]) => image({ content: textarea.value, image: { item: resources[0].uri, type: MediaImageMimeType.PNG, }, }), }); // … } ``` As we didn't provide an `acl` option to the `uploadFolder` method, the folder will be immutable. See the [Uploading Content](../../storage/usage/upload) guide for more information on ACL templates. ## Submit On-Chain Import the `post` action from the `@lens-protocol/client` package: ```ts import { post } from "@lens-protocol/client/actions"; ``` and the adapter for the library of your choice: ```ts filename='viem' import { handleOperationWith } from "@lens-protocol/client/viem"; ``` ```ts filename="ethers" // coming soon ``` Then, using the `folder.uri` from the previous step, submit the post on-chain. ```ts filename="post.ts" const result = await post(sessionClient, { contentUri: "", }).andThen(handleOperationWith(wallet)); ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; import { storage } from "./storage"; const const client = PublicClient.create({ environment: mainnet storage, }); ``` The Lens SDK example here leverages a functional approach to chaining operations using the `Result` object. See the [Error Handling](../../features/best-practices/error-handling) guide for more information. Then, use the `post` mutation to create a Lens Post with the folder URI as `contentUri`. ```graphql filename="Post" mutation { post(request: { contentUri: "" }) { ... on PostResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on PostOperationValidationFailed { reason } ... on TransactionWillFail { reason } } } ``` ```json filename="PostResponse" { "data": { "post": { "hash": "0x5e9d8f8a4b8e4b8e4b8e4b8e4b8e4b8e4b8e4b" } } } ``` Coming soon ## Wait for the Transaction to Complete Then, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Use the `sessionClient.waitForTransaction` method to poll the transaction status until it is mined. ```ts highlight="3" const result = await post(sessionClient, { contentUri: "" }) .andThen(handleOperationWith(wallet)); .andThen(sessionClient.waitForTransaction); ``` In this example, we will assume the previous step returned a `PostResponse` object so we will poll the `transactionStatus` query until it returns a `FinishedTransactionStatus` result. ```graphql filename="Query" query { transactionStatus( request: { txHash: "0x5e9d8f8a4b8e4b8e4b8e4b8e4b8e4b8e4b8e4b" } ) { ... on NotIndexedYetStatus { reason txHasMined } ... on PendingTransactionStatus { blockTimestamp } ... on FinishedTransactionStatus { blockTimestamp } ... on FailedTransactionStatus { reason blockTimestamp } } } ``` Coming soon ## Fetch the Post Finally, fetch the newly created post. Use the `fetchPost` action to fetch the post by its transaction hash. ```ts highlight="8" import { fetchPost } from "@lens-protocol/client/actions"; // … const result = await post(sessionClient, { contentUri: "" }) .andThen(handleOperationWith(wallet)); .andThen(sessionClient.waitForTransaction) .andThen((txHash) => fetchPost(sessionClient, { txHash })); if (result.isOk()) { console.log(result.value); // { id: "42", … } } ``` Use the `post` query with the transaction hash to fetch the post. ```graphql filename="Query" query { post(request: { txHash: "0x5e9d8f8a4b8e4b8e4b8e4b8e4b8e4b8e4b8e4b" }) { ... on Post { ...Post } ... on Repost { ...Repost } } } ``` ```json filename="Response" { "data": { "post": { "id": "42", "author": { "address": "0x1234…", "username": "lens/wagmi", "metadata": { "name": "WAGMI", "picture": "https://example.com/wagmi.jpg" } }, "timestamp": "2022-01-01T00:00:00Z", "app": { "metadata": { "name": "Lens", "logo": "https://example.com/lens.jpg" } }, "metadata": { "content": "Hello, World!" }, "root": null, "quoteOf": null, "commentOn": null } } } ``` Coming soon That's it! You have successfully posted an image on Lens.
================ File: src/pages/protocol/usernames/assign.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Assigning Usernames This guide explains how to assign and unassign a username to an Account on Lens. --- ## Assign a Username To assign a Username to the logged-in Account, follow these steps. You MUST be authenticated as Account Owner or Account Manager of the Account you want to assign a Username to. ### Fetch Owned Usernames First, list all usernames owned by the logged-in Account. Use the paginated `fetchUsernames` action to fetch the list of usernames owned by the logged-in Account address. ```ts filename="Get Owned Usernames" import { evmAddress } from "@lens-protocol/client"; import { fetchUsernames } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchUsernames(sessionClient, { filter: { owned: evmAddress("0x1234…") }, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` Use the paginated `usernames` query to fetch the list of usernames owned by the logged-in Account address. ```graphql filename="Query" query { usernames(request: { owner: "0x1234…" }) { items { id value namespace localName linkedTo ownedBy } pageInfo { prev next } } } ``` ```json filename="Response" { "data": { "usernames": { "items": [ { "id": "1234", "value": "lens/bob", "namespace": "0x4567…", "localName": "bob", "linkedTo": "0xFFEE…", "ownedBy": "0x1234…" } ], "pageInfo": { "prev": null, "next": null } } } } ``` Coming soon See the [Pagination](../best-practices/pagination) guide for more information on how to handle paginated results. ### Check Rules Next, inspect the `username.operations.canAssign` field of the desired Username to determine whether the logged-in Account is allowed to assign the given Username. Some username namespaces may have restrictions on who can assign a Username. ```ts filename="Check Rules" switch (username.operations.canAssign.__typename) { case "NamespaceOperationValidationPassed": // Assignment is allowed break; case "NamespaceOperationValidationFailed": // Assignment is not allowed console.log(username.operations.canAssign.reason); break; case "NamespaceOperationValidationUnknown": // Validation outcome is unknown break; } ``` Where: - `NamespaceOperationValidationPassed`: The logged-in Account is allowed to assign the given Username. - `NamespaceOperationValidationFailed`: Assignment is not allowed. The `reason` field explains why, and `unsatisfiedRules` lists the unmet requirements. - `NamespaceOperationValidationUnknown`: The Namespace has one or more _unknown rules_ requiring ad-hoc verification. The `extraChecksRequired` field provides the addresses and configurations of these rules. Treat the `NamespaceOperationValidationUnknown` as _failed_ unless you intend to support the specific rules. See [Namespace Rules](./rules) for more information. ### Assign the Username Next, if allowed, assign the desired Username to the Account. Use the `assignUsernameToAccount` action to assign a Username. ```ts filename="Lens Username" import { assignUsernameToAccount } from "@lens-protocol/client/actions"; const result = await assignUsernameToAccount(sessionClient, { username: { localName: "wagmi", }, }); ``` ```ts filename="Custom Namespace" import { evmAddress } from "@lens-protocol/client"; import { assignUsernameToAccount } from "@lens-protocol/client/actions"; const result = await assignUsernameToAccount(sessionClient, { username: { localName: "wagmi", namespace: evmAddress("0x5678…"), }, }); ``` Use the `assignUsernameToAccount` mutation to assign a Username. ```graphql filename="Lens Username" mutation { assignUsernameToAccount(request: { username: { localName: "bob" } }) { ...AssignUsernameToAccountResult } } ``` ```graphql filename="Custom Namespace" mutation { assignUsernameToAccount( request: { username: { localName: "bob", namespace: "0x5678…" } } ) { ...AssignUsernameToAccountResult } } ``` ```graphql filename="AssignUsernameToAccountResult" fragment AssignUsernameToAccountResult on AssignUsernameToAccountResult { ... on AssignUsernameResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on NamespaceOperationValidationFailed { reason } ... on TransactionWillFail { reason } } ``` ```json filename="AssignUsernameResponse" { "data": { "assignUsernameToAccount": { "hash": "0x…" } } } ``` Coming soon ### Handle Result Finally, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,9" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await assignUsernameToAccount(sessionClient, { username: { localName: "wagmi", }, }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,9" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await assignUsernameToAccount(sessionClient, { username: { localName: "wagmi", }, }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Finally, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Coming soon ## Unassign a Username To unassign a Username from the logged-in Account, follow these steps. You MUST be authenticated as Account Owner or Account Manager of the Account that owns the Username you want to unassign. ### Check Rules First, inspect the `username.operations.canUnassign` field of the desired Username to determine whether the logged-in Account is allowed to unassign the given Username. Some username namespaces may have restrictions on who can unassign a Username. ```ts filename="Check Rules" switch (username.operations.canUnassign.__typename) { case "NamespaceOperationValidationPassed": // Unassignment is allowed break; case "NamespaceOperationValidationFailed": // Unassignment is not allowed console.log(username.operations.canUnassign.reason); break; case "NamespaceOperationValidationUnknown": // Validation outcome is unknown break; } ``` Where: - `NamespaceOperationValidationPassed`: The logged-in Account is allowed to unassign the given Username. - `NamespaceOperationValidationFailed`: Unassignment is not allowed. The `reason` field explains why, and `unsatisfiedRules` lists the unmet requirements. - `NamespaceOperationValidationUnknown`: The Namespace has one or more _unknown rules_ requiring ad-hoc verification. The `extraChecksRequired` field provides the addresses and configurations of these rules. Treat the `NamespaceOperationValidationUnknown` as _failed_ unless you intend to support the specific rules. See [Namespace Rules](./rules) for more information. ### Unassign Current Username Next, if allowed, unassign the Username. Use the `unassignUsernameFromAccount` to unassign a Username. ```ts filename="Lens Username" import { unassignUsernameFromAccount } from "@lens-protocol/client/actions"; const result = await unassignUsernameFromAccount(sessionClient); ``` ```ts filename="Custom Namespace" import { evmAddress } from "@lens-protocol/client"; import { unassignUsernameFromAccount } from "@lens-protocol/client/actions"; const result = await unassignUsernameFromAccount(sessionClient, { username: { localName: "wagmi", namespace: evmAddress("0x5678…"), }, }); ``` Use the `unassignUsernameFromAccount` mutation to unassign a Username. ```graphql filename="Lens Username" mutation { unassignUsernameFromAccount(request: {}) { ...UnassignUsernameToAccountResult } } ``` ```graphql filename="Custom Namespace" mutation { unassignUsernameFromAccount( request: { # the username contract for which the username is to be unassigned namespace: "0xF0F931CA31cb3abC452cC8007ebD555ca3Cd81b6" } ) { ...UnassignUsernameToAccountResult } } ``` ```graphql filename="fragments" fragment UnassignUsernameToAccountResult on UnassignUsernameToAccountResult { ... on UnassignUsernameResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on NamespaceOperationValidationFailed { reason } ... on TransactionWillFail { reason } } ``` ```json filename="UnassignUsernameResponse" { "data": { "assignUsernameToAccount": { "hash": "0x…" } } } ``` Coming soon ### Handle Result Finally, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,6" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await unassignUsernameFromAccount(sessionClient).andThen( handleOperationWith(walletClient) ); ``` ```ts filename="ethers" highlight="1,6" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await unassignUsernameFromAccount(sessionClient).andThen( handleOperationWith(signer) ); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Finally, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Coming soon ================ File: src/pages/protocol/usernames/create.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Create Username This guide explains how to create a new username. --- Username issuance is regulated by [Namespace Rules](./rules) defined on the desired Namespace. If you’re creating a username under the Global Lens Namespace (i.e., `lens/*`), the only restrictions are: - Allowed characters: `a-z`, `0-9`, `-`, and `_` - Minimum length: 5 characters - Must start with a letter or a number The length of a username is limited to a **maximum of 26 characters on any namespace**. To create a new username, follow these steps. You MUST be authenticated as the Account Owner or Account Manager of the Account that you intend to be the initial owner of the new username. ## Verify Availability First, verify if the desired username is available. Use the `canCreateUsername` action as follows: ```ts filename="Global Lens Namespace" import { canCreateUsername } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await canCreateUsername(sessionClient, { localName: "wagmi", }); if (result.isErr()) { return console.error(result.error); } result.value; // CanCreateUsernameResult ``` ```ts filename="Custom Namespace" import { evmAddress } from "@lens-protocol/client"; import { canCreateUsername } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await canCreateUsername(sessionClient, { localName: "wagmi", namespace: evmAddress("0x1234…"), }); if (result.isErr()) { return console.error(result.error); } result.value; // CanCreateUsernameResult ``` Use the `canCreateUsername` query as follows: ```graphql filename="Global Lens Namespace" query { canCreateUsername(request: { localName: "wagmi" }) { ...CanCreateUsernameResult } } ``` ```graphql filename="Custom Namespace" query { canCreateUsername(request: { localName: "wagmi", namespace: "0x1234…" }) { ...CanCreateUsernameResult } } ``` ```graphql filename="CanCreateUsernameResult" fragment CanCreateUsernameResult on CanCreateUsernameResult { __typename ... on NamespaceOperationValidationPassed { passed } ... on NamespaceOperationValidationUnknown { extraChecksRequired { __typename id type address executesOn config { ...AnyKeyValue } } } ... on NamespaceOperationValidationFailed { reason unsatisfiedRules { required { __typename rule reason message config { ...AnyKeyValue } } anyOf { __typename rule reason message config { ...AnyKeyValue } } } } ... on UsernameTaken { reason } } ``` Coming soon The `CanCreateUsernameResult` tells you if the logged-in Account satisfy the Namespace Rules for creating a username, and if the desired username is available. ```ts filename="Check CanCreateUsernameResult" switch (data.__typename) { case "NamespaceOperationValidationPassed": // Creating a username is allowed break; case "NamespaceOperationValidationFailed": // Creating a username is not allowed console.log(data.reason); break; case "NamespaceOperationValidationUnknown": // Validation outcome is unknown break; case "UsernameTaken": // The desired username is not available break; } ``` Where: - `NamespaceOperationValidationPassed`: The logged-in Account can create a username under the desired Namespace. - `NamespaceOperationValidationFailed`: Reposting is not allowed. The `reason` field explains why, and `unsatisfiedRules` lists the unmet requirements. - `NamespaceOperationValidationUnknown`: The Namespace has one or more _unknown rules_ requiring ad-hoc verification. The `extraChecksRequired` field provides the addresses and configurations of these rules. - `UsernameTaken`: The desired username is not available. Treat the `NamespaceOperationValidationUnknown` as _failed_ unless you intend to support the specific rules. See [Namespace Rules](./rules) for more information. ## Create the Username Next, if available, create the username. Use the `createUsername` action to create the desired username. ```ts filename="Global Lens Namespace" import { createUsername } from "@lens-protocol/client/actions"; const result = await createUsername(sessionClient, { username: { localName: "wagmi", }, }); ``` ```ts filename="Custom Namespace" import { evmAddress } from "@lens-protocol/client"; import { createUsername } from "@lens-protocol/client/actions"; const result = await createUsername(sessionClient, { username: { localName: "wagmi", namespace: evmAddress("0x1234…"), }, }); ``` Use the `createUsername` mutation to create the desired username. ```graphql filename="Global Lens Namespace" mutation { createUsername(request: { username: { localName: "wagmi" } }) { ... on CreateUsernameResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on NamespaceOperationValidationFailed { reason } ... on TransactionWillFail { reason } } } ``` ```graphql filename="Custom Namespace" mutation { createUsername( request: { username: { localName: "wagmi", namespace: "0x1234…" } } ) { ... on CreateUsernameResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on NamespaceOperationValidationFailed { reason } ... on TransactionWillFail { reason } } } ``` ```json filename="CreateUsernameResponse" { "data": { "createUsername": { "hash": "0x…" } } } ``` Coming soon ## Handle Result Finally, handle the result using the adapter for the library of your choice and wait for it to be indexed. ```ts filename="viem" highlight="1,10,11" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await createUsername(sessionClient, { username: { localName: "wagmi", }, }) .andThen(handleOperationWith(walletClient)) .andThen(sessionClient.waitForTransaction); ``` ```ts filename="ethers" highlight="1,10,11" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await createUsername(sessionClient, { username: { localName: "wagmi", }, }) .andThen(handleOperationWith(signer)) .andThen(sessionClient.waitForTransaction); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Finally, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Coming soon That's it—you have successfully created a new username. ================ File: src/pages/protocol/usernames/custom-namespaces.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Custom Username Namespaces This guide explains custom Username Namespaces and how to create and manage them. --- As mentioned in the [Username](../../concepts/username) concept page, there are two Namespace groups: - **The Global Namespace**: The familiar `lens/*` namespace. - **Custom Namespace**: App or group-specific namespaces that can govern issuance, monetization, and more by means of [Username Rules](./namespace-rules). ## Create a Custom Namespace To create a custom Namespace, follow these steps. You MUST be authenticated as [Builder](../authentication) to create a custom Namespace. ### Create Namespace Metadata First, construct a Namespace Metadata object with the necessary content. Use the `@lens-protocol/metadata` package to construct a valid `NamespaceMetadata` object: ```ts filename="Example" import { namespace } from "@lens-protocol/metadata"; const metadata = namespace({ description: "A collection of usernames", collection: { name: "Lens Usernames", description: "The official lens/ usernames", }, }); ``` If you opted to manually create Metadata objects, make sure they conform to the [Namespace Metadata JSON Schema](https://json-schemas.lens.dev/namespace/1.0.0.json). ```json filename="Example" { "$schema": "https://json-schemas.lens.dev/namespace/1.0.0.json", "name": "Lens Usernames", "description": "The official lens/ usernames", "lens": { "id": "6418053f-89ae-4f9e-8362-8d1b6c0cdafb", "description": "A collection of usernames" } } ``` Since usernames are [ERC-721](https://eips.ethereum.org/EIPS/eip-721) tokens, you can also specify [EIP-7572](https://eips.ethereum.org/EIPS/eip-7572) contract-level metadata which makes it easier to trade and display usernames in wallets and marketplaces. ### Upload Namespace Metadata Next, upload the Namespace Metadata object to a public URI. ```ts import { storageClient } from "./storage-client"; const { uri } = await storageClient.uploadAsJson(metadata); console.log(uri); // e.g., lens://4f91ca… ``` This example uses [Grove storage](../../storage) to host the Metadata object. See the [Lens Metadata Standards](../best-practices/metadata-standards#host-metadata-objects) guide for more information on hosting Metadata objects. ### Deploy Namespace Contract Next, deploy a Lens Namespace smart contract. Use the `createUsernameNamespace` action to deploy the contract. ```ts filename="Rules Checked Against Account" import { evmAddress, uri, RulesSubject } from "@lens-protocol/client"; import { createUsernameNamespace } from "@lens-protocol/client/actions"; const result = await createUsernameNamespace(sessionClient, { symbol: "FOO", name: "LensUsernames", namespace: "foo", // foo/ metadataUri: uri("lens://4f91ca…"), rules: { required: [ { usernameLengthRule: { maxLength: 10, minLength: 3, }, }, ], }, rulesSubject: RulesSubject.Account, // default }); ``` ```ts filename="Rules Checked Against Signer" import { evmAddress, uri, RulesSubject } from "@lens-protocol/client"; import { createUsernameNamespace } from "@lens-protocol/client/actions"; const result = await createUsernameNamespace(sessionClient, { symbol: "FOO", name: "LensUsernames", namespace: "foo", // foo/ metadataUri: uri("lens://4f91ca…"), rules: { required: [ { usernameLengthRule: { maxLength: 10, minLength: 3, }, }, ], }, rulesSubject: RulesSubject.Signer, }); ``` ```ts filename="With Admins" import { evmAddress, uri } from "@lens-protocol/client"; import { createUsernameNamespace } from "@lens-protocol/client/actions"; const result = await createUsernameNamespace(sessionClient, { admins: [evmAddress("0x5071DeEcD24EBFA6161107e9a875855bF79f7b21")], symbol: "FOO", name: "LensUsernames", namespace: "foo", metadataUri: uri("lens://4f91ca…"), }); ``` Use the `createUsernameNamespace` mutation to deploy the contract. ```graphql filename="Example" mutation { createUsernameNamespace( request: { symbol: "FOO" # namespace NFT collection ERC721 symbol namespace: "foo" # foo/ # optional # metadataUri: "lens://4f91ca…" # name: "LensUsernames" # namespace NFT collection ERC721 name # rules: { # required: [{ usernameLengthRule: { maxLength: 10, minLength: 3 } }] # } # rulesSubject: Signer # optional (default: Account) } ) { ... on CreateNamespaceResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ```json filename="CreateNamespaceResponse" { "data": { "createUsernameNamespace": { "hash": "0x1234567890abcdef1234567890abcdef12345678" } } } ``` Coming soon To learn more about how to use Username Rules, see the [Username Rules](./namespace-rules) guide. ### Handle Result Next, handle the result using the adapter for the library of your choice and wait for it to be indexed. ```ts filename="viem" highlight="1,10,11" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await createUsernameNamespace(sessionClient, { symbol: "FOO", namespace: "foo", metadataUri: uri("lens://4f91ca…"), }) .andThen(handleOperationWith(walletClient)) .andThen(sessionClient.waitForTransaction); ``` ```ts filename="ethers" highlight="1,10,11" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await createUsernameNamespace(sessionClient, { symbol: "FOO", namespace: "foo", metadataUri: uri("lens://4f91ca…"), }) .andThen(handleOperationWith(signer)) .andThen(sessionClient.waitForTransaction); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Next, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Coming soon ### Fetch New Namespace Finally, fetch the newly created Namespace using the `fetchNamespace` action. ```ts filename="viem" highlight="1,10" import { fetchNamespace } from "@lens-protocol/client/actions"; // … const result = await createUsernameNamespace(sessionClient, { symbol: "FOO", namespace: "foo", metadataUri: uri("lens://4f91ca…"), }) .andThen(handleOperationWith(walletClientOrSigner)) .andThen(sessionClient.waitForTransaction) .andThen((txHash) => fetchNamespace(sessionClient, { txHash })); if (result.isErr()) { return console.error(result.error); } // namespace: UsernameNamespace | null const namespace = result.value; ``` Finally, fetch the newly created Namespace using the `namespace` query. ```graphql filename="Query" query { namespace(request: { txHash: "0x1234…" }) { address createdAt owner tokenName tokenSymbol metadata { description } } } ``` ```json filename="Response" { "data": { "namespace": { "address": "0xdeadbeef…", "createdAt": "2021-09-01T00:00:00Z", "owner": "0x1234…", "tokenName": "LensUsernames", "tokenSymbol": "FOO", "metadata": { "description": "My custom namespace description" } } } } ``` That's it—you now know how to create and manage Custom Username Namespaces, allowing you to build app- or group-specific namespaces. ## Fetch a Namespace Use the `fetchNamespace` action to fetch a single Namespace by address or by transaction hash. ```ts filename="By Address" import { evmAddress } from "@lens-protocol/client"; import { fetchNamespace } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchNamespace(client, { namespace: evmAddress("0x1234…"), }); if (result.isErr()) { return console.error(result.error); } const namespace = result.value; ``` ```ts filename="By Tx Hash" import { txHash } from "@lens-protocol/client"; import { fetchNamespace } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchNamespace(client, { txHash: txHash("0x1234…"), }); if (result.isErr()) { return console.error(result.error); } const namespace = result.value; ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; export const client = PublicClient.create({ environment: mainnet, }); ``` Use the `namespace` query to fetch a single Namespace by address or by transaction hash. ```graphql filename="Query" query { namespace( request: { namespace: "0xdeadbeef…" # OR # txHash: TxHash! } ) { address createdAt owner tokenName tokenSymbol metadata { id description } } } ``` ```json filename="Response" { "data": { "namespace": { "address": "0xdeadbeef…", "createdAt": "2021-09-01T00:00:00Z", "owner": "0x1234…", "tokenName": "LensUsernames", "tokenSymbol": "FOO", "metadata": { "id": "6418053f-89ae-4f9e-8362-8d1b6c0cdafb", "description": "My custom namespace description" } } } } ``` Coming soon ## Search Namespaces Use the paginated `fetchNamespaces` action to search for namespaces. ```ts filename="Search By Query" import { fetchNamespaces } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchNamespaces(client, { filter: { searchBy: "name", }, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="Search by Managed" import { evmAddress } from "@lens-protocol/client"; import { fetchNamespaces } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchNamespaces(client, { filter: { managedBy: { includeOwners: true, // optional address: evmAddress("0x1234…"), }, }, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; export const client = PublicClient.create({ environment: mainnet, }); ``` Use the paginated `namespaces` query to search for namespaces. ```graphql filename="Query" query { namespaces( request: { filter: { searchBy: 'name' # OPTIONAL # managedBy: { # includeOwners: true # optional # address: "0x1234…" # } } orderBy: LATEST_FIRST # other options: ALPHABETICAL, OLDEST_FIRST } ) { items { address createdAt owner tokenName tokenSymbol metadata { id description } # other fields such as namespace rules # will be documented in due course } pageInfo { prev next } } } ``` ```json filename="Response" { "data": { "namespaces": { "items": [ { "address": "0xdeadbeef…", "createdAt": "2021-09-01T00:00:00Z", "owner": "0x1234…", "tokenName": "LensUsernames", "tokenSymbol": "FOO", "metadata": { "id": "6418053f-89ae-4f9e-8362-8d1b6c0cdafb", "description": "My custom namespace description" } } ], "pageInfo": { "prev": null, "next": "U29mdHdhcmU=" } } } } ``` Coming soon Continue with the [Pagination](../best-practices/pagination) guide for more information on how to handle paginated results. ## Access Control The Namespace contract supports two roles: _Owner_ and _Administrator_. Administrators can: - Update the Namespace Metadata - Update the Namespace Rules - Update the Namespace Extra Data The Owner can do everything the administrators can do, plus transfer ownership of the Namespace to another address. See the [Team Management](../best-practices/team-management) guide for more information on how to manage these roles. ================ File: src/pages/protocol/usernames/fetch.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Fetch Usernames This guide will help you with fetching Usernames from Lens API. --- ## Fetch a Username Use the `fetchUsername` function to fetch a single username by localName or ID. ```ts filename="By User Name" import { fetchUsername } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchUsername(client, { username: { localName: "alice", // namespace: evmAddress("0x1234…"), - optional for custom namespaces }, }); if (result.isErr()) { return console.error(result.error); } // { ID: string, value: string, linkedTo: evmAddress, owner: evmAddress, ... } const username = result.value; ``` ```ts filename="By Id" import { fetchUsername } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchUsername(client, { ID: "1234…", }); if (result.isErr()) { return console.error(result.error); } // { ID: string, value: string, linkedTo: evmAddress, owner: evmAddress, ... } const username = result.value; ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; export const client = PublicClient.create({ environment: mainnet, }); ``` Use the `username` query to fetch a username by local name and namespace (if different from `lens/*`) or ID. ```graphql filename="Query" query { username( request: { username: { localName: "wagmi" # Optional. Defaults to lens/* namespace. # namespace: EvmAddress } # OR you can use this query also to fetch the username by ID # id: ID } ) { ...Username } } ``` ```graphql filename="Username" fragment Username on Username { id # ID! # The fully qualifieds username value (e.g., lens/wagmi). value # UsernameValue! # The namespace of the username namespace # EvmAddress! # The local name of the username (e.g., bob). localName # String! # The address that the username is linked to, if any. linkedTo # EvmAddress, # The address that owns the username entry. ownedBy # EvmAddress, # The timestamp when the username was created. timestamp # DateTime, } ``` ```json filename="Result" { "data": { "username": { "id": "1234", "value": "lens/wagmi", "namespace": "0x5E647e6197fa5C6aA814E11C3504BE232a3D671a", "localName": "bob", "linkedTo": "0x5E647e6197fa5C6aA814E11C3504BE232a3D671a", "ownedBy": "0x5E647e6197fa5C6aA814E11C3504BE232a3D671a" } } } ``` Coming soon ## List Usernames Use the paginated `fetchUsernames` action to fetch a list of Usernames based on the provided filters. ```ts filename="Search by Local Name" import { fetchUsernames } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchUsernames(client, { filter: { localNameQuery: "tom", // namespace: evmAddress("0x1234…"), - optional for custom namespaces }, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="Filter By Usage" import { evmAddress } from "@lens-protocol/client"; import { fetchUsernames } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchUsernames(client, { filter: { linkedTo: evmAddress("0x1234…"), }, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="Filter By Owner" import { evmAddress } from "@lens-protocol/client"; import { fetchUsernames } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchUsernames(client, { filter: { owner: evmAddress("0x1234…"), }, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; export const client = PublicClient.create({ environment: mainnet, }); ``` Use the paginated `usernames` query to fetch a list of Usernames based on the provided filters. ```graphql filename="Query" query { groups( request: { filter: { # Filter by local name localNameQuery: "tom" # OR address linked to # linkedTo: "0x1234…" # OR address owned by # owner: "0x1234…" } orderBy: LATEST_MINTED # other options: FIRST_MINTED } ) { items { id value localName linkedTo ownedBy timestamp namespace { address createdAt owner metadata { id description } } } pageInfo { prev next } } } ``` ```json filename="Response" { "data": { "groups": { "items": [ { "id": "1234", "value": "lens/wagmi", "namespace": { "address": "0x5E647e6197fa5C6aA814E11C3504BE232a3D671a", "createdAt": "2021-09-01T00:00:00Z", "owner": "0x5E647e6197fa5C6aA814E11C3504BE232a3D671a", "metadata": { "id": "1234", "description": "The official lens/ usernames" } }, "localName": "bob", "linkedTo": "0x5E647e6197fa5C6aA814E11C3504BE232a3D671a", "ownedBy": "0x5E647e6197fa5C6aA814E11C3504BE232a3D671a" } ], "pageInfo": { "prev": null, "next": null } } } } ``` Coming soon See the [Pagination](../best-practices/pagination) guide for more information on how to handle paginated results. ================ File: src/pages/protocol/usernames/namespace-rules.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Namespace Rules This guide explains how to use username Namespace Rules and how to implement custom ones. --- Namespace Rules allow administrators to add requirements or constraints that will be applied when a Username in a given Namespace is created or assigned to an Account. Lens provides three built-in Group rules: - `UsernamePricePerLengthNamespaceRule` - Requires a payment based on Username length. - `TokenGatedNamespaceRule` - Requires an account to hold a certain token to create a Username. - `UsernameLengthNamespaceRule` - Restricts the length of Usernames. For the `UsernamePricePerLengthNamespaceRule`, a **1.5%** Lens treasury fee is deducted from the payment before the remaining amount is transferred to the designated recipient. To keep usernames web-friendly across the ecosystem, the Namespace primitive enforces a **maximum length of 255 characters**. Two additional built-in rules are also applied by default to every new Namespace: - `UsernameReservedNamespaceRule` - This rule allows the Namespace owner or admins to reserve a specific set of usernames. See [Reserved Usernames](./reserved-usernames) for more information. - `UsernameSimpleCharsetNamespaceRule` - This rule limits valid characters to `a-z`, `0-9`, `-`, and `_`, ensuring consistency. Usernames cannot begin with `-` or `_`. ## Using Namespace Rules As part of creating [Custom Namespaces](./custom-namespaces), you can pass a `rules` object that defines the `required` rules and/or an `anyOf` set, where satisfying any one rule allows the Username creation or assignment to succeed. These rules can be built-in or custom. This section presumes you are familiar with the process of [creating a Namespace](./custom-namespaces) on Lens. ### Username Price Per Length Namespace Rule This rule can be applied to a Username namespace to require a payment based on the length of the Username being created. ```ts filename="Native Token" import { bigDecimal, evmAddress } from "@lens-protocol/client"; import { createUsernameNamespace } from "@lens-protocol/client/actions"; const result = await createUsernameNamespace(sessionClient, { symbol: "FOO", namespace: "foo", rules: { required: [ { usernamePricePerLengthRule: { native: bigDecimal('0.5'), // e.g., 0.5 GHO recipient: evmAddress("0x1234…"), costOverrides: [ { amount: bigDecimal('5'), // 5 GHO length: 1, }, { amount: bigDecimal('4'), // 4 GHO length: 2, }, { amount: bigDecimal('3'), // 3 GHO length: 3, }, { amount: bigDecimal('2'), // 2 GHO length: 4, } ] } } ], }); ``` ```ts filename="ERC-20 Token" import { bigDecimal, evmAddress } from "@lens-protocol/client"; import { createUsernameNamespace } from "@lens-protocol/client/actions"; const result = await createUsernameNamespace(sessionClient, { symbol: "FOO", namespace: "foo", rules: { required: [ { usernamePricePerLengthRule: { erc20: { currency: evmAddress("0x5678…"), value: bigDecimal('0.5'), // e.g., 0.5 USDC }, recipient: evmAddress("0x1234…"), costOverrides: [ { amount: bigDecimal('5'), // 5 USDC length: 1, }, { amount: bigDecimal('4'), // 4 USDC length: 2, }, { amount: bigDecimal('3'), // 3 USDC length: 3, }, { amount: bigDecimal('2'), // 2 USDC length: 4, } ] } } ], }); ``` ```graphql filename="Native Token" mutation { createUsernameNamespace( request: { symbol: "FOO" namespace: "foo" rules: { required: [ { usernamePricePerLengthRule: { native: "0.5" # e.g., 0.5 GHO recipient: "0x1234…" costOverrides: [ { amount: "5" # 5 GHO length: 1 } { amount: "4" # 4 GHO length: 2 } { amount: "3" # 3 GHO length: 3 } { amount: "2" # 2 GHO length: 4 } ] } } ] } } ) { ... on CreateNamespaceResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ```graphql filename="ERC-20 Token" mutation { createUsernameNamespace( request: { symbol: "FOO" namespace: "foo" rules: { required: [ { usernamePricePerLengthRule: { erc20: { currency: "0x5678…" value: "0.5" # e.g., 0.5 USDC } recipient: "0x1234…" costOverrides: [ { amount: "5" # 5 USDC length: 1 } { amount: "4" # 4 USDC length: 2 } { amount: "3" # 3 USDC length: 3 } { amount: "2" # 2 USDC length: 4 } ] } } ] } } ) { ... on CreateNamespaceResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` Coming soon ### Token Gated Namespace Rule This rule requires holding a certain balance of a token (fungible or non-fungible) to create a Username. Configuration includes the token address, the token standard (ERC-20, ERC-721, or ERC-1155), and the required token amount. For ERC-1155 tokens, an additional token type ID is required. ```ts filename="ERC-721" import { bigDecimal, evmAddress, } from "@lens-protocol/client"; import { createUsernameNamespace } from "@lens-protocol/client/actions"; const result = await createUsernameNamespace(sessionClient, { symbol: "FOO", namespace: "foo", rules: { required: [ { tokenGatedRule: { token: { currency: evmAddress("0x1234…"), standard: TokenStandard.Erc721, value: bigDecimal("1"), }, }, } ], }); ``` ```ts filename="ERC-1155" import { bigDecimal, bigIntString, evmAddress, } from "@lens-protocol/client"; import { createUsernameNamespace } from "@lens-protocol/client/actions"; const result = await createUsernameNamespace(sessionClient, { symbol: "FOO", namespace: "foo", rules: { required: [ { tokenGatedRule: { token: { currency: evmAddress("0x1234…"), standard: TokenStandard.Erc1155, value: bigDecimal("100"), tokenId: bigIntString("123"), }, }, } ], }); ``` ```graphql filename="ERC-721" mutation { createUsernameNamespace( request: { symbol: "FOO" namespace: "foo" rules: { required: [ { tokenGatedRule: { token: { standard: ERC721, currency: "0x1234…", value: "1" } } } ] } } ) { ... on CreateNamespaceResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ```graphql filename="ERC-1155" mutation { createUsernameNamespace( request: { symbol: "FOO" namespace: "foo" rules: { required: [ { tokenGatedRule: { token: { standard: ERC1155 currency: "0x1234…" value: "100" tokenId: "123" } } } ] } } ) { ... on CreateNamespaceResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` Coming soon ### Username Length Namespace Rule This rule can restricts the minimum and/or maximum length of Usernames. ```ts filename="UsernameLengthNamespaceRule" import { bigDecimal, evmAddress } from "@lens-protocol/client"; import { createUsernameNamespace } from "@lens-protocol/client/actions"; const result = await createUsernameNamespace(sessionClient, { symbol: "FOO", namespace: "foo", rules: { required: [ { usernameLengthRule: { minLength: 3, maxLength: 10, }, }, ], }, }); ``` ```graphql filename="UsernameLengthNamespaceRule" mutation { createUsernameNamespace( request: { symbol: "FOO" namespace: "foo" rules: { required: [{ usernameLengthRule: { minLength: 3, maxLength: 10 } }] } } ) { ... on CreateNamespaceResponse { hash } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` Coming soon ### Custom Namespace Rule You can also use custom rules by specifying the rule contract address, when it applies, and the configuration parameters as key-value pairs. ```ts filename="Custom Group Rule" import { blockchainData, evmAddress, NamespaceRuleExecuteOn, } from "@lens-protocol/client"; import { createUsernameNamespace } from "@lens-protocol/client/actions"; const result = await createUsernameNamespace(sessionClient, { symbol: "FOO", namespace: "foo", rules: { required: [ { unknownRule: { address: evmAddress("0x1234…"), executeOn: [ NamespaceRuleExecuteOn.Creating, NamespaceRuleExecuteOn.Assigning, ], params: [ { raw: { // 32 bytes key (e.g., keccak(name)) key: blockchainData("0xac5f04…"), // an ABI encoded value data: blockchainData("0x00"), }, }, ], }, }, ], }, }); ``` ```graphql filename="Mutation" mutation { createUsernameNamespace( request: { symbol: "FOO" namespace: "foo" rules: { required: [ { unknownRule: { address: "0x1234…" executeOn: [CREATING, ASSIGNING] params: [{ raw: { key: "0xac5f04…", data: "0x00" } }] } } ] } } ) { ... on CreateNamespaceResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` Coming soon ### Update a Namespace Rules To update a Namespace rules configuration, follow these steps. You MUST be authenticated as [Builder](../authentication) and be either the owner or an admin of the Namespace you intend to configure. #### Identify Current Rules First, inspect the `namespace.rules` field to know the current rules configuration. ```ts filename="NamespaceRules" type NamespaceRules = { required: NamespaceRule; anyOf: NamespaceRule; }; ``` ```ts filename="NamespaceRule" type NamespaceRule = { id: RuleId; type: NamespaceRuleType; address: EvmAddress; executesOn: NamespaceRuleExecuteOn[]; config: AnyKeyValue[]; }; ``` ```ts filename="AnyKeyValue" type AnyKeyValue = | IntKeyValue | IntNullableKeyValue | AddressKeyValue | StringKeyValue | BooleanKeyValue | RawKeyValue | BigDecimalKeyValue | DictionaryKeyValue | ArrayKeyValue; ``` ```ts filename="ArrayKeyValue" type ArrayKeyValue = { __typename: "ArrayKeyValue"; key: string; array: | IntKeyValue | IntNullableKeyValue | AddressKeyValue | StringKeyValue | BooleanKeyValue | RawKeyValue | BigDecimalKeyValue | DictionaryKeyValue; }; ``` ```ts filename="DictionaryKeyValue" type DictionaryKeyValue = { __typename: "DictionaryKeyValue"; key: string; dictionary: | IntKeyValue | IntNullableKeyValue | AddressKeyValue | StringKeyValue | BooleanKeyValue | RawKeyValue | BigDecimalKeyValue; }; ``` ```ts filename="Others" type IntKeyValue = { __typename: "IntKeyValue"; key: string; int: number; }; type IntNullableKeyValue = { __typename: "IntNullableKeyValue"; key: string; optionalInt: number | null; }; type AddressKeyValue = { __typename: "AddressKeyValue"; key: string; address: EvmAddress; }; type StringKeyValue = { __typename: "StringKeyValue"; key: string; string: string; }; type BooleanKeyValue = { __typename: "BooleanKeyValue"; key: string; boolean: boolean; }; type RawKeyValue = { __typename: "RawKeyValue"; key: string; data: BlockchainData; }; type BigDecimalKeyValue = { __typename: "BigDecimalKeyValue"; key: string; bigDecimal: BigDecimal; }; ``` ```graphql filename="NamespaceRules" type NamespaceRules { required: [NamespaceRule!]! anyOf: [NamespaceRule!]! } ``` ```graphql filename="NamespaceRule" type NamespaceRule { id: RuleId! type: NamespaceRuleType! address: EvmAddress! executesOn: [NamespaceRuleExecuteOn!]! config: [AnyKeyValue!]! } ``` ```graphql filename="AnyKeyValue" fragment AnyKeyValue on AnyKeyValue { ... on IntKeyValue { ...IntKeyValue } ... on IntNullableKeyValue { ...IntNullableKeyValue } ... on AddressKeyValue { ...AddressKeyValue } ... on StringKeyValue { ...StringKeyValue } ... on BooleanKeyValue { ...BooleanKeyValue } ... on RawKeyValue { ...RawKeyValue } ... on BigDecimalKeyValue { ...BigDecimalKeyValue } ... on DictionaryKeyValue { ...DictionaryKeyValue } ... on ArrayKeyValue { ...ArrayKeyValue } } ``` ```graphql filename="ArrayKeyValue" fragment ArrayKeyValue on ArrayKeyValue { __typename key array { ... on IntKeyValue { ...IntKeyValue } ... on IntNullableKeyValue { ...IntNullableKeyValue } ... on AddressKeyValue { ...AddressKeyValue } ... on StringKeyValue { ...StringKeyValue } ... on BooleanKeyValue { ...BooleanKeyValue } ... on RawKeyValue { ...RawKeyValue } ... on BigDecimalKeyValue { ...BigDecimalKeyValue } ... on DictionaryKeyValue { ...DictionaryKeyValue } } } ``` ```graphql filename="DictionaryKeyValue" fragment DictionaryKeyValue on DictionaryKeyValue { __typename key dictionary { ... on IntKeyValue { ...IntKeyValue } ... on IntNullableKeyValue { ...IntNullableKeyValue } ... on AddressKeyValue { ...AddressKeyValue } ... on StringKeyValue { ...StringKeyValue } ... on BooleanKeyValue { ...BooleanKeyValue } ... on RawKeyValue { ...RawKeyValue } ... on BigDecimalKeyValue { ...BigDecimalKeyValue } } } ``` ```graphql filename="Others" fragment IntKeyValue on IntKeyValue { __typename key int } fragment IntNullableKeyValue on IntNullableKeyValue { __typename key optionalInt } fragment AddressKeyValue on AddressKeyValue { __typename key address } fragment StringKeyValue on StringKeyValue { __typename key string } fragment BooleanKeyValue on BooleanKeyValue { __typename key boolean } fragment RawKeyValue on RawKeyValue { __typename key data } fragment BigDecimalKeyValue on BigDecimalKeyValue { __typename key bigDecimal } ``` The configuration for the built-in rules with one or more parameters is as follows. | Key | Type | Description | | ---------------------- | --------------------------- | ----------------------------------------------------------- | | `cost` | `CostObject` | Object containing the cost of creating a username. | | `cost.currency` | `EvmAddress` | Address of the ERC-20 token contract. | | `cost.value` | `BigDecimal` | Amount of the currency to pay. | | `recipient` | `EvmAddress` | Address that will receive the payment. | | `costOverrides` | `Array` | Array of cost overrides for different lengths of usernames. | | `costOverrides.amount` | `BigDecimal` | Amount of the currency. | | `costOverrides.length` | `Int` | Length of the username. | | Key | Type | Description | | --------------- | ------------ | ------------------------------------------------------- | | `assetContract` | `EvmAddress` | Address of the token contract. | | `assetName` | `String` | Name of the token. | | `assetSymbol` | `String` | Symbol of the token. | | `amount` | `BigDecimal` | Minimum number of tokens required to create a username. | | Key | Type | Description | | ----------- | ----- | ----------------------------------- | | `minLength` | `Int` | The minimum length of the username. | | `maxLength` | `Int` | The maximum length of the username. | Keep note of the Rule IDs you might want to remove. #### Update the Rules Configuration Next, update the rules configuration of the Namespace as follows. Use the `updateNamespaceRules` action to update the rules configuration of a given namespace. ```ts filename="Add Rules" import { bigDecimal, evmAddress } from "@lens-protocol/client"; import { updateNamespaceRules } from "@lens-protocol/client/actions"; const result = await updateNamespaceRules(sessionClient, { namespace: namespace.address, toAdd: { required: [ { tokenGatedRule: { token: { standard: TokenStandard.Erc20, currency: evmAddress("0x5678…"), value: bigDecimal("1.5"), // Token value in its main unit }, }, }, ], }, }); ``` ```ts filename="Remove Rules" import { updateNamespaceRules } from "@lens-protocol/client/actions"; const result = await updateNamespaceRules(sessionClient, { namespace: namespace.address, toRemove: [namespace.rules.required[0].id], }); ``` Use the `updateNamespaceRules` mutation to update the rules configuration of a given namespace. ```graphql filename="Add Rules" mutation { updateNamespaceRules( request: { namespace: "0x1234…" toAdd: { required: [ { tokenGatedRule: { token: { standard: ERC20, currency: "0x5678…", value: "1.5" } } } ] } } ) { ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ```graphql filename="Remove Rules" mutation { updateNamespaceRules(request: { namespace: "0x1234…", toRemove: ["ej6g…"] }) { ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` Coming soon #### Handle Result Then, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await updateNamespaceRules(sessionClient, { namespace: evmAddress("0x1234…"), // … }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await updateNamespaceRules(sessionClient, { namespace: evmAddress("0x1234…"), // … }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Then, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Coming soon --- ## Building a Namespace Rule Let's illustrate the process with an example. We will build a custom Namespace Rule that requires Usernames to be created only if their length has an specific parity, for example, all usernames must have an even length. To build a custom Namespace Rule, you must implement the following `INamespaceRule` interface: ```solidity filename="INamespaceRule.sol" import {KeyValue} from "contracts/core/types/Types.sol"; interface INamespaceRule { function configure(bytes32 configSalt, KeyValue[] calldata ruleParams) external; function processCreation( bytes32 configSalt, address originalMsgSender, address account, string calldata username, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external; function processRemoval( bytes32 configSalt, address originalMsgSender, string calldata username, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external; function processAssigning( bytes32 configSalt, address originalMsgSender, address account, string calldata username, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external; function processUnassigning( bytes32 configSalt, address originalMsgSender, address account, string calldata username, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external; } ``` Each function of this interface must assume to be invoked by the Namespace contract. In other words, assume the `msg.sender` will be the Namespace contract. A Lens dependency package with all relevant interfaces will be available soon. ### Implement the Configure Function First, implement the `configure` function. This function has the purpose of initializing any required state for the rule to work properly. It receives two parameters, a 32-byte configuration salt (`configSalt`), and an array of custom parameters as key-value pairs (`ruleParams`). The `configSalt` is there to allow the same rule contract to be used many times, with different configurations, for the same Namespace. So, for a given Namespace Rule implementation, the pair (Namespace Address, Configuration Salt) should identify a rule configuration. The `configure` function can be called multiple times by the same Namespace passing the same configuration salt in order to update that rule configuration (i.e. reconfigure it). The `ruleParams` is an array of key-value pairs that can be used to pass any custom configuration parameters to the rule. Each key is `bytes32`, we put the hash of the parameter name there, and each value is `bytes`, we set the ABI-encoded parameter value there. Given that `ruleParams` is an array, this allows the rule to define which parameters are optional and which are required, acting accordingly when any of them are not present. In our example, we only need to decode a boolean parameter, which will indicate if the rule will enforce Usernames to have an even or an odd length. Let's define a storage mapping to store this configuration: ```solidity contract UsernameParityLengthNamespaceRule is INamespaceRule { mapping(address namespace => mapping(bytes32 configSalt => bool mustBeEven)) internal _mustBeEvenLength; } ``` The configuration is stored in the mapping using the Namespace contract address (`msg.sender`) and the configuration salt as keys. With this setup, the same rule can be used by different Namespaces, as well as be used by the same Namespace many times. Now let's code the `configure` function itself, decoding the boolean parameter and storing it in the mapping: ```solidity contract UsernameParityLengthNamespaceRule is INamespaceRule { mapping(address namespace => mapping(bytes32 configSalt => bool mustBeEven)) internal _mustBeEvenLength; function configure(bytes32 configSalt, KeyValue[] calldata ruleParams) external override { bool mustBeEven = true; // We set `true` as default value for (uint256 i = 0; i < ruleParams.length; i++) { if (ruleParams[i].key == keccak256("lens.param.mustBeEven")) { mustBeEven = abi.decode(ruleParams[i].value, (bool)); break; } } _mustBeEvenLength[msg.sender][configSalt] = mustBeEven; } } ``` We treated the `mustBeEven` parameter as optional, defaulting to `true` (even length) when not present. ### Implement the Process Creation function Next, implement the `processCreation` function. This function is invoked by the Namespace contract every time a username is being created, so then our custom logic can be applied to shape under which conditions this operation can succeed. The function receives the configuration salt (`configSalt`), the address that is trying to create the Username (`originalMsgSender`), the `account` who will own the created Username, the `username` being created, an array of key-value pairs with the custom parameters passed to the Namespace (`primitiveParams`), and an array of key-value pairs in case the rule requires additional parameters to work (`ruleParams`). The function must revert if the requirements imposed by the rule are not met. ```solidity contract UsernameParityLengthNamespaceRule is INamespaceRule { mapping(address namespace => mapping(bytes32 configSalt => bool mustBeEven)) internal _mustBeEvenLength; // ... function processCreation( bytes32 configSalt, address originalMsgSender, address account, string calldata username, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external view override { // Retrieve the rule configuration bool mustBeEven = _mustBeEvenLength[msg.sender][configSalt]; // Get the length of the username being created uint256 usernameLength = bytes(username).length; // Check if the length is even (otherwise it is odd) bool isEvenLength = usernameLength % 2 == 0; // Require the parity of the username being created to match // the parity required by the rule require(isEvenLength == mustBeEven); } // ... } ``` ### Implement the Process Removal function Next, implement the `processRemoval` function. This function is invoked by the Namespace contract every time a username is being removed. The function receives the configuration salt (`configSalt`), the address that is trying to remove the Username (`originalMsgSender`), the `username` being removed, an array of key-value pairs with the custom parameters passed to the Namespace (`primitiveParams`), and an array of key-value pairs in case the rule requires additional parameters to work (`ruleParams`). The function must revert if the requirements imposed by the rule are not met. In our example, the parity rule does not apply to removal, so we revert with `NotImplemented`. This is good practice in case the rule is accidentally enabled for this selector. ```solidity contract UsernameParityLengthNamespaceRule is INamespaceRule { // ... function processRemoval( bytes32 configSalt, address originalMsgSender, string calldata username, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external pure override { revert Errors.NotImplemented(); } // ... } ``` ### Implement the Process Assigning function Next, implement the `processAssigning` function. This function is invoked by the Namespace contract every time a username is being assigned to an account. The function receives the configuration salt (`configSalt`), the address that is trying to assign the Username (`originalMsgSender`), the `account` who the username will be assigned to, the `username` being assigned, an array of key-value pairs with the custom parameters passed to the Namespace (`primitiveParams`), and an array of key-value pairs in case the rule requires additional parameters to work (`ruleParams`). The function must revert if the requirements imposed by the rule are not met. Similar to removal, our parity rule does not apply to the assignment operation, so we revert with `NotImplemented`. ```solidity contract UsernameParityLengthNamespaceRule is INamespaceRule { // ... function processAssigning( bytes32 configSalt, address originalMsgSender, address account, string calldata username, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external pure override { revert Errors.NotImplemented(); } // ... } ``` ### Implement the Process Unassigning function Finally, implement the `processUnassigning` function. This function is invoked by the Namespace contract every time a username is being unassigned from an account. The function receives the configuration salt (`configSalt`), the address that is trying to unassign the Username (`originalMsgSender`), the `account` who the username will be unassigned from, the `username` being unassigned, an array of key-value pairs with the custom parameters passed to the Namespace (`primitiveParams`), and an array of key-value pairs in case the rule requires additional parameters to work (`ruleParams`). The function must revert if the requirements imposed by the rule are not met. Again, our parity rule does not apply to the unassigning process, so we revert with `NotImplemented`. ```solidity contract UsernameParityLengthNamespaceRule is INamespaceRule { // ... function processUnassigning( bytes32 configSalt, address originalMsgSender, address account, string calldata username, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external pure override { revert Errors.NotImplemented(); } } ``` Now the `UsernameParityLengthNamespaceRule` is ready to be applied to any Namespace. See the full code below: ```solidity contract UsernameParityLengthNamespaceRule is INamespaceRule { mapping(address => mapping(bytes32 => bool)) internal _mustBeEvenLength; function configure(bytes32 configSalt, KeyValue[] calldata ruleParams) external override { bool mustBeEven = true; // We set `true` as default value for (uint256 i = 0; i < ruleParams.length; i++) { if (ruleParams[i].key == keccak256("lens.param.mustBeEven")) { mustBeEven = abi.decode(ruleParams[i].value, (bool)); break; } } _mustBeEvenLength[msg.sender][configSalt] = mustBeEven; } function processCreation( bytes32 configSalt, address originalMsgSender, address account, string calldata username, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external view override { // Retrieve the rule configuration bool mustBeEven = _mustBeEvenLength[msg.sender][configSalt]; // Get the length of the username being created uint256 usernameLength = bytes(username).length; // Check if the length is even (otherwise it is odd) bool isEvenLength = usernameLength % 2 == 0; // Require the parity of the username being created to match // the parity required by the rule require(isEvenLength == mustBeEven); } function processRemoval( bytes32 configSalt, address originalMsgSender, string calldata username, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external pure override { revert Errors.NotImplemented(); } function processAssigning( bytes32 configSalt, address originalMsgSender, address account, string calldata username, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external pure override { revert Errors.NotImplemented(); } function processUnassigning( bytes32 configSalt, address originalMsgSender, address account, string calldata username, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external pure override { revert Errors.NotImplemented(); } } ``` Stay tuned for API integration of rules and more guides! ================ File: src/pages/protocol/usernames/reserved-usernames.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Reserved Usernames This guide explains how to reserve usernames in your custom Namespace. --- Namespaces configured with the `UsernameReservedNamespaceRule` allow the Namespace owner or admins to reserve a specific set of usernames. This rule is applied by default to every new Namespace create through the Lens API. ## List Reserved Usernames Use the paginated `fetchNamespaceReservedUsernames` action to fetch a list of reserved usernames for a given namespace. ```ts filename="Fetch Reserved Usernames" import { evmAddress } from "@lens-protocol/client"; import { fetchNamespaceReservedUsernames } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchNamespaceReservedUsernames(client, { namespace: evmAddress("0x1234…"), }); if (result.isErr()) { return console.error(result.error); } // items: Array<{ ruleId: RuleId, namespace: EvmAddress, localName: string }> const { items, pageInfo } = result.value; ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; export const client = PublicClient.create({ environment: mainnet, }); ``` Use the paginated `namespaceReservedUsernames` query to fetch a list of reserved usernames for a given namespace. ```graphql filename="Query" query { namespaceReservedUsernames(request: { namespace: "0x1234…" }) { items { ruleId namespace localName } pageInfo { prev next } } } ``` ```json filename="Response" { "data": { "namespaceReservedUsernames": { "items": [ { "ruleId": "ej6g…", "namespace": "0x1234…", "localName": "nike" }, { "ruleId": "ej6g…", "namespace": "0x1234…", "localName": "coca-cola" } ], "pageInfo": { "prev": null, "next": null } } } } ``` Coming soon See the [Pagination](../best-practices/pagination) guide for more information on how to handle paginated results. ## Update Reserved Usernames To update the reserved usernames of a Namespace, you can either release or reserve usernames. You MUST be authenticated as [Builder](../authentication) and be either the owner or an admin of the Namespace you intend to configure reserved usernames for. Use the `updateReservedUsernames` action to update the reserved usernames for a given namespace. ```ts filename="example.ts" import { evmAddress } from "@lens-protocol/client"; import { updateReservedUsernames } from "@lens-protocol/client/actions"; const result = await updateReservedUsernames(sessionClient, { namespace: evmAddress("0x1234…"), toRelease: ["alice", "bob"], toReserve: ["charlie", "dave"], }); ``` And, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,9" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await updateReservedUsernames(sessionClient, { namespace: evmAddress("0x1234…"), toRelease: ["alice", "bob"], toReserve: ["charlie", "dave"], }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,9" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await updateReservedUsernames(sessionClient, { namespace: evmAddress("0x1234…"), toRelease: ["alice", "bob"], toReserve: ["charlie", "dave"], }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Use the `updateReservedUsernames` mutation to update the reserved usernames for a given namespace. ```graphql filename="Mutation" mutation { updateReservedUsernames( request: { namespace: "0x1234…" toRelease: ["alice", "bob"] toReserve: ["charlie", "dave"] } ) { ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` And, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. ================ File: src/pages/protocol/index.mdx ================ export const meta = { showBreadcrumbs: false, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; import OverviewIllustration from "@/components/mdx/components/overview/OverviewIllustration"; import HackathonBanner from "@/components/HackathonBanner"; export default ({ children }) => {children}; {/* Start of the page content */} # Lens Social Protocol Add SocialFi features to any application. --- The Lens Social Protocol is a collection of primitives designed to streamline development of SocialFi applications. They include the Social Primitives (Accounts, Usernames, Graphs, Groups, Feeds), Apps, Rules, Actions and Sponsorships.
Lens offers a highly customisable set of powerful Social Primitives developers can choose from. The Lens architecture has been streamlined to make integrating Social Primitives easy and flexible. Developers can build experiences with easy-to-plug-in “Social Legos.” Social Primitives are modular, enabling developers to create custom instances tailored to their needs. These core contracts can be modified with Rules and Actions. Apps and Sponsorships allow for easy management and customization for application builders. ================ File: src/pages/protocol/user-rewards.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # User Rewards Learn about Lens Account rewards and token distribution. --- Lens rewards active accounts with GHO tokens through our Smart Token Distributor: a data-driven, adaptive system that optimizes user engagement, retention, and economic participation in the decentralized ecosystem. ## How It Works The Smart Token Distributor uses a combination of heuristics, machine learning, and reinforcement learning to allocate tokens fairly and strategically. The system evaluates users across multiple dimensions: - **Contributors**: Accounts who interact frequently (likes, comments, tips, collects) - **Verified Accounts**: Authentic accounts filtered by ML models to exclude spam and bots - **Creators**: Accounts who receive meaningful engagement on their content - **Influencers**: Accounts with strong network connections and social influence - **Financially Active Accounts**: Accounts with significant on-chain financial participation - **Engaged Accounts**: Accounts who engage regularly over time The distributor calculates a composite score based on: - **Contributor Score**: Measures ecosystem engagement through interactions - **Creator Score**: Rewards valuable content generation and quality engagement received - **Consistency Score**: Ensures sustainable, long-term participation - **Wallet Score**: Considers financial participation and staking activity ## Fetch Account Rewards You can fetch the token distributions received by the logged-in Account using the tools below. Use the `useTokenDistributions` hook to fetch rewards from token distributions. ```tsx filename="With Loading" const { data, loading, error } = useTokenDistributions(request); ``` ```tsx filename="With Suspense" const { data, error } = useTokenDistributions({ suspense: true, ...request }); ``` Token distributions can be fetched only by the logged-in user Account. ```ts filename="Example" import { useTokenDistributions, evmAddress } from "@lens-protocol/react"; // … const { data, loading, error } = useTokenDistributions(); if (loading) { return

Loading…

; } if (error) { return

{error.message}

; } // data: {items: Array, pageInfo: PageInfo} ```
Use the paginated `fetchTokenDistributions` action to fetch rewards from token distributions. ```ts filename="Example" import { fetchTokenDistributions } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchTokenDistributions(client); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` Use the paginated `tokenDistributions` query to fetch rewards from token distributions. ```graphql filename="Query" query { tokenDistributions( request: { # optional, number of items per page (default: FIFTY) pageSize: TEN # other option is FIFTY } ) { items { amount { asset { address symbol decimals name } value } txHash timestamp } pageInfo { prev next } } } ``` ```json filename="Response" { "data": { "tokenDistributions": { "items": [ { "amount": { "asset": { "address": "0x0101010101010101010101010101010101010101", "symbol": "ETH", "decimals": 18, "name": "Ethereum" }, "value": "1000000000000000000" }, "txHash": "0x0101010101010101010101010101010101010101", "timestamp": "2024-01-01T00:00:00.000Z" } ], "pageInfo": { "prev": null, "next": "U29mdHdhcmU=" } } } } ```
Continue with the [Pagination](../best-practices/pagination) guide for more information on how to handle paginated results. ================ File: src/pages/storage/resources/glossary.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Glossary Key Terms and Concepts in Grove storage system. --- ## Access Control Layer An Access Control Layer (ACL) configuration determines whether content on Grove is mutable or immutable. It defines the rules for editing and deleting content, using a _Lens Account_, _Wallet Address_, or a _Generic Contract Call_ to enforce access permissions. ## File A user-provided file containing arbitrary data. It can be any type of content, such as documents, images, videos, or application-specific data. Each file is uniquely identified by a storage key and may be subject to access control rules if an ACL template is provided during upload. ## Folder A lightweight structure for organizing files and grouping them for bulk uploads or deletions. It does not support full folder semantics, such as nesting or adding files after creation. Instead, it serves as a fixed collection of files referenced by a shared storage key. ## Folder Index A folder index is an optional JSON file uploaded alongside a folder to define its contents. When resolving a folder’s storage key, the index file determines the response. If no folder index is present, a 404 status code is returned. ## Lens URI A Lens URI is a unique identifier for a resource on Grove. It follows the syntax: ```text lens:// ``` where `` is a [Storage Key](#storage-key) assigned to the resource. Example: `lens://af5225b6262e03be6bfacf31aa416ea5e00ebb05e802d0573222a92f8d0677f5` ## Mutability A mutable resource can be modified or deleted after it has been uploaded. Immutable resources can never be modified or deleted once they have been uploaded. You can control the mutability of a file by providing an ACL template during upload. ## Storage Key A Storage Key is a globally unique hexadecimal identifier assigned to a file or folder on Grove. It serves as a persistent reference, ensuring that each piece of stored content can be uniquely addressed and retrieved. The storage key always points to the latest version of the associated file or folder. Example: `af5225b6262e03be6bfacf31aa416ea5e00ebb05e802d0573222a92f8d0677f5` ================ File: src/pages/storage/usage/delete.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Deleting Content This guide will walk you through deleting content from Grove. --- A mutable resource can be deleted only by authorized addresses, as defined in its [Access Control configuration](./upload#permission-models). The Grove API enforces this by requiring a signed message to verify identity before allowing any changes. Deleting a file removes it permanently, while deleting a folder also removes all its contents. To delete a resource, follow these steps. ## Define a Signer First, create an object that satisfies the `Signer` interface: ```ts filename="Signer" interface Signer { signMessage({ message }): Promise; } ``` The address used to sign the message will be extracted from the signature and used to validate the ACL for the resource being deleted. If you are using [Viem](https://viem.sh/), the `WalletClient` instances satisfies the `Signer` interface so you can use it directly. ## Delete the Resource Then, delete the resource by calling the `delete` method, using its `lens://` URI to remove a file or an entire folder along with its contents. ```ts let response = await storageClient.delete("lens://af5225b…", walletClient); // response.success: boolean - true if the resource was deleted successfully ``` To delete a resource, follow these steps. ## Request a New Challenge First, request a new Challenge to sign for deleting the resource identified by a given storage key. ```bash filename="curl" curl -X POST 'https://api.grove.storage/challenge/new' \ -H 'Content-Type: application/json' \ -d '{ "storage_key": 323c0e1cceb…, "action": "delete" }' ``` ```http filename="HTTP" POST /challenge/new HTTP/1.1 Host: api.grove.storage Content-Type: application/json { "storage_key": "323c0e1cceb…", "action": "delete" } ``` This returns the following object. ```json filename="Response" { "message": "Access request for action=delete storage_key=323c0e1cceb… expires_at=1730906260859", "signature": "", "secret_random": "7525445508888297412" } ``` Where: - **`message`** is the message to be signed. - **`secret_random`** is a unique identifier for this challenge. - **`signature`** is the placeholder for the signature. ## Sign the Message Next, sign the `message` from the previous step using a signer that is authorized to delete the resource according to its ACL configuration. ```ts filename="viem" import { privateKeyToAccount } from "viem/accounts"; const account = privateKeyToAccount(process.env.APP_PRIVATE_KEY); const signature = account.signMessage({ message }); ``` ```ts filename="ethers" import { Wallet } from "ethers"; const signer = new Wallet(process.env.APP_PRIVATE_KEY); const signature = signer.signMessage(message); ``` ## Submit the Signed Challenge Next, post the signed challenge to the `/challenge/sign` endpoint. ```bash filename="curl" curl -X POST 'https://api.grove.storage/challenge/sign' \ -H 'Content-Type: application/json' \ -d '{ "message": "Access request for action=delete storage_key=323c0e1cceb…", "signature": "", "secret_random": "7525445508888297412" }' ``` ```http filename="HTTP" POST /challenge/sign HTTP/1.1 Host: api.grove.storage Content-Type: application/json { "message": "Access request for action=delete storage_key=323c0e1cceb…", "signature": "", "secret_random": "7525445508888297412" } ``` Where `` is the signature obtained in the previous step. This returns the following object. ```json filename="Response" { "challenge_cid": "QmWJiNhWCg1YiN1VWa8jJYL5rBNoPSAb5UxbohWF4ojLAn" } ``` Where `challenge_cid` identifies the signed challenge. ## Delete the Resource Finally, delete the resource by sending a DELETE request to the following endpoint: ```text https://api.grove.storage/?challenge_cid=&secret_random= ``` where: - `` is the storage key of the resource - `` is the `challenge_cid` from the step 3 - `` is the `secret_random` from the step 1 That's it—you successfully deleted a resource from Grove. ================ File: src/pages/storage/usage/edit.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Editing Content This guide will walk you through editing content on Grove. --- A mutable resource can be modified only by authorized addresses, as defined in its [Access Control configuration](./upload#permission-models). The Grove API enforces this by requiring a signed message to verify identity before allowing any changes. ## Editing a File Editing a file retains its `lens://` URI, replacing its content with a new version while keeping the same reference. To edit a file, follow these steps. ### Define a Signer First, create an object that satisfies the `Signer` interface: ```ts filename="Signer" interface Signer { signMessage({ message }): Promise; } ``` The address used to sign the message will be extracted from the signature and used to validate the ACL for the resource being edited. If you are using [Viem](https://viem.sh/), the `WalletClient` instances satisfies the `Signer` interface so you can use it directly. ### Define the New ACL Then, define the new ACL configuration to use. ```ts filename="Lens Account" import { chains } from "@lens-chain/sdk/viem"; import { lensAccountOnly } from "@lens-chain/storage-client"; const acl = lensAccountOnly( "0x1234…", // Lens Account Address chains.testnet.id ); ``` ```ts filename="Wallet Address" import { chains } from "@lens-chain/sdk/viem"; import { walletOnly } from "@lens-chain/storage-client"; const acl = walletOnly( "0x1234…", // Wallet Address chains.testnet.id ); ``` ```ts filename="Generic ACL" import { chains } from "@lens-chain/sdk/viem"; import { genericAcl, RECOVERED_ADDRESS_PARAM_MARKER, } from "@lens-chain/storage-client"; const acl = genericAcl(chains.testnet.id) .withContractAddress("0x1234…") .withFunctionSig("someFunction(address)") .withParams([RECOVERED_ADDRESS_PARAM_MARKER]) .build(); ``` It's developer responsability to provide the same ACL configuration if they want to retain the same access control settings. ### Edit the File Finally, use the `editFile` method to update the file. Suppose you have a form that allows users to replace the file content, an image in this case: ```html filename="index.html"
``` In the form’s submit event handler, you can edit the by passing: - the `lens://` URI of the file to be edited - the new `File` reference - the `Signer` instance - the ACL configuration ```ts filename="Edit Example"highlight="7-12" async function onSubmit(event: SubmitEvent) { event.preventDefault(); const input = event.currentTarget.elements["image"]; const file = input.files[0]; const response = await storageClient.editFile( "lens://323c0e1cceb…", file, walletClient, { acl } ); // response.uri: 'lens://323c0e1cceb…' } ``` The response is the same `FileUploadResponse` object as when [uploading a new file](./download#uploading-a-file).
To edit a file, follow these steps. ### Request a New Challenge First, request a new Challenge to sign for editing the resource identified by a given [Storage Key](../resources/glossary#storage-key). ```bash filename="curl" curl -X POST 'https://api.grove.storage/challenge/new' \ -H 'Content-Type: application/json' \ -d '{ "storage_key": 323c0e1cceb…, "action": "edit" }' ``` ```http filename="HTTP" POST /challenge/new HTTP/1.1 Host: api.grove.storage Content-Type: application/json { "storage_key": "323c0e1cceb…", "action": "edit" } ``` This returns the following object. ```json filename="Response" { "message": "Access request for action=edit storage_key=323c0e1cceb… expires_at=1730906260859", "signature": "", "secret_random": "7525445508888297412" } ``` Where: - **`message`** is the message to be signed. - **`secret_random`** is a unique identifier for this challenge. - **`signature`** is the placeholder for the signature. ### Sign the Message Next, sign the `message` from the previous step using a signer that is authorized to edit the file according to its original ACL configuration. ```ts filename="viem" import { privateKeyToAccount } from "viem/accounts"; const account = privateKeyToAccount(process.env.APP_PRIVATE_KEY); const signature = account.signMessage({ message }); ``` ```ts filename="ethers" import { Wallet } from "ethers"; const signer = new Wallet(process.env.APP_PRIVATE_KEY); const signature = signer.signMessage(message); ``` ### Submit the Signed Challenge Next, post the signed challenge to the `/challenge/sign` endpoint. ```bash filename="curl" curl -X POST 'https://api.grove.storage/challenge/sign' \ -H 'Content-Type: application/json' \ -d '{ "message": "Access request for action=edit storage_key=323c0e1cceb…", "signature": "", "secret_random": "7525445508888297412" }' ``` ```http filename="HTTP" POST /challenge/sign HTTP/1.1 Host: api.grove.storage Content-Type: application/json { "message": "Access request for action=edit storage_key=323c0e1cceb…", "signature": "", "secret_random": "7525445508888297412" } ``` Where `` is the signature obtained in the previous step. This returns the following object. ```json filename="Response" { "challenge_cid": "QmWJiNhWCg1YiN1VWa8jJYL5rBNoPSAb5UxbohWF4ojLAn" } ``` Where `challenge_cid` identifies the signed challenge. ### Define an ACL Next, define the new ACL to use. Create an `acl.json` file with the desired content. ```json filename="Lens Account" { "template": "lens_account", "lens_account": "0x1234…", "chain_id": 37111 } ``` ```json filename="Wallet Address" { "template": "wallet_address", "wallet_address": "0x1234…", "chain_id": 37111 } ``` ```json filename="Generic ACL" { "template": "generic", "chain_id": 37111, "contract_address": "", "function_sig": "someFunction(address)", "params": [""] } ``` ### Edit the File Finally, update the file using a [multipart POST request](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST#multipart_form_submission) to the following endpoint: ```text https://api.grove.storage/?challenge_cid=&secret_random= ``` where: - `` is the storage key of the file - `` is the `challenge_cid` from the step 3 - `` is the `secret_random` from the step 1 Suppose we are updating a video file with file named `watch_this_instead.mp4`. ```bash filename="curl" curl -X PUT 'https://api.grove.storage/323c0e1cceb…?challenge_cid=QmWJiNhWCg1YiN1VWa8jJYL5rBNoPSAb5UxbohWF4ojLAn&secret_random=7525445508888297412 \ -F '323c0e1ccebcfa70dc130772…=/path/to/watch_this_instead.mp4;type=video/mp4' \ -F 'lens-acl.json=/path/to/acl.json;type=application/json' ``` ```http filename="HTTP" PUT /323c0e1cceb…?challenge_cid=QmWJiNhWCg1YiN1VWa8jJYL5rBNoPSAb5UxbohWF4ojLAn&secret_random=7525445508888297412 Host: api.grove.storage Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW ----WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; name="323c0e1cceb…"; filename="watch_this_instead.mp4" Content-Type: video/mp4 ----WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; name="lens-acl.json"; filename="acl.json" Content-Type: application/json ----WebKitFormBoundary7MA4YWxkTrZu0gW-- ``` **What happens here:** 1. The file `watch_this_instead.mp4` is addressed using the same storage key as the original file. 2. The ACL configuration from step 4 is included as a separate multipart body and addressed under `name=lens-acl.json`. The server may respond with one of the following status codes: - `201 Created`: The folder content has been propagated to the underlying storage infrastructure. - `202 Accepted`: The folder content is being saved in the edge infrastructure and will be propagated to the underlying storage infrastructure asynchronously. In both cases, the new content is immediately available for download.
That's it—you successfully edited a file. ## Editing a JSON File If you need to update a JSON file and you are using the `@lens-chain/storage-client` library, you can use the `updateJson` method. ```ts filename="JSON Upload" highlight="4,6" import { chains } from "@lens-chain/sdk/viem"; const acl = lensAccountOnly("0x1234…", chains.testnet.id); // your ACL configuration const newData = { key: "value" }; const response = await storageClient.updateJson( "lens://323c0e1cceb…", newData, walletClient, { acl } ); ``` ================ File: src/pages/storage/usage/getting-started.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Gettings Started Get started with Grove in just a few lines of code. --- ## TypeScript You can interact with Grove's API via the `@lens-chain/storage-client` library. ### Install the Package First, install the `@lens-chain/storage-client` package: ```bash filename="npm" npm install @lens-chain/storage-client@latest ``` ```bash filename="yarn" yarn add @lens-chain/storage-client@latest ``` ```bash filename="pnpm" pnpm add @lens-chain/storage-client@latest ``` ### Instantiate the Client Then, instantiate the client with the following code: ```ts import { StorageClient } from "@lens-chain/storage-client"; const storageClient = StorageClient.create(); ``` That's it—you are now ready to upload files to Grove. ## API You can also interact with Grove using the RESTful API available at `https://api.grove.storage`. In the following guides, we will demonstrate how to interact with this API using `curl` commands. ================ File: src/pages/storage/index.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; import StorageIllustrationOne from "@/components/mdx/components/concepts/illustrations/StorageIllustrationOne"; import StorageIllustrationTwo from "@/components/mdx/components/concepts/illustrations/StorageIllustrationTwo"; export default ({ children }) => {children}; {/* Start of the page content */} # Grove Secure, flexible, onchain-controlled storage layer for Web3 apps. --- Grove allows developers to upload, edit, delete and retrieve data stored on Grove all powered by access control binded to the EVM network. Grove implements an efficient service layer which is positioned between IPFS nodes and EVM-based blockchain nodes. We've abstracted away all the hard parts for you, so that storing and retrieval of your data becomes fun to integrate into your web3 apps.
With our approach, any modifying access to your data can be controlled only by the data owners: during the inital upload you can provide an ACL template that will be later used to validate any modification attempts with public blockchain nodes. This feature is opt-in so if you prefer to have your data stored as immutable, you can just use the defaults. The dynamic nature of Grove allows builders to set any access control they need, unlocking a huge range of possibilities. Grove is not just limited to Lens: it is EVM compatible, to be used with any EVM chain, for any kind of data. For its first release, Grove is available on Lens, Abstract, Sophon, ZKsync, Base and Ethereum mainnet. ================ File: src/pages/protocol/accounts/fetch.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Fetch Accounts This guide will help you with fetching Account data from the Lens API. --- Lens Account data is comprised of: - Account Identity - Account Metadata - ML Score - Operational flags To illustrate how to fetch accounts, we will use the following fragment, which includes some of the most common fields of an Account: ```graphql filename="Account" fragment Account on Account { address username { value } metadata { name picture } } ``` At the end of this guide, we will expand on some of the fields that are not fully covered in the example above. ## Get an Account Use the `useAccount` hook to fetch a single Lens Account. Returns `null` if no account is found. ```tsx filename="With Loading" const { data, loading, error } = useAccount(request); ``` ```tsx filename="With Suspense" const { data, error } = useAccount({ suspense: true, ...request }); ``` An Account can be fetched by its address, username, txHash, or legacy Lens v2 ID. ```ts filename="Account Address" import { useAccount, evmAddress } from "@lens-protocol/react"; // … const { data, loading, error } = useAccount({ address: evmAddress("0x1234…"), }); if (loading) { return

Loading…

; } if (error) { return

{error.message}

; } // data: Account | null ``` ```ts filename="Lens Username" import { useAccount, evmAddress } from "@lens-protocol/react"; // … const { data, loading, error } = useAccount({ username: { localName: "wagmi", }, }); if (loading) { return

Loading…

; } if (error) { return

{error.message}

; } // data: Account | null ``` ```ts filename="Custom Username" import { useAccount, evmAddress } from "@lens-protocol/react"; // … const { data, loading, error } = useAccount({ username: { localName: "foobar", namespace: evmAddress("0x1234…"), // the Username namespace address }, }); if (loading) { return

Loading…

; } if (error) { return

{error.message}

; } // data: Account | null ``` ```ts filename="Tx Hash" import { useAccount, txHash } from "@lens-protocol/react"; // … const { data, loading, error } = useAccount({ txHash: txHash("0x1234…"), }); if (loading) { return

Loading…

; } if (error) { return

{error.message}

; } // data: Account | null ``` ```ts filename="Legacy Profile Id" import { useAccount, LegacyProfileId } from "@lens-protocol/react"; // … const { data, loading, error } = useAccount({ legacyProfileId: "0x05" as LegacyProfileId, }); if (loading) { return

Loading…

; } if (error) { return

{error.message}

; } // data: Account | null ```
Use the `fetchAccount` action to fetch an Account by its address, username, txHash or legacy Lens v2 ID. ```ts filename="By Address" import { evmAddress } from "@lens-protocol/client"; import { fetchAccount } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchAccount(client, { address: evmAddress("0x1234…"), }); if (result.isErr()) { return console.error(result.error); } const account = result.value; ``` ```ts filename="By Lens Username" import { fetchAccount } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchAccount(client, { username: { localName: "wagmi", }, }); if (result.isErr()) { return console.error(result.error); } const account = result.value; ``` ```ts filename="By Any Username" import { evmAddress } from "@lens-protocol/client"; import { fetchAccount } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchAccount(client, { username: { localName: "foobar", namespace: evmAddress("0x1234…"), // the Username namespace address }, }); if (result.isErr()) { return console.error(result.error); } const account = result.value; ``` ```ts filename="By Tx Hash" import { txHash } from "@lens-protocol/client"; import { fetchAccount } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchAccount(client, { txHash: txHash("0x1234…"), }); if (result.isErr()) { return console.error(result.error); } const account = result.value; ``` ```ts filename="By Legacy Profile Id" import { LegacyProfileId } from "@lens-protocol/client"; import { fetchAccount } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchAccount(client, { legacyProfileId: "0x05" as LegacyProfileId, }); if (result.isErr()) { return console.error(result.error); } const account = result.value; ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; export const client = PublicClient.create({ environment: mainnet, }); ``` Use the `account` query to fetch an Account by its address, username, txHash, or legacy Lens v2 ID. ```graphql filename="Query" query { account( request: { username: { localName: "wagmi" # Optional. Defaults to lens/* namespace. # namespace: EvmAddress } # OR the Account address # address: EvmAddress # OR the Legacy Profile ID # legacyProfileId: LegacyProfileId # e.g., "0x05" # OR the TxHash of the Account creation transaction # txHash: TxHash } ) { address username { value } metadata { name picture } } } ``` ```json filename="Response" { "data": { "account": { "address": "0x1234…", "username": { "value": "lens/wagmi" }, "metadata": { "name": "WAGMI", "picture": "https://example.com/wagmi.jpg" } } } } ```
## Bulk Accounts List Use the `useAccountsBulk` hook to fetch a finite number of accounts. ```tsx filename="With Loading" const { data, loading, error } = useAccountsBulk(request); ``` ```tsx filename="With Suspense" const { data, error } = useAccountsBulk({ suspense: true, ...request }); ``` Accounts can be fetched by their addresses, usernames, or legacy Lens v2 IDs. ```ts filename="By Addresses" import { useAccountsBulk, evmAddress } from "@lens-protocol/react"; // … const { data, loading, error } = useAccountsBulk({ addresses: [evmAddress("0x1234…"), evmAddress("0x5678…")], }); if (loading) { return

Loading…

; } if (error) { return

{error.message}

; } // data: Array ``` ```ts filename="By Usernames" import { useAccountsBulk } from "@lens-protocol/react"; // … const { data, loading, error } = useAccountsBulk({ usernames: [ { localName: "wagmi", // Optional. Defaults to lens/* namespace. // namespace: evmAddress("0x1234…"), }, { localName: "ape", // Optional. Defaults to lens/* namespace. // namespace: evmAddress("0x5678…"), }, ], }); if (loading) { return

Loading…

; } if (error) { return

{error.message}

; } // data: Array ``` ```ts filename="By Legacy Profile IDs" import { useAccountsBulk, LegacyProfileId } from "@lens-protocol/react"; // … const { data, loading, error } = useAccountsBulk({ legacyProfileIds: ["0x05" as LegacyProfileId, "0x06" as LegacyProfileId], }); if (loading) { return

Loading…

; } if (error) { return

{error.message}

; } // data: Array ``` ```ts filename="Owned By Addresses" import { useAccountsBulk, evmAddress } from "@lens-protocol/react"; // … const { data, loading, error } = useAccountsBulk({ ownedBy: [evmAddress("0x1234…"), evmAddress("0x5678…")], }); if (loading) { return

Loading…

; } if (error) { return

{error.message}

; } // data: Array ```
Use the `fetchAccountsBulk` action to fetch a finite number of accounts by their addresses, usernames, or legacy Lens v2 IDs. ```ts filename="By Addresses" import { evmAddress } from "@lens-protocol/client"; import { fetchAccountsBulk } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchAccountsBulk(client, { addresses: [evmAddress("0x1234…"), evmAddress("0x5678…")], }); if (result.isErr()) { return console.error(result.error); } // Array const accounts = result.value; ``` ```ts filename="By Usernames" import { fetchAccountsBulk } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchAccountsBulk(client, { usernames: [ { localName: "wagmi", // Optional. Defaults to lens/* namespace. // namespace: evmAddress("0x1234…"), }, { localName: "ape", // Optional. Defaults to lens/* namespace. // namespace: evmAddress("0x5678…"), }, ], }); if (result.isErr()) { return console.error(result.error); } // Array const accounts = result.value; ``` ```ts filename="By Legacy Profile IDs" import { LegacyProfileId } from "@lens-protocol/client"; import { fetchAccountsBulk } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchAccountsBulk(client, { legacyProfileIds: ["0x05" as LegacyProfileId, "0x06" as LegacyProfileId], }); if (result.isErr()) { return console.error(result.error); } // Array const accounts = result.value; ``` ```ts filename="Owned By Addresses" import { evmAddress } from "@lens-protocol/client"; import { fetchAccountsBulk } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchAccountsBulk(client, { ownedBy: [evmAddress("0x1234…"), evmAddress("0x5678…")], }); if (result.isErr()) { return console.error(result.error); } // Array const accounts = result.value; ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; export const client = PublicClient.create({ environment: mainnet, }); ``` Use the `accountsBulk` query to fetch a finite number of accounts by their addresses, usernames, or legacy Lens v2 IDs. ```graphql filename="Query" query { accountsBulk( request: { usernames: [ { localName: "wagmi" # Optional. Defaults to lens/* namespace. # namespace: EvmAddress } { localName: "ape" # Optional. Defaults to lens/* namespace. # namespace: EvmAddress } ] # OR a list of Account addresses # addresses: [EvmAddress!] # OR a list of Legacy Profile IDs # legacyProfileIds: [LegacyProfileId!] e.g., ["0x05", "0x06"] # OR a list of ownedBy addresses # ownedBy: [EvmAddress!] } ) { address username { value } metadata { name picture } } } ``` ```json filename="Response" { "data": { "accountsBulk": [ { "address": "0x1234…", "username": { "value": "lens/wagmi" }, "metadata": { "name": "WAGMI", "picture": "https://example.com/wagmi.jpg" } }, { "address": "0x5678…", "username": { "value": "lens/ape" }, "metadata": { "name": "APE", "picture": "https://example.com/bob.jpg" } } ] } } ```
## Search Accounts Use the `useAccounts` hook to search for accounts. ```tsx filename="With Loading" const { data, loading, error } = useAccounts(request); ``` ```tsx filename="With Suspense" const { data, error } = useAccounts({ suspense: true, ...request }); ``` Accounts can be searched by their username and namespace. ```ts filename="Search on Lens Namespace" import { useAccounts, evmAddress } from "@lens-protocol/react"; // … const { data, loading, error } = useAccounts({ filter: { searchBy: { localNameQuery: "wagmi", }, }, }); if (loading) { return

Loading…

; } if (error) { return

{error.message}

; } // data: Array ``` ```ts filename="Search on Custom Namespace" import { useAccounts, evmAddress } from "@lens-protocol/react"; // … const { data, loading, error } = useAccounts({ filter: { searchBy: { localNameQuery: "wagmi", namespace: evmAddress("0x1234…"), }, }, }); if (loading) { return

Loading…

; } if (error) { return

{error.message}

; } // data: Array ```
Use the paginated `fetchAccounts` action to search for accounts by their usernames. ```ts filename="Search on Lens Namespace" import { fetchAccounts } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchAccounts(client, { filter: { searchBy: { localNameQuery: "wagmi", }, }, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="Search on Custom Namespace" import { evmAddress } from "@lens-protocol/client"; import { fetchAccounts } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchAccounts(client, { filter: { searchBy: { localNameQuery: "wagmi", namespace: evmAddress("0x1234…"), }, }, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; export const client = PublicClient.create({ environment: mainnet, }); ``` Use the paginated `accounts` query to search for accounts by their usernames. ```graphql filename="Query" query { accounts( request: { filter: { searchBy: { localNameQuery: "bob" # Optional. Defaults to lens/* namespace. # namespace: EvmAddress } } orderBy: ACCOUNT_SCORE # other options: ALPHABETICAL, BEST_MATCH } ) { items { address username { value } metadata { name picture } } pageInfo { prev next } } } ``` ```json filename="Response" { "data": { "accounts": { "items": [ { "address": "0x1234…", "username": { "value": "lens/bob" }, "metadata": { "name": "Bob", "picture": "https://example.com/bob.jpg" } } ], "pageInfo": { "prev": null, "next": "U29mdHdhcmU=" } } } } ```
Continue with the [Pagination](../best-practices/pagination) guide for more information on how to handle paginated results. ## Recommended Accounts Accounts recommendations are generated by leveraging a user's social graph and engagement data. Machine learning is employed to rank potential accounts to follow, based on the likelihood and quality of interaction. ### Fetch Recommendations Use the `useAccountRecommendations` hook to fetch recommended accounts. ```tsx filename="With Loading" const { data, loading, error } = useAccountRecommendations(request); ``` ```tsx filename="With Suspense" const { data, error } = useAccountRecommendations({ suspense: true, ...request, }); ``` The list of recommended Accounts is based on Lens Machine Learning (ML) algorithms. ```ts filename="Example" import { useAccountRecommendations, evmAddress } from "@lens-protocol/react"; // … const { data, loading, error } = useAccountRecommendations({ account: evmAddress("0x1234…"), shuffle: true, // optional, shuffle the results }); if (loading) { return

Loading…

; } if (error) { return

{error.message}

; } // data: {items: Array, pageInfo: PageInfo} ```
Use the paginated `fetchAccountRecommendations` action to retrieve a list of recommended Accounts based on Lens Machine Learning (ML) algorithms. ```ts filename="Example" import { evmAddress } from "@lens-protocol/client"; import { fetchAccountRecommendations } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchAccountRecommendations(client, { account: evmAddress("0x1234…"), shuffle: true, // optional, shuffle the results }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; export const client = PublicClient.create({ environment: mainnet, }); ``` Use the paginated `fetchAccountRecommendations` query to retrieve a list of recommended Accounts based on Lens Machine Learning (ML) algorithms. ```graphql filename="Query" query { mlAccountRecommendations( request: { account: "0x1234…" # optional, shuffle the results # shuffle: Boolean } ) { items { address username { value } metadata { name picture } } pageInfo { prev next } } } ``` ```json filename="Response" { "data": { "mlAccountRecommendations": { "items": [ { "address": "0x1234…", "username": { "value": "lens/bob" }, "metadata": { "name": "Bob", "picture": "https://example.com/bob.jpg" } } ], "pageInfo": { "prev": null, "next": "U29mdHdhcmU=" } } } } ```
Continue with the [Pagination](../best-practices/pagination) guide for more information on how to handle paginated results. ### Dismiss Recommendations When you dismiss recommended Accounts, they are removed from your suggestions and the recommendation algorithm adjusts accordingly. You MUST be authenticated as the Account Owner or Account Manager to dismiss account recommendations for a given Lens Account. Use the `dismissRecommendedAccount` action to remove Accounts from the list of recommendations. ```ts filename="Example" import { evmAddress } from "@lens-protocol/client"; import { dismissRecommendedAccount } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await dismissRecommendedAccount(client, { accounts: [evmAddress("0x1234…"), evmAddress("0x5678…")], }); if (result.isErr()) { return console.error(result.error); } ``` Use the `dismissRecommendedAccount` mutation to remove Accounts from the list of recommendations. ```graphql filename="Mutation" mutation { mlDismissRecommendedAccounts( request: { accounts: ["0x1234…", "0x5678…"] } ) {} } ``` ```json filename="Response" { "data": { "mlDismissRecommendedAccounts": null } } ``` Coming soon ## Account Fields In this section we will expand on some of the Account fields that are not covered in the examples above. ### Account Identity An Account can have one or more usernames across different namespaces, but only one username per namespace. By default, the `username` field returns the username in the `lens/` namespace, if available. It also accepts an optional argument to specify a different namespace. ```graphql filename="Account" fragment Account on Account { # ... username { ...Username } socialX: username(request: { namespace: "0x1234…" }) { ...Username } } ``` ```graphql filename="Username" fragment Username on Username { id # ID! # The fully qualified username value (e.g., lens/wagmi). value # UsernameValue! # The namespace of the username namespace # EvmAddress! # The local name of the username (e.g., bob). localName # String! # The address that the username is linked to, if any. linkedTo # EvmAddress, # The address that owns the username entry. ownedBy # EvmAddress, # The timestamp when the username was created. timestamp # DateTime, } ``` ### Account Metadata Although briefly mentioned in the examples, the `metadata` field is a rich object that can include a variety of information about the account. It contains the Account Metadata object that was linked to the Account at the time of creation or update. This object can include the following fields: See the [Create an Account](./create) guide for more information on how this object is created. ```graphql filename="AccountMetadata" type AccountMetadata { # A bag of attributes. attributes: [MetadataAttribute!]! # The profile bio as markdown. bio: String # The profile cover picture. coverPicture: URI # A unique identifier. id: String! # The profile display name. name: String # The profile picture. picture: URI } ``` ```graphql filename="MetadataAttribute" type MetadataAttribute { type: MetadataAttributeType! key: String! value: String! } enum MetadataAttributeType { BOOLEAN DATE NUMBER STRING JSON } ``` ### Account Score The Lens team has implemented a series of measures to uphold the integrity of the Lens ecosystem. The Account Score is a probability-based measure that evaluates an account's signal strength, helping to reduce the impact of spammy behavior on the user experience. This score is calculated using a set of ML algorithms that consider factors like follower graphs, content, and other variables. Higher scores suggest a positive and active presence within the ecosystem. ```graphql filename="Fragment" fragment Account on Account { # ... score } ``` ### Logged-In Operations The Lens schema allows logged-in users to fetch details about available actions and actions already taken, via the `operations` field. ```ts filename="TypeScript" type Account = { operations: LoggedInAccountOperations | null; }; type LoggedInAccountOperations = { isFollowedByMe: boolean; isFollowingMe: boolean; canFollow: OperationValidationOutcome | null; canUnfollow: OperationValidationOutcome | null; isMutedByMe: boolean; isBlockedByMe: boolean; hasBlockedMe: boolean; canBlock: OperationValidationOutcome | null; canUnblock: OperationValidationOutcome | null; hasReported: boolean; }; ``` ```graphql filename="GraphQL" fragment Account on Account { operations { ...OperationValidationOutcome } } fragment OperationValidationOutcome on OperationValidationOutcome { canBlock canUnblock canFollow { ...OperationValidationOutcome } canUnfollow { ...OperationValidationOutcome } hasBlockedMe hasReported isBlockedByMe isFollowedByMe isFollowingMe isMutedByMe } ``` The `LoggedInAccountOperations` type specifies the actions the user can perform (e.g., _canFollow_, _canBlock_) and the actions already taken (e.g., _isFollowedByMe_, _isBlockedByMe_). Where: - `isFollowedByMe`: Indicates whether the logged-in account follows the account. - `isFollowingMe`: Indicates whether the account follows the logged-in account. - `canFollow`: Indicates whether the logged-in account can follow the account. - `canUnfollow`: Indicates whether the logged-in account can unfollow the account. - `isMutedByMe`: Indicates whether the account is muted by the logged-in account. - `isBlockedByMe`: Indicates whether the account is blocked by the logged-in account. - `hasBlockedMe`: Indicates whether the account has blocked the logged-in account. - `canBlock`: Indicates whether the logged-in account can block the account. - `canUnblock`: Indicates whether the logged-in account can unblock the account. - `hasReported`: Indicates whether the logged-in account has reported the account. Fields returning an `OperationValidationOutcome` give information on the feasibility of the operation. More details in the [Querying Data](../best-practices/querying-data#operation-validation) guide. The `isFollowedByMe`, `isFollowingMe`, `canFollow`, and `canUnfollow` fields accept an optional argument specifying the Graph address to check the follow status. ```ts filename="fragments/accounts.ts" import { graphql, OperationValidationOutcomeFragment, } from "@lens-protocol/client"; export const LoggedInAccountOperationsFragment = graphql( ` fragment LoggedInAccountOperations on LoggedInAccountOperations { isFollowedByMeOnMyGraph: isFollowedByMe(request: { graph: "0x1234…" }) isFollowingMeOnMyGraph: isFollowingMe(request: { graph: "0x1234…" }) canFollowOnMyGraph: canFollow(request: { graph: "0x1234…" }) { ...OperationValidationOutcome } canUnfollowOnMyGraph: canUnfollow(request: { graph: "0x1234…" }) { ...OperationValidationOutcome } } `, [OperationValidationOutcomeFragment], ); ``` Alias the corresponding fields as needed: ```graphql filename="LoggedInAccountOperations" fragment LoggedInAccountOperations on LoggedInAccountOperations { isFollowedByMeOnMyGraph: isFollowedByMe(request: { graph: "0x1234…" }) isFollowingMeOnMyGraph: isFollowingMe(request: { graph: "0x1234…" }) canFollowOnMyGraph: canFollow(request: { graph: "0x1234…" }) { ...OperationValidationOutcome } canUnfollowOnMyGraph: canUnfollow(request: { graph: "0x1234…" }) { ...OperationValidationOutcome } } ``` ### Is-Member-Of The `isMemberOf` field returns a boolean indicating whether the account is a member of a specific group. ```ts filename="fragments/accounts.ts" import { graphql } from "@lens-protocol/client"; export const IsMemberOfFragment = graphql(` fragment IsMemberOf on Account { vip: isMemberOfGroup(group: "0x1234…") pro: isMemberOfGroup(group: "0x5678…") } `); ``` ```graphql filename="IsMemberOf" fragment IsMemberOf on Account { vip: isMemberOfGroup(group: "0x1234…") pro: isMemberOfGroup(group: "0x5678…") } ``` If an argument is not provided, the query follows a fallback approach: - It first checks for a Graph address specified within the query scope. - If no Graph address is found, it defaults to using the global Lens Graph. ================ File: src/pages/protocol/accounts/funds.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Account Funds This guide explains how to manage your Lens Account funds. --- The Lens Account is a smart wallet that allows you to manage funds for your collects, tips, and other transactions on Lens. By using the Lens Account funds, your wallet's exposure to the Lens app is minimized, which helps mitigate potential security threats. ## Account Balances A Lens Account can hold native **GHO** tokens (**GRASS** on testnet) and various ERC-20 tokens. To fetch token balances of the account refer to the [Balances](../tools/balances) docs. ## Fiat On-Ramps Integrate fiat on-ramps into your app to allow users to deposit funds into their Lens Account using a debit or credit card. This can be achieved with [thirdweb Pay](https://portal.thirdweb.com/connect/pay/overview).

The following procedure lays a foundation for integrating fiat on-ramps into your app. Due to the absence of liquidity and swapping routes, this solution is fully testable only on the Lens Mainnet. ### Get Your Client ID First, log in to the [thirdweb dashboard](https://thirdweb.com/team). Navigate to the _Settings_ page and create an API key to get your Client ID. ### Configure thirdweb SDK Next, install the thirdweb SDK. ```bash filename="npm" npm install thirdweb ``` ```bash filename="yarn" yarn add thirdweb ``` ```bash filename="pnpm" pnpm add thirdweb ``` And, wrap your app with the `` component as follows. ```tsx filename="App.tsx" import { ThirdwebProvider } from "thirdweb/react"; export default function App() { return {/* Your app code here */}; } ``` ### Create a thirdweb Client Create a thirdweb client using your Client ID. ```ts filename="thirdweb.ts" import { createThirdwebClient } from "thirdweb"; export const client = createThirdwebClient({ clientId: "", }); ``` ### The PayEmbed Widget Finally, integrate the `PayEmbed` widget into your app to allow users to deposit funds using fiat on-ramps. ```tsx import { defineChain, NATIVE_TOKEN_ADDRESS } from "thirdweb"; import { PayEmbed } from "thirdweb/react"; import { client } from "./thirdweb"; const lensChain = defineChain({ id: 232 }); // … { console.log("Purchase success", purchase); }, }} />; ``` Use the `paymentInfo.sellerAddress` to specify the Lens Account address to top up. The integration suggested here is known as [Commerce payment](https://playground.thirdweb.com/connect/pay/commerce), hence the term _seller_ for the address receiving the tokens. ### Wallet Adapters To prevent the need to reconnect the wallet when using the `PayEmbed` widget, you can utilize the wallet adapter for your chosen library. ```ts filename="thirdweb.ts" import { createThirdwebClient } from "thirdweb"; import { viemAdapter } from "thirdweb/adapters/viem"; import { ethereum } from "thirdweb/chains"; import { type Address, createWalletClient, custom } from "viem"; import { walletClient } from "./wallet"; export const thirdwebWallet = await viemAdapter.wallet.fromViem({ walletClient: walletClient, }); export const client = createThirdwebClient({ clientId: "", }); ``` ```ts filename="wallet.ts" import "viem/window"; import { Address, createWalletClient, custom } from "viem"; import { chains } from "@lens-chain/sdk/viem"; // For more information on hoisting accounts, // visit: https://viem.sh/docs/accounts/local.html#optional-hoist-the-account const [account] = (await window.ethereum!.request({ method: "eth_requestAccounts", })) as [Address]; export const walletClient = createWalletClient({ account, chain: chains.mainnet, transport: custom(window.ethereum!), }); ``` If you encounter a TypeScript error while assigning the `WalletClient` instance to the thirdweb viem adapter, it is likely due to a version mismatch between the viem version you have installed and the version used in the thirdweb SDK. ```ts filename="Type Error" highlight="2" export const thirdwebWallet = await viemAdapter.wallet.fromViem({ walletClient: walletClient, }); ``` To fix this, you can force the viem version in your `package.json` file according to the package manager you are using. ```json filename="npm" { "dependencies": { "thirdweb": "^5.89.0", "viem": "^2.21.55" }, "overrides": { "viem": "^2.21.55" } } ``` ```json filename="yarn" { "dependencies": { "thirdweb": "^5.89.0", "viem": "^2.21.55" }, "resolutions": { "viem": "^2.21.55" } } ``` ```json filename="pnpm" { "dependencies": { "thirdweb": "^5.89.0", "viem": "^2.21.55" }, "pnpm": { "overrides": { "viem": "^2.21.55" } } } ``` ```ts filename="thirdweb.ts" import { createThirdwebClient } from "thirdweb"; import { ethers6Adapter } from "thirdweb/adapters/ethers6"; import { ethereum } from "thirdweb/chains"; import { type Address, createWalletClient, custom } from "viem"; import { signer } from "./signer"; export const thirdwebWallet = await ethers6Adapter.signer.fromEthers({ signer, }); export const client = createThirdwebClient({ clientId: "", }); await thirdwebWallet.connect({ client }); ``` ```ts filename="signer.ts" import { Signer } from "@lens-chain/sdk/ethers"; import { browserProvider, lensProvider } from "./providers"; const network = await browserProvider.getNetwork(); export const signer = Signer.from( await browserProvider.getSigner(), Number(network.chainId), lensProvider, ); ``` ```ts filename="providers.ts" import "@lens-chain/sdk/globals"; import { BrowserProvider, getDefaultProvider, Network, } from "@lens-chain/sdk/ethers"; import { Eip1193Provider } from "ethers"; // Lens Chain (L2) export const lensProvider = getDefaultProvider(Network.Testnet); // User's network export const browserProvider = new BrowserProvider( window.ethereum as Eip1193Provider, ); ``` Then, run the following line before embedding the `PayEmbed` widget. ```ts await thirdwebWallet.connect({ client }); ``` And pass the thirdweb wallet to the `PayEmbed` widget. ```tsx import { thirdwebWallet } from "./thirdweb"; // … ; ``` That's it—this will make the `PayEmbed` widget render without displaying a _Connect_ button. ## Deposit Funds Account Owners and Account Managers can deposit funds into their Lens Accounts. To deposit funds, follow these steps. You MUST be authenticated as the Account Owner or Account Manager to deposit funds into the authenticated Lens Account. ### Prepare the Request First, specify the amount to deposit. Use the `deposit` action to prepate the transaction request. ```ts filename="Native Token Deposit" import { bigDecimal } from "@lens-protocol/client"; import { deposit } from "@lens-protocol/client/actions"; const result = await deposit(sessionClient, { native: bigDecimal(42.5), }); ``` ```ts filename="ERC-20 Token Deposit" import { bigDecimal, evmAddress } from "@lens-protocol/client"; import { deposit } from "@lens-protocol/client/actions"; const result = await deposit(sessionClient, { erc20: { currency: evmAddress("0x1234…"), value: bigDecimal(42.5), }, }); ``` Use the `deposit` mutation to prepare the transaction request. ```graphql filename="Native Token Deposit" mutation { deposit(request: { native: "42.5" }) { ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on InsufficientFunds { reason } ... on TransactionWillFail { reason } } } ``` ```graphql filename="ERC-20 Token Deposit" mutation { deposit(request: { erc20: { currency: "0x1234…", value: "42.5" } }) { ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on InsufficientFunds { reason } ... on TransactionWillFail { reason } } } ``` Coming soon ### Handle Result Then, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,7" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await withdraw(sessionClient, { // … }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,7" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await withdraw(sessionClient, { // … }).andThen(handleOperationWith(signer)); ``` Unlike most other transactions, there is no need to wait for the transaction to be indexed. Then, handle the result as described in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Unlike most other transactions, there is no need to poll the `transactionStatus` query to wait for the transaction to be indexed. Coming soon That's it—you can now use the funds in the Lens Account for collecting, tipping, and any other action or rule that requires funds. ## Wrapped GHO Account Owners can wrap and unwrap **GHO**—or **GRASS** on testnet— held in their Lens Accounts. Account Managers can also wrap and unwrap GHO from an account they manage, provided they have the `canTransferTokens` and `canTransferNative` permissions. See the [Account Manager](./manager#add-account-managers) guide for more information. ### Wrap GHO To wrap tokens, follow these steps. You MUST be authenticated as the Account Owner or Account Manager to be able to wrap **GHO**—or **GRASS** on testnet—held in your Lens Account. #### Prepare the Request First, specify the amount to wrap. Use the `wrapTokens` action to prepate the transaction request. ```ts filename="Wrap Tokens" import { bigDecimal } from "@lens-protocol/client"; import { wrapTokens } from "@lens-protocol/client/actions"; const result = await wrapTokens(sessionClient, { amount: bigDecimal(42.5), }); ``` Use the `wrapTokens` mutation to prepare the transaction request. ```graphql filename="Wrap Tokens" mutation { wrapTokens(request: { amount: "42.5" }) { ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on InsufficientFunds { reason } ... on TransactionWillFail { reason } } } ``` Coming soon #### Handle Result Then, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,7" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await await wrapTokens(sessionClient, { amount: bigDecimal(42.5), }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,7" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await await wrapTokens(sessionClient, { amount: bigDecimal(42.5), }).andThen(handleOperationWith(signer)); ``` Unlike most other transactions, there is no need to wait for the transaction to be indexed. Then, handle the result as described in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Unlike most other transactions, there is no need to poll the `transactionStatus` query to wait for the transaction to be indexed. Coming soon That's it—wrapped tokens are now in the Lens Account. ### Unwrap GHO To unwrap tokens, follow these steps. You MUST be authenticated as the Account Owner or Account Manager to be able to unwrap WGHO or WGRASS held in your Lens Account. #### Prepare the Request First, specify the amount to unwrap. Use the `unwrapTokens` action to prepare the transaction request. ```ts filename="Unwrap Tokens" import { bigDecimal } from "@lens-protocol/client"; import { unwrapTokens } from "@lens-protocol/client/actions"; const result = await unwrapTokens(sessionClient, { amount: bigDecimal(42.5), }); ``` Use the `unwrapTokens` mutation to prepare the transaction request. ```graphql filename="Unwrap Tokens" mutation { unwrapTokens(request: { amount: "42.5" }) { ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on InsufficientFunds { reason } ... on TransactionWillFail { reason } } } ``` Coming soon #### Handle Result Then, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,7" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await unwrapTokens(sessionClient, { // … }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,7" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await unwrapTokens(sessionClient, { // … }).andThen(handleOperationWith(signer)); ``` Unlike most other transactions, there is no need to wait for the transaction to be indexed. Then, handle the result as described in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Unlike most other transactions, there is no need to poll the `transactionStatus` query to wait for the transaction to be indexed. Coming soon That's it—unwrapped tokens are now in the Lens Account. ## Withdraw Funds Account Owners can withdraw funds from their Lens Accounts. Account Managers can also withdraw funds from an account they manage, provided they have the `canTransferTokens` and/or `canTransferNative` permissions. See the [Account Manager](./manager#add-account-managers) guide for more information. To withdraw funds from a Lens Account, follow these steps. You MUST be authenticated as the Account Owner or Account Manager with the necessary permissions of the Lens Account you intend to withdraw funds from. ### Prepare the Request First, specify the amount to withdraw and the destination address. Use the `withdraw` action to prepate the transaction request. ```ts filename="Native Token Withdrawal" import { bigDecimal } from "@lens-protocol/client"; import { withdraw } from "@lens-protocol/client/actions"; const result = await withdraw(sessionClient, { native: bigDecimal(42.5), }); ``` ```ts filename="ERC-20 Token Withdrawal" import { bigDecimal, evmAddress } from "@lens-protocol/client"; import { withdraw } from "@lens-protocol/client/actions"; const result = await withdraw(sessionClient, { erc20: { currency: evmAddress("0x1234…"), value: bigDecimal(42.5), }, }); ``` Use the `withdraw` mutation to prepare the transaction request. ```graphql filename="Native Token Withdrawal" mutation { widhdraw(request: { native: "42.5" }) { ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on InsufficientFunds { reason } ... on TransactionWillFail { reason } } } ``` ```graphql filename="ERC-20 Token Withdrawal" mutation { widhdraw(request: { erc20: { currency: "0x1234…", value: "42.5" } }) { ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on InsufficientFunds { reason } ... on TransactionWillFail { reason } } } ``` Coming soon ### Handle Result Then, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,7" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await withdraw(sessionClient, { // … }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,7" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await withdraw(sessionClient, { // … }).andThen(handleOperationWith(signer)); ``` Unlike most other transactions, there is no need to wait for the transaction to be indexed. Then, handle the result as described in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Unlike most other transactions, there is no need to poll the `transactionStatus` query to wait for the transaction to be indexed. Coming soon That's it—funds are now in the wallet address. ================ File: src/pages/protocol/apps/manage.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Manage Apps This guide explains how to manage Apps on Lens. --- ## Update App Metadata To update the metadata of an existing app, follow these steps. You MUST be authenticated as [Builder](../authentication) and be owner or admin of the App to update its metadata. ### Create New App Metadata First, construct an App Metadata object with the new content. Use the `@lens-protocol/metadata` package to construct a valid `AppMetadata` object: ```ts filename="Example" import { MetadataAttributeType, app } from "@lens-protocol/metadata"; const metadata = app({ name: "XYZ", tagline: "The next big thing", description: "An app to rule them all", logo: "lens://4f91cab87ab5e4f5066f878b72…", developer: "John Doe ", url: "https://example.com", termsOfService: "https://example.com/terms", privacyPolicy: "https://example.com/privacy", platforms: ["web", "ios", "android"], }); ``` If you opted for manually create Metadata objects, make sure it conform to the [App Metadata JSON Schema](https://json-schemas.lens.dev/app/1.0.0.json). ```json filename="Example" { "$schema": "https://json-schemas.lens.dev/app/1.0.0.json", "lens": { "id": "6418053f-89ae-4f9e-8362-8d1b6c0cdafb", "name": "XYZ", "tagline": "The next big thing", "description": "An app to rule them all", "logo": "lens://4f91cab87ab5e4f5066f878b72…", "url": "https://example.com", "platforms": ["web", "ios", "android"] } } ``` ### Upload App Metadata Next, upload the App Metadata object to a public URI. ```ts import { storageClient } from "./storage-client"; const { uri } = await storageClient.uploadAsJson(metadata); console.log(uri); // e.g., lens://4f91ca… ``` This example uses [Grove storage](../../storage) to host the Metadata object. See the [Lens Metadata Standards](../best-practices/metadata-standards#host-metadata-objects) guide for more information on hosting Metadata objects. ### Update New Custom App Metadata Next, update the app metadata using `setAppMetadata` action. ```ts filename="Example" import { uri, evmAddress } from "@lens-protocol/client"; import { setAppMetadata } from "@lens-protocol/client/actions"; // … const result = await setAppMetadata(sessionClient, { metadataUri: uri("lens://4f91…"), // the URI with new metadata app: evmAddress("0x1234…"), }); ``` Next, update the app metadata using `setAppMetadata` mutation. ```graphql filename="Mutation" mutation { createApp( request: { metadataUri: "lens://4f91cab87ab5e4f5066f878b72…" app: "0x1234…" } ) { ... on SponsoredTransactionRequest { SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ### Handle Result Finally, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await setAppMetadata(sessionClient, { metadataUri: uri("lens://4f91…"), app: evmAddress("0x1234…"), }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await setAppMetadata(sessionClient, { metadataUri: uri("lens://4f91…"), app: evmAddress("0x1234…"), }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Finally, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. ## Update App Feeds To update the custom feeds of an existing app, follow these steps. You MUST be authenticated as [Builder](../authentication) and be owner or admin of the App to update its feeds. ### Update the App Feeds First, update the custom feeds in an app using `addAppFeeds` or `removeAppFeeds` actions. ```ts filename="Add Custom Feeds" import { evmAddress } from "@lens-protocol/client"; import { addAppFeeds } from "@lens-protocol/client/actions"; // … const result = await addAppFeeds(sessionClient, { feeds: [evmAddress("0x4546…")], app: evmAddress("0x1234…"), }); ``` ```ts filename="Remove Custom Feeds" import { evmAddress } from "@lens-protocol/client"; import { removeAppFeeds } from "@lens-protocol/client/actions"; // … const result = await removeAppFeeds(sessionClient, { feeds: [evmAddress("0x4546…")], app: evmAddress("0x1234…"), }); ``` First, update the custom feeds in an app using `addAppFeeds` or `removeAppFeeds` mutations. ```graphql filename="Add Custom Feeds Mutation" mutation { addAppFeeds(request: { feeds: ["0x1234…"], app: "0x1234…" }) { ... on SponsoredTransactionRequest { SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ```graphql filename="Remove Custom Feeds Mutation" mutation { removeAppFeeds(request: { feeds: ["0x1234…"], app: "0x1234…" }) { ... on SponsoredTransactionRequest { SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ### Handle Result Then, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await addAppFeeds(sessionClient, { feeds: ['0x4567…'] app: evmAddress("0x1234…") }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await removeAppFeeds(sessionClient, { feeds: ['0x4567…'] app: evmAddress("0x1234…") }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Then, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. ## Set App Default Feed To set the default feed for an app, follow these steps. You MUST be authenticated as [Builder](../authentication) and be owner or admin of the App to set its default feed. ### Update App Default Feed First, use `setDefaultAppFeed` action to set the default feed of an app. ```ts filename="Set Custom Default Feed" import { evmAddress } from "@lens-protocol/client"; import { setDefaultAppFeed } from "@lens-protocol/client/actions"; // … const result = await setDefaultAppFeed(sessionClient, { feed: { custom: evmAddress("0x4546…") }, app: evmAddress("0x1234…"), }); ``` ```ts filename="Set Global Feed as Default" import { evmAddress } from "@lens-protocol/client"; import { setDefaultAppFeed } from "@lens-protocol/client/actions"; // … const result = await setDefaultAppFeed(sessionClient, { feed: { globalFeed: true }, app: evmAddress("0x1234…"), }); ``` ```ts filename="Remove Default Feed" import { evmAddress } from "@lens-protocol/client"; import { setDefaultAppFeed } from "@lens-protocol/client/actions"; // … const result = await setDefaultAppFeed(sessionClient, { feed: { none: true }, app: evmAddress("0x1234…"), }); ``` First, use `setDefaultAppFeed` mutation to set the default feed of an app. ```graphql filename="Mutation" mutation { setDefaultAppFeed( request: { feed: { # set global feed { globalFeed: true } # or set custom feed address # { # custom: EvmAddress # } # or remove default feed # { # none: true # } }, app: "0x1234…" } ) { ... on SponsoredTransactionRequest { SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ### Handle Result Then, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await setDefaultAppFeed(sessionClient, { feeds: { global: true }, app: evmAddress("0x1234…"), }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await setDefaultAppFeed(sessionClient, { feeds: { global: true }, app: evmAddress("0x1234…"), }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Then, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. ## Update App Graph To update the graph of an existing app, follow these steps. You MUST be authenticated as [Builder](../authentication) and be owner or admin of the App to update its graph. ### Set New Custom App Graph First, use `setAppGraph` action to update or set the graph of an existing app. ```ts filename="Set Custom Graph" import { evmAddress } from "@lens-protocol/client"; import { setAppGraph } from "@lens-protocol/client/actions"; // … const result = await setAppGraph(sessionClient, { graph: { custom: evmAddress("0x1234…") }, app: evmAddress("0x1234…"), }); ``` ```ts filename="Set Global Graph" import { evmAddress } from "@lens-protocol/client"; import { setAppGraph } from "@lens-protocol/client/actions"; // … const result = await setAppGraph(sessionClient, { graph: { globalGraph: true }, app: evmAddress("0x1234…"), }); ``` ```ts filename="Remove Graph" import { evmAddress } from "@lens-protocol/client"; import { setAppGraph } from "@lens-protocol/client/actions"; // … const result = await setAppGraph(sessionClient, { graph: { none: true }, app: evmAddress("0x1234…"), }); ``` First, use `setAppGraph` mutation to update or set the metadata of an existing app. ```graphql filename="Mutation" mutation { setAppGraph( request: { graph: { # set global graph { globalGraph: true } # or set custom graph address # { # custom: EvmAddress # } # or remove graph # { # none: true # } }, app: "0x1234…" } ) { ... on SponsoredTransactionRequest { SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ### Handle Result Then, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await setAppGraph(sessionClient, { graph: { globalGraph: true }, app: evmAddress("0x1234…"), }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await setAppGraph(sessionClient, { graph: { globalGraph: true }, app: evmAddress("0x1234…"), }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Then, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. ## App Treasury To update the treasury of an existing app, follow these steps. You MUST be authenticated as [Builder](../authentication) and be owner or admin of the App to update its treasury. ### Update App Treasury First, use `setAppTreasury` action to update or set the treasury of an existing app. ```ts filename="Example" import { evmAddress } from "@lens-protocol/client"; import { setAppTreasury } from "@lens-protocol/client/actions"; // … const result = await setAppTreasury(sessionClient, { treasury: evmAddress('0x4567…') app: evmAddress('0x1234…') }); ``` First, use `setAppTreasury` mutation to update or set the treasury of an existing app. ```graphql filename="Mutation" mutation { setAppTreasury(request: { treasury: "0x4567…", app: "0x1234…" }) { ... on SponsoredTransactionRequest { SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ### Handle Result Then, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await setAppTreasury(sessionClient, { treasury: evmAddress("0x4567…"), app: evmAddress("0x1234…"), }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await setAppTreasury(sessionClient, { treasury: evmAddress("0x4567…"), app: evmAddress("0x1234…"), }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Then, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. ## App Sponsorship To update the sponsorship of an existing app, follow these steps. You MUST be authenticated as [Builder](../authentication) and be owner or admin of the App to update its sponsorship. ### Update App Sponsorship First, use `setAppSponsorship` action to update or set the sponsorship of an existing app. ```ts filename="Example" import { evmAddress } from "@lens-protocol/client"; import { setAppSponsorship } from "@lens-protocol/client/actions"; // … const result = await setAppSponsorship(sessionClient, { app: evmAddress("0x1234…"), sponsorship: evmAddress("0x4567…"), }); ``` First, use `setAppSponsorship` mutation to update or set the sponsorship of an existing app. ```graphql filename="Mutation" mutation { setAppSponsorship(request: { sponsorship: "0x4567…", app: "0x1234…" }) { ... on SponsoredTransactionRequest { SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ### Handle Result Then, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await setAppSponsorship(sessionClient, { sponsorship: evmAddress("0x4567…"), app: evmAddress("0x1234…"), }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await setAppSponsorship(sessionClient, { sponsorship: evmAddress("0x4567…"), app: evmAddress("0x1234…"), }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Then, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. ## Group Management App Groups allow you to organize initiatives or projects within your application. ## Fetch App Groups Use the `fetchAppGroups` action to fetch a list of groups using an App. ```ts filename="Example" import { evmAddress } from "@lens-protocol/client"; import { fetchAppGroups } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchAppGroups(client, { app: evmAddress("0x1234…"), }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; export const client = PublicClient.create({ environment: mainnet, }); ``` Use the paginated `appGroups` query to fetch a list of groups using an App. ```graphql filename="Query" query { appGroups( request: { # required, app address app: "0x1234…" # optional, number of items per page (default: FIFTY) # pageSize: TEN # other option is FIFTY # optional, cursor to start from # cursor: "0x1234…" } ) { items { address owner timestamp metadata { id name icon coverPicture description } } pageInfo { prev next } } } ``` ```json filename="Response" { "data": { "appGroups": { "items": [ { "address": "0x1234…", "owner": "0x5678…", "timestamp": "2024-12-22T21:15:44+00:00", "metadata": { "id": "0x1234…", "name": "Group Name", "icon": "https://example.com/icon.jpg", "coverPicture": "https://example.com/cover.jpg", "description": "Group Description" } } ], "pageInfo": { "prev": null, "next": null } } } } ``` Coming soon See the [Pagination](../best-practices/pagination) guide for more information on how to handle paginated results. --- ## Add App Groups To add groups to your app, follow these steps. You MUST be authenticated as [Builder](../authentication) and be the owner or admin of the App to add groups. ### Add Groups to App Use the `addAppGroups` action to add groups to an app. ```ts filename="Example" import { evmAddress } from "@lens-protocol/client"; import { addAppGroups } from "@lens-protocol/client/actions"; const result = await addAppGroups(sessionClient, { app: evmAddress("0x1234…"), groups: [evmAddress("0x5678…"), evmAddress("0x9012…")], }); if (result.isErr()) { return console.error(result.error); } ``` Use the `addAppGroups` mutation to add groups to an app. ```graphql filename="Mutation" mutation { addAppGroups(request: { app: "0x1234…", groups: ["0x5678…", "0x9012…"] }) { ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ### Handle Result Finally, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,9" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await addAppGroups(sessionClient, { app: evmAddress("0x1234…"), groups: [evmAddress("0x5678…"), evmAddress("0x9012…")], }) .andThen(handleOperationWith(walletClient)) .andThen(sessionClient.waitForTransaction); ``` ```ts filename="ethers" highlight="1,9" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await addAppGroups(sessionClient, { app: evmAddress("0x1234…"), groups: [evmAddress("0x5678…"), evmAddress("0x9012…")], }) .andThen(handleOperationWith(signer)) .andThen(sessionClient.waitForTransaction); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Finally, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. --- ## Remove App Groups To remove groups from your app, follow these steps. You MUST be authenticated as [Builder](../authentication) and be the owner or admin of the App to remove groups. ### Remove Groups from App Use the `removeAppGroups` action to remove groups from an app. ```ts filename="Example" import { evmAddress } from "@lens-protocol/client"; import { removeAppGroups } from "@lens-protocol/client/actions"; const result = await removeAppGroups(sessionClient, { app: evmAddress("0x1234…"), groups: [evmAddress("0x5678…"), evmAddress("0x9012…")], }); if (result.isErr()) { return console.error(result.error); } ``` Use the `removeAppGroups` mutation to remove groups from an app. ```graphql filename="Mutation" mutation { removeAppGroups(request: { app: "0x1234…", groups: ["0x5678…", "0x9012…"] }) { ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ### Handle Result Finally, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,9" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await removeAppGroups(sessionClient, { app: evmAddress("0x1234…"), groups: [evmAddress("0x5678…"), evmAddress("0x9012…")], }) .andThen(handleOperationWith(walletClient)) .andThen(sessionClient.waitForTransaction); ``` ```ts filename="ethers" highlight="1,9" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await removeAppGroups(sessionClient, { app: evmAddress("0x1234…"), groups: [evmAddress("0x5678…"), evmAddress("0x9012…")], }) .andThen(handleOperationWith(signer)) .andThen(sessionClient.waitForTransaction); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Finally, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. --- See the [Groups](../groups) section for more information on creating and managing individual groups. ## Access Control The App contract supports two roles: _Owner_ and _Administrator_. Administrators can: - Update the App Metadata - Update the App Rules - Update the App Feeds and Graph - Update the App Treasury - Update the App Sponsorship The Owner can do everything the administrators can do, plus transfer ownership of the App to another address. See the [Team Management](../best-practices/team-management) guide for more information on how to manage these roles. ================ File: src/pages/protocol/feeds/custom-feeds.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Custom Feeds This guide will introduce the concept of Custom Feeds and how to create and manage them. --- As mentioned in the [Feed](../../concepts/feed) concept page, there are two classes of Feed instances: - **The Global Feed**: The familiar shared feed that aggregates all public Lens activity. - **Custom Feeds**: App or group-specific feeds that can be open or governed by [Feed Rules](./feed-rules). ## Create a Custom Feed To create a Custom Feed, follow these steps. You MUST be authenticated as [Builder](../authentication) to create a Feed. ### Create Metadata First, construct a Feed Metadata object with the necessary content. Use the `@lens-protocol/metadata` package to construct a valid `FeedMetadata` object: ```ts filename="Example" import { feed } from "@lens-protocol/metadata"; const metadata = feed({ name: "XYZ", description: "My custom feed description", }); ``` If you opted to manually create Metadata objects, make sure they conform to the [Feed Metadata JSON Schema](https://json-schemas.lens.dev/feed/1.0.0.json). ```json filename="Example" { "$schema": "https://json-schemas.lens.dev/feed/1.0.0.json", "lens": { "id": "6418053f-89ae-4f9e-8362-8d1b6c0cdafb", "name": "XYZ", "description": "My custom field description" } } ``` ### Upload Metadata Next, upload the Feed Metadata object to a public URI. ```ts import { storageClient } from "./storage-client"; const { uri } = await storageClient.uploadAsJson(metadata); console.log(uri); // e.g., lens://4f91ca… ``` This example uses [Grove storage](../../storage) to host the Metadata object. See the [Lens Metadata Standards](../best-practices/metadata-standards#host-metadata-objects) guide for more information on hosting Metadata objects. ### Deploy Feed Contract Next, deploy the Lens Feed smart contract. Use the `createFeed` action to deploy the Lens Feed smart contract. ```ts filename="Simple Feed" import { uri } from "@lens-protocol/client"; import { createFeed } from "@lens-protocol/client/actions"; const result = await createFeed(sessionClient, { metadataUri: uri("lens://4f91ca…"), }); ``` ```ts filename="Feed with Admins" import { evmAddress, uri } from "@lens-protocol/client"; import { createFeed } from "@lens-protocol/client/actions"; const result = await createFeed(sessionClient, { admins: [evmAddress("0x5071DeEcD24EBFA6161107e9a875855bF79f7b21")], metadataUri: uri("lens://4f91ca…"), }); ``` ```ts filename="Feed with Rules" import { evmAddress, uri } from "@lens-protocol/client"; import { createFeed } from "@lens-protocol/client/actions"; const result = await createFeed(sessionClient, { metadataUri: uri("lens://4f91ca…"), rules: { required: [ { groupGatedRule: { group: evmAddress("0x1234…"), }, }, ], }, }); ``` Use the `createFeed` mutation to deploy the Lens Feed smart contract. ```graphql filename="Simple Feed" mutation { createFeed(request: { metadataUri: "lens://4f91cab87ab5e4f5066f878b72…" }) { ... on CreateFeedResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ```graphql filename="Feed with Admins" mutation { createFeed( request: { metadataUri: "lens://4f91cab87ab5e4f5066f878b72…" admins: ["0x5071DeEcD24EBFA6161107e9a875855bF79f7b21"] } ) { ... on CreateFeedResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ```graphql filename="Feed with Rules" mutation { createFeed( request: { metadataUri: "lens://4f91cab87ab5e4f5066f878b72…" rules: { required: [{ groupGatedRule: { group: "0x1234…" } }] } } ) { ... on CreateFeedResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ```json filename="CreateFeedResponse" { "data": { "createFeed": { "hash": "0x…" } } } ``` Coming soon To learn more about how to use Feed Rules, see the [Feed Rules](./feed-rules) guide. ### Handle Result Next, handle the result using the adapter for the library of your choice and wait for it to be indexed. ```ts filename="viem" highlight="1,8,9" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await createFeed(sessionClient, { metadataUri: uri("lens://4f91ca…"), }) .andThen(handleOperationWith(walletClient)) .andThen(sessionClient.waitForTransaction); ``` ```ts filename="ethers" highlight="1,8,9" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await createFeed(sessionClient, { metadataUri: uri("lens://4f91ca…"), }) .andThen(handleOperationWith(signer)) .andThen(sessionClient.waitForTransaction); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Finally, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Coming soon ### Fetch New Feed Finally, fetch the newly created Feed using the `fetchFeed` action. ```ts filename="viem" highlight="1,10" import { fetchFeed } from "@lens-protocol/client/actions"; // … const result = await createFeed(sessionClient, { metadataUri: uri("lens://4f91…"), // the URI from the previous step }) .andThen(handleOperationWith(walletClientOrSigner)) .andThen(sessionClient.waitForTransaction) .andThen((txHash) => fetchFeed(sessionClient, { txHash })); if (result.isErr()) { return console.error(result.error); } // feed: Feed | null const feed = result.value; ``` Finally, fetch the newly created Feed using the `feed` query. ```graphql filename="Query" query { feed(request: { txHash: "0x1234…" }) { address createdAt owner metadata { name description } } } ``` ```json filename="Response" { "data": { "feed": { "address": "0x1234…", "createdAt": "2021-09-01T00:00:00Z", "metadata": { "name": "XYZ", "description": "My custom feed description" }, "owner": "0x1234…" } } } ``` ## Fetch a Feed Use the `fetchFeed` action to fetch a single Feed by address or by transaction hash. ```ts filename="By Address" import { evmAddress } from "@lens-protocol/client"; import { fetchFeed } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchFeed(client, { feed: evmAddress("0x1234…"), }); if (result.isErr()) { return console.error(result.error); } const feed = result.value; ``` ```ts filename="By Tx Hash" import { txHash } from "@lens-protocol/client"; import { fetchFeed } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchFeed(client, { txHash: txHash("0x1234…"), }); if (result.isErr()) { return console.error(result.error); } const feed = result.value; ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; export const client = PublicClient.create({ environment: mainnet, }); ``` Use the `feed` query to fetch a single Feed by address or by transaction hash. ```graphql filename="Query" query { feed( request: { feed: "0xdeadbeef…" # OR # txHash: TxHash! } ) { address createdAt owner metadata { id name description } # other fields such as feed rules # will be documented in due course } } ``` ```json filename="Response" { "data": { "feed": { "address": "0xdeadbeef…", "createdAt": "2021-09-01T00:00:00Z", "owner": "0x1234…", "metadata": { "id": "6418053f-89ae-4f9e-8362-8d1b6c0cdafb", "name": "XYZ", "description": "My custom feed description" } } } } ``` Coming soon ## Search Feeds Use the `useFeeds` hook to fetch a list of Lens Feeds. ```tsx filename="With Loading" const { data, loading, error } = useFeeds(request); ``` ```tsx filename="With Suspense" const { data, error } = useFeeds({ suspense: true, ...request }); ``` Feeds can be fetched by search query, app address, or managed by. ```ts filename="Search By Feed Name" import { useFeeds } from "@lens-protocol/react"; // … const { data, loading, error } = useFeeds({ filter: { searchBy: "feedName", }, }); if (loading) { return

Loading…

; } if (error) { return

{error.message}

; } // data: Array ``` ```ts filename="By App" import { evmAddress, useFeeds } from "@lens-protocol/react"; // … const { data, loading, error } = useFeeds({ filter: { app: evmAddress("0x1234…"), }, }); if (loading) { return

Loading…

; } if (error) { return

{error.message}

; } // data: Array ``` ```ts filename="Managed by an Address" import { evmAddress, useFeeds } from "@lens-protocol/react"; // … const { data, loading, error } = useFeeds({ filter: { managedBy: { includeOwners: true, // optional address: evmAddress("0x1234…"), }, }, }); if (loading) { return

Loading…

; } if (error) { return

{error.message}

; } // data: Array ```
Use the paginated `fetchFeeds` action to search for feeds. ```ts filename="Search By Feed Name" import { fetchFeeds } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchFeeds(client, { filter: { searchBy: "feedName", }, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="Managed by an Address" import { evmAddress } from "@lens-protocol/client"; import { fetchFeeds } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchFeeds(client, { filter: { managedBy: { includeOwners: true, // optional address: evmAddress("0x1234…"), }, }, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="By App" import { evmAddress } from "@lens-protocol/client"; import { fetchFeeds } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchFeeds(client, { filter: { app: evmAddress("0x1234…"), }, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; export const client = PublicClient.create({ environment: mainnet, }); ``` Use the paginated `feeds` query to search for feeds. ```graphql filename="Query" query { feeds( request: { filter: { searchBy: 'feedName' # OPTIONAL # managedBy: { # includeOwners: true # optional # address: "0x1234…" # } # OR app # app: "0x1234…" } orderBy: LATEST_FIRST # other options: ALPHABETICAL, OLDEST_FIRST } ) { items { address createdAt owner metadata { id name description } # other fields such as feed rules # will be documented in due course } pageInfo { prev next } } } ``` ```json filename="Response" { "data": { "feeds": { "items": [ { "address": "0xdeadbeef…", "createdAt": "2021-09-01T00:00:00Z", "owner": "0x1234…", "metadata": { "id": "6418053f-89ae-4f9e-8362-8d1b6c0cdafb", "name": "XYZ", "description": "My custom feed description" } } ], "pageInfo": { "prev": null, "next": "U29mdHdhcmU=" } } } } ```
Continue with the [Pagination](../best-practices/pagination) guide for more information on how to handle paginated results. ## Update Feed Metadata To update a Feed Metadata, follow these steps. You MUST be authenticated as [Builder](../authentication), [Account Manager](../authentication), or [Account Owner](../authentication) and be either the owner or an admin of the Feed you intend to update. ### Create New Metadata First, create a new Feed Metadata object with the updated details. It's the developer's responsibility to copy over any existing data that should be retained. The process is similar to the one in the [Create a Feed](#create-a-custom-feed) guide, so we will keep this example brief. ```ts filename="Example" import { feed } from "@lens-protocol/metadata"; const metadata = feed({ name: "XYZ", description: "My feed description", }); ``` ### Upload Metadata Next, upload the Feed Metadata object to a public URI. ```ts filename="Upload Metadata" import { storageClient } from "./storage-client"; // … const { uri } = await storageClient.uploadAsJson(metadata); console.log(uri); // e.g., lens://4f91ca… ``` If [Grove storage](../../storage) was used, you can also decide to edit the file at the existing URI. See the [Editing Content](../../storage/usage/edit) guide for more information. ### Update Metadata URI Next, update the Feed metadata URI with the new URI. Use the `setFeedMetadata` action to update the Feed Metadata URI. ```ts import { uri } from "@lens-protocol/client"; import { setFeedMetadata } from "@lens-protocol/client/actions"; const result = await setFeedMetadata(sessionClient, { feed: feed.address, metadataUri: uri("lens://4f91ca…"), }); if (result.isErr()) { return console.error(result.error); } ``` Use the `setFeedMetadata` mutation to update the Feed Metadata URI. ```graphql filename="Mutation" mutation { setFeedMetadata(request: { feed: "0x1234…", metadataUri: "lens://4f91ca…" }) { ... on SetFeedMetadataResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` Coming soon ### Handle Result Finally, handle the result using the adapter for the library of your choice: ```ts filename="viem" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/viem"; // … const result = await setFeedMetadata(sessionClient, { feed: feed.address, metadataUri: uri("lens://4f91ca…"), }).andThen(handleOperationWith(walletClient)); ``` ```ts filename="ethers" highlight="1,8" import { handleOperationWith } from "@lens-protocol/client/ethers"; // … const result = await setFeedMetadata(sessionClient, { feed: feed.address, metadataUri: uri("lens://4f91ca…"), }).andThen(handleOperationWith(signer)); ``` See the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide for more information on how to determine the status of the transaction. Finally, handle the result as explained in the [Transaction Lifecycle](../best-practices/transaction-lifecycle) guide. Coming soon ## Access Control The Feed contract supports two roles: _Owner_ and _Administrator_. Administrators can: - Update the Feed Metadata - Update the Feed Rules - Update the Feed Extra Data The Owner can do everything the administrators can do, plus transfer ownership of the Feed to another address. See the [Team Management](../best-practices/team-management) guide for more information on how to manage these roles. ================ File: src/pages/protocol/resources/changelog.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: false, showNext: false, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Changelog All notable changes to this project will be documented in this page. --- This page collects any notable changes to the Lens Protocol v3, API, and SDK. {/* ALWAYS KEEP Unreleased at the top */} ## SNS Notifications SNS Notifications now include support for a new notification type: `TokenDistributionSuccess`. ### Token Distribution Success This notification fires when an account is rewarded with tokens. ```graphql filename="Create Token Distribution Success Notification" mutation CreateTokenDistributionSuccessNotification { createSnsSubscriptions( request: { webhook: "https://example.com/webhook", topics: [{ tokenDistributionSuccess: { # Optional, filter events by account # recipient: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by tokens # tokens: ["0x0101010101010101010101010101010101010101"], } }] } ) } ``` ## New Payment Source Option for Collect and Tipping Actions With this release, we added a new `paymentSource` parameter for simple collect and tipping actions on posts and accounts. This parameter accepts two values: - `ACCOUNT` (default) - Maintains backward compatibility with existing implementations - `SIGNER` - Deducts the balance from the authenticated user (account owner or manager, depending on their role) The default value of ACCOUNT ensures that existing integrations continue to work without any changes. When you specify SIGNER, the payment will be processed using the balance of the currently authenticated user based on their role permissions. ```diff filename="Tipping in GHO using Signer Balance" mutation { executePostAction( request: { post: "42", action: { tipping: { native: "100" + paymentSource: "SIGNER" } } } ) { ... on ExecutePostActionResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } + ... on SignerErc20ApprovalRequired { + reason + amount { + ...Erc20Amount + } + } ... on InsufficientFunds { reason } } } ``` Refer to the documentation on implementation guidelines: [Account Actions](../accounts/actions) and [Post Actions](../feeds/post-actions). ## Change to the Can Simple Collection Operation Due to the manager/owner spending we no longer know in advance who will pay for the action therefore `SimpleCollectValidationFailedReason` no longer return `NOT_ENOUGH_BALANCE` error reason. ## Native Token Support in Account and Post Actions ### Post Simple Collect Action When configuring a post simple collect action with price, the user can now specify either `native` or `erc20` token. ```diff filename="Paid Collect in GHO" mutation { post( request: { contentUri: "lens://4f91ca…" actions: [ { simpleCollect: { payToCollect: { + native: "5.25" recipients: [ { address: "0x5678…", percent: 30 } { address: "0x9abc…", percent: 70 } ] } } } ] } ) { ...PostResult } } ``` ```diff filename="Paid Collect in ERC20" mutation { post( request: { contentUri: "lens://4f91ca…" actions: [ { simpleCollect: { payToCollect: { // `amount` is deprecated in favor of the `erc20` - amount: { currency: "0x1234…", value: "42.42" } + erc20: { currency: "0x1234…", value: "42.42" } recipients: [ { address: "0x5678…", percent: 30 } { address: "0x9abc…", percent: 70 } ] } } } ] } ) { ...PostResult } } ``` `SimpleCollectAction` type returned from `Post.actions` now contains `price` field which is one of `NativeAmount` or `Erc20Amount`. ```diff filename="SimpleCollectAction" { "__typename": "SimpleCollectAction", "payToCollect": { "__typename": "PayToCollectConfig", + "price": { + "__typename": "Erc20Amount", + "asset": { + "__typename": "Erc20", + "name": "Wrapped GHO", + "symbol": "wGHO", + "contract": { + "__typename": "NetworkAddress", + "address": "0x1234…", + "chainId": 37111 + }, + "decimals": 18 + }, + "value": "42.42" + }, // `amount` is deprecated in favor of the `price` - "amount": { - "__typename": "Erc20Amount", - "asset": { - "__typename": "Erc20", - "name": "Wrapped GHO", - "symbol": "wGHO", - "contract": { - "__typename": "NetworkAddress", - "address": "0x1234…", - "chainId": 37111 - }, - "decimals": 18 - }, - "value": "42.42" - }, }, } ``` ### Account/Post tipping actions Similarly to Simple Collect action, tipping action now supports tipping directly in native token. ```diff filename="Tipping in GHO" mutation { executePostAction( + request: { post: "42", action: { tipping: { native: "100" } } } ) { ... on ExecutePostActionResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ```diff filename="Tipping in ERC20" mutation { executePostAction( request: { post: "42" action: { tipping: { - currency: "0x5678…" - value: "100" + erc20: { currency: "0x5678…", value: "100" } } } } ) { ... on ExecutePostActionResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` `LoggedInPostOperations` `lastTip` was updated to expose native token or ERC20 token last tip amount. ```diff filename="LoggedInPostOperations" fragment LoggedInPostOperations on LoggedInPostOperations { lastTip { // `amount` is deprecated in favor of the `tipAmount` - amount { - asset { - name - symbol - } - value - } + tipAmount { + ... on Erc20Amount { + asset { + name + symbol + } + value + } + ... on Erc20Amount { + asset { + name + symbol + } + value + } + } date } } ``` ## SNS Notifications SNS Notifications filters are now accepting arrays of values, instead of a single value. ### Changed When using the `createSnsSubscriptions` mutation, the `topics` field now accepts arrays of values. ```diff filename="Create SNS Subscriptions" mutation { createSnsSubscriptions( request: { webhook: "https://example.com/webhook", topics: [{ postCreated: { - feed: "0x0101010101010101010101010101010101010101", + feed: ["0x0101010101010101010101010101010101010101"], - app: "0x0101010101010101010101010101010101010101", + app: ["0x0101010101010101010101010101010101010101"], } }] } ) } ``` ## Faster App Verification The App Verification (also known as Operation Verification) process, previously relying on a server-to-server call to an app-provided Verification Endpoint, has been replaced with a new approach based on the exchange of App Signer signing keys. This change eliminates the server-to-server call latency that was inherent in the previous verification process. ### Changed The app-provided Authorization Endpoint should return the signing key of an App Signer: ```diff HTTP/1.1 200 OK Content-Type: application/json { "allowed": true, "sponsored": true, - "appVerificationEndpoint": "https://example.com/verify" + "signingKey": "0x1234…" } ``` The Verification Endpoint is no longer required and can be removed from the app's code. See the updated [Authorization Workflows](../apps/authorization-workflows) guide for more details. ## Bugfixes and Improvements Refinements to the GraphQL API and SDK. ### Changed Since the `UsernameReservedNamespaceRule` is applied by default to any Namespace created through the Lens factories, it is no longer necessary to specify it in the `rules` field when creating a Namespace. ```diff filename="Create Namespace" mutation { createUsernameNamespace( request: { symbol: "FOO" namespace: "foo" metadataURI: "lens://4f91ca…" rules: { required: [ - { - usernameReservedRule: { reserved: ["asdfgh"] } - } ] } } ) { ... on CreateNamespaceResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ## Bugfixes and Improvements ### Changed A minor renaming of GraphQL input types has been implemented. This change will only impact you if you use these input types as operation parameters, as demonstrated below. If you are using the Lens SDK, simply update to the latest `@canary` version to accommodate this change. ```diff filename="MlexplorePostsRequest" - query ExploreQuery($request: MlexplorePostsRequest!) { + query ExploreQuery($request: PostsExploreRequest!) { value: mlPostsExplore(request: $request) { __typename items { ...Post } pageInfo { ...PaginatedResultInfo } } } ``` ```diff filename="MLExplorePostsFilter" - query ExploreQuery($filter: MLExplorePostsFilter!) { + query ExploreQuery($filter: PostsExploreFilter!) { value: mlPostsExplore(request: { filter: $filter }) { __typename items { ...Post } pageInfo { ...PaginatedResultInfo } } } ``` ```diff filename="MlpostsForYouRequest" - query PostsForYou($request: MlpostsForYouRequest!) { + query PostsForYou($request: PostsForYouRequest!) { value: mlPostsForYou(request: $request) { __typename items { ...PostForYou } pageInfo { ...PaginatedResultInfo } } } ``` ```diff filename="MlaccountRecommendationsRequest" - query AccountRecommendations($request: MlaccountRecommendationsRequest!) { + query AccountRecommendations($request: AccountRecommendationsRequest!) { value: mlAccountRecommendations(request: $request) { __typename items { ...Account } pageInfo { ...PaginatedResultInfo } } } ``` {/* ### Removed */} ## Prepare for Mainnet ### Stable Lens Metadata Standard The [Lens Metadata Standard package](https://github.com/lens-protocol/metadata) has reached a **stable 2.0 release** — no functional changes, just a transition to a stable version. Update to: ```bash filename="npm" npm install @lens-protocol/metadata@latest ``` ```bash filename="yarn" yarn add @lens-protocol/metadata@latest ``` ```bash filename="pnpm" pnpm add @lens-protocol/metadata@latest ``` to align with the stable 2.x version. ### Updated Lens Chain SDK The dependency on the Lens Chain SDK has been updated to the [latest stable version](../../chain/resources/changelog#2025-04-01-prepare-for-mainnet). Make sure to update to the latest Lens SDK canary and Lens Chain SDK stable version. ```bash filename="npm" npm install @lens-chain/sdk@latest @lens-protocol/client@canary ``` ```bash filename="yarn" yarn add @lens-chain/sdk@latest @lens-protocol/client@canary ``` ```bash filename="pnpm" pnpm add @lens-chain/sdk@latest @lens-protocol/client@canary ``` ## Bugfixes and Improvements This release includes several bug fixes and improvements. ### Changed #### Expose Feed Under Group.feed The `Group.feed` field has been upgraded to a fully-fledged `Feed` object. If you are using the Lens SDK, simply update to the latest version to access the new `feed` object. If you are not using the SDK, update your GraphQL fragments as demonstrated below. ```diff filename="Group" fragment Group on Group { __typename address - feed + feed { + ...Feed + } timestamp owner banningEnabled membershipApprovalEnabled metadata { ...GroupMetadata } rules { ...GroupRules } operations { ...LoggedInGroupOperations } } ``` ```graphql filename="Feed" fragment Feed on Feed { __typename address createdAt metadata { __typename description id name } owner operations { ...LoggedInFeedPostOperations } rules { ...FeedRules } } ``` #### SessionClient#getAuthenticatedUser No Longer Returns a Thenable In the usage below is not a breaking change per-se since any `await` expression implicitly wraps the value in a `Promise`. However, your linter/IDE may flag this as an error. ```diff - const result = await sessionClient.getAuthenticatedUser(); + const result = sessionClient.getAuthenticatedUser(); ``` #### BanMemberGroupRule Applied Automatically The `BanMemberGroupRule` does not require any configuration because it is applied automatically when a Group is created. ```diff filename="TypeScript" import { evmAddress, uri } from "@lens-protocol/client"; import { createGroup } from "@lens-protocol/client/actions"; const result = await createGroup(sessionClient, { metadataUri: uri("lens://4f91c…"), - rules: { - required: [ - { - banAccountRule: { - enable: true, - }, - }, - ], }, }); ``` ```diff filename="GraphQL" mutation { createGroup( request: { metadataUri: "lens://4f91c…" - rules: { required: [{ banAccountRule: { enable: true } }] } } ) { ... on CreateGroupResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ## New Full Protocol Release With this release, we redeployed a completely new protocol and reset all testnet state. Update the latest `@lens-protocol/client@canary` and use the following diffs to update your GQL fragments. ### Changed To create a Post with paid-collect, group the relevant properties under the `payToCollect` property. ```diff filename="TypeScript" import { dateTime, evmAddress, uri } from "@lens-protocol/client"; const result = await post(sessionClient, { contentUri: uri("lens://4f91ca…"), actions: [ { simpleCollect: { - amount: { - currency: evmAddress("0x1234…"), - value: "42.42", - }, - recipients: [ - { - address: evmAddress("0x5678…"), - percent: 30, // 30% - }, - { - address: evmAddress("0x9abc…"), - percent: 70, // 70% - }, - ], - referralShare: 5, // 5% + payToCollect: { + amount: { + currency: evmAddress("0x1234…"), + value: "42.42", + }, + recipients: [ + { + address: evmAddress("0x5678…"), + percent: 30, // 30% + }, + { + address: evmAddress("0x9abc…"), + percent: 70, // 70% + }, + ], + referralShare: 5, // 5% + }, }, }, ], }); ``` ```diff filename="GraphQL" mutation { post( request: { contentUri: "lens://4f91ca…" actions: [ { simpleCollect: { - amount: { currency: "0x1234…", value: "42.42" } - recipients: [ - { address: "0x5678…", percent: 30 } - { address: "0x9abc…", percent: 70 } - ] - referralShare: 5 + payToCollect: { + amount: { currency: "0x1234…", value: "42.42" } + recipients: [ + { address: "0x5678…", percent: 30 } + { address: "0x9abc…", percent: 70 } + ] + referralShare: 5 + } } } ] } ) { ...PostResult } } ``` The `SimpleCollectActionConfig` properties related to paid-collect are now grouped under the `payToCollect` property. ```diff filename="SimpleCollectAction" fragment SimpleCollectAction on SimpleCollectAction { __typename address - amount { - ...Erc20Amount - } - recipients { - ...RecipientPercent - } - referralShare + payToCollect { + __typename + amount { + ...Erc20Amount + } + recipients { + ...RecipientPercent + } + referralShare + } collectLimit followerOnGraph { ...FollowerOn } endsAt isImmutable collectNftAddress } ``` ## Image Resizing and Original URIs This release introduces changes that allow resizing of Post, Account, and other metadata image URLs, as well as the option to retrieve the original Lens URIs for media URLs. Additionally, it includes various bug fixes and improvements. ### Changed `UnknownAction` got split into `UnknownAccountAction` and `UnknownPostAction`. ```diff filename="UnknownAction" fragment AccountAction on AccountAction { __typename ... on TippingAccountAction { ...TippingAccountAction } ... on UnknownAccountAction { - ...UnknownAction + ...UnknownAccountAction } } + fragment UnknownAccountAction on UnknownAccountAction { + __typename + address + config { + ...AnyKeyValue + } + metadata { + ...ActionMetadata + } + } fragment PostAction on PostAction { ... on SimpleCollectAction { ...SimpleCollectAction } ... on UnknownPostAction { - ...UnknownAction + ...UnknownPostAction } } + fragment UnknownPostAction on UnknownPostAction { + __typename + address + config { + ...AnyKeyValue + } + metadata { + ...ActionMetadata + } + } - fragment UnknownAction on UnknownAction { - __typename - address - config { - ...AnyKeyValue - } - metadata { - ...ActionMetadata - } - } ``` Some `EventMetadata` fields got renamed for consistency with other metadata objects. ```diff filename="EventMetadata" fragment EventMetadata on EventMetadata { … location { - ...EventMetadataLensLocation + ...EventLocation } … schedulingAdjustments { - ...EventMetadataLensSchedulingAdjustments + ...EventSchedulingAdjustments } … } - fragment EventMetadataLensLocation on EventMetadataLensLocation { + fragment EventLocation on EventLocation { __typename physical virtual } - fragment EventMetadataLensSchedulingAdjustments on EventMetadataLensSchedulingAdjustments { + fragment EventSchedulingAdjustments on EventSchedulingAdjustments { __typename timezoneId timezoneOffset } ``` ### Changed Removed redundant `title` field from Graph, Feed, and Action metadata objects. Use `name` field instead. - Update the `@lens-protocol/metadata@next` to get the correct builder functions. - Update the `@lens-protocol/client@canary` or use the following diffs to update your GQL fragments. ```diff filename="GraphMetadata" fragment GraphMetadata on GraphMetadata { __typename description id name - title } ``` ```diff filename="FeedMetadata" fragment FeedMetadata on FeedMetadata { __typename description id name - title } ``` ```diff filename="ActionMetadata" fragment ActionMetadata on ActionMetadata { __typename id name - title source authors configureParams { ...KeyValuePair } description executeParams { ...KeyValuePair } setDisabledParams { ...KeyValuePair } } ``` ## Relocate Lens Chain SDK The Lens Chain SDK has been relocated under: `@lens-chain/sdk`. Since it's a peer dependency fo the `@lens-protocol/client`, you need to update the package in your project. ```bash filename="npm" npm uninstall @lens-network/sdk npm install @lens-chain/sdk@canary ``` ```bash filename="yarn" yarn remove @lens-network/sdk yarn add @lens-chain/sdk@canary ``` ```bash filename="pnpm" pnpm remove @lens-network/sdk pnpm add @lens-chain/sdk@canary ``` ## Bugfixes and Improvements ### Changed Fix to structural typing mismatch of `AnyKeyValue` union. Update to the latest `@lens-protocol/client@canary` to get the latest types. ```diff filename="ArrayKeyValue" type ArrayKeyValue = { __typename: "ArrayKeyValue"; key: string; + array: - value: | IntKeyValue | IntNullableKeyValue | AddressKeyValue | StringKeyValue | BooleanKeyValue | RawKeyValue | BigDecimalKeyValue | DictionaryKeyValue; }; ``` ```diff filename="DictionaryKeyValue" type DictionaryKeyValue = { __typename: "DictionaryKeyValue"; key: string; + dictionary: - value: | IntKeyValue | IntNullableKeyValue | AddressKeyValue | StringKeyValue | BooleanKeyValue | RawKeyValue | BigDecimalKeyValue; }; ``` ```diff filename="Others" type IntKeyValue = { __typename: "IntKeyValue"; key: string; + int: number; - value: number; }; type IntNullableKeyValue = { __typename: "IntNullableKeyValue"; key: string; + optionalInt: number | null; - value: number | null; }; type AddressKeyValue = { __typename: "AddressKeyValue"; key: string; + address: EvmAddress; - value: EvmAddress; }; type StringKeyValue = { __typename: "StringKeyValue"; key: string; + string: string; - value: string; }; type BooleanKeyValue = { __typename: "BooleanKeyValue"; key: string; + boolean: boolean; - value: boolean; }; type RawKeyValue = { __typename: "RawKeyValue"; key: string; + data: BlockchainData; - value: BlockchainData; }; type BigDecimalKeyValue = { __typename: "BigDecimalKeyValue"; key: string; + bigDecimal: BigDecimal; - value: BigDecimal; }; ``` ```diff filename="ArrayKeyValue" fragment ArrayKeyValue on ArrayKeyValue { __typename key + array { - value { ... on IntKeyValue { ...IntKeyValue } ... on IntNullableKeyValue { ...IntNullableKeyValue } ... on AddressKeyValue { ...AddressKeyValue } ... on StringKeyValue { ...StringKeyValue } ... on BooleanKeyValue { ...BooleanKeyValue } ... on RawKeyValue { ...RawKeyValue } ... on BigDecimalKeyValue { ...BigDecimalKeyValue } ... on DictionaryKeyValue { ...DictionaryKeyValue } } } ``` ```diff filename="DictionaryKeyValue" fragment DictionaryKeyValue on DictionaryKeyValue { __typename key + dictionary { - value { ... on IntKeyValue { ...IntKeyValue } ... on IntNullableKeyValue { ...IntNullableKeyValue } ... on AddressKeyValue { ...AddressKeyValue } ... on StringKeyValue { ...StringKeyValue } ... on BooleanKeyValue { ...BooleanKeyValue } ... on RawKeyValue { ...RawKeyValue } ... on BigDecimalKeyValue { ...BigDecimalKeyValue } } } ``` ```diff filename="Others" fragment IntKeyValue on IntKeyValue { __typename key + int - value } fragment IntNullableKeyValue on IntNullableKeyValue { __typename key + optionalInt - value } fragment AddressKeyValue on AddressKeyValue { __typename key + address - value } fragment StringKeyValue on StringKeyValue { __typename key + string - value } fragment BooleanKeyValue on BooleanKeyValue { __typename key + boolean - value } fragment RawKeyValue on RawKeyValue { __typename key + data - value } fragment BigDecimalKeyValue on BigDecimalKeyValue { __typename key + bigDecimal - value } ``` {/* ### Removed */} ## Bugfixes and Improvements ### Added - New `whoExecutedActionOnPost` query (`fetchWhoExecutedActionOnPost` SDK action). - New `Post.contentUri` field surfaces the original Content URI of the Post. ### Changed #### TimelineItem fix The `TimelineItem.reposts` was incorrectly returning array of `Post`, instead it should return array of `Repost`. ```diff filename="TimelineItem" fragment TimelineItem on TimelineItem { __typename id primary { ...Post } comments { ...Post } reposts { - ...Post + ...Repost } } ``` #### Simple Collect Action Prepare Simple Collect Action GQL to support multi-recipient. Support at contract level coming soon. ```diff filename="Execute Simple Collect Action" const result = await executePostAction(sessionClient, { post: postId("42"), action: { - simpleCollect: true, + simpleCollect: { + selected: true, + }, }, }); ``` ```diff filename="Execute Mutation" mutation { executePostAction(request: { post: "42", action: { - simpleCollect: true + simpleCollect: { + selected: true + } } }) { ... on ExecutePostActionResponse { hash } ... on SponsoredTransactionRequest { ...SponsoredTransactionRequest } ... on SelfFundedTransactionRequest { ...SelfFundedTransactionRequest } ... on TransactionWillFail { reason } } } ``` ```diff filename="SimpleCollectAction" type SimpleCollectAction { address: EvmAddress! amount: Erc20Amount - recipient: EvmAddress + recipients: [RecipientPercent!] + referralShare: Int collectLimit: Int followerOnGraph: FollowerOn endsAt: DateTime isImmutable: Boolean! } ``` #### Post Actions The `whoActedOnPost` query (`fetchWhoActedOnPost` SDK action) is now `whoExecutedActionOnAccount` (`fetchWhoExecutedActionOnAccount` SDK action). {/* ### Removed */} ## Actions, Rules, and Sponsorships ### Added - Follow Rules - Feed Rules - Graph Rules - Username Namespace Rules - Group Rules - Post Rules - Post Actions - Account Actions - Sponsorship support: apps can now sponsor their own users ### Changed - Logged-in operations objects: - `Post.operations.canEdit`, `Post.operations.canDelete`, `Post.operations.canComment`, `Post.operations.canQuote`, `Post.operations.canRepost` now returns a new `PostOperationValidationOutcome` union with all the details regarding rules validation. - `Account.operations.canFollow` and `Account.operations.canUnfollow` now returns a new `AccountFollowOperationValidationOutcome` union with all the details regarding rules validation. - Paginated query `postActions` got renamed into `postActionContracts` and the return type is now consolidated, including `ActionMetatada` for unknown Post Actions. Routine release with several bug fixes and improvements. ### Changed `NestedPost` and `PostReference` nodes got removed. ```diff fragment Post on Post { id author { ...Account } metadata { ...PostMetadata } root { - ...NestedPost + ...ReferencedPost } quoteOf { - ...NestedPost + ...ReferencedPost } commentOn { - ...NestedPost + ...ReferencedPost } stats { ...PostStats } } ``` where: ```graphql filename="ReferencedPost" fragment ReferencedPost on Post { id author { ...Account } metadata { ...PostMetadata } # root, quoteOf, commentOn omitted to avoid circular references } ``` ## Lens API Testnet Lens API Testnet is now available for developers to start building and testing their applications. ### Changed #### Explicit Authentication Roles The `challenge` mutation now requires explicit authentication roles to be passed in the request. ```diff filename="Account Owner" mutation { challenge( request: { - app: "" - account: "" - signedBy: "" + accountOwner: { + app: "" + account: "" + owner: "" + } } ) { __typename id text } } ``` ```diff filename="Account Manager" mutation { challenge( request: { - app: "" - account: "" - signedBy: "" + accountManager: { + app: "" + account: "" + manager: "" + } } ) { __typename id text } } ``` ```diff filename="Onboarding User" mutation { challenge( request: { - app: "" - account: "" - signedBy: "" + onboardingUser: { + app: "" + wallet: "" + } } ) { __typename id text } } ``` ```diff filename="Builder" mutation { challenge( request: { - account: "" - signedBy: "" - builder: true + builder: { + address: "" + } } ) { __typename id text } } ``` ## Developer Preview Announcement ================ File: src/pages/protocol/tools/sns-notifications.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # SNS Notifications This guide will help you set up real-time push notification using Amazon SNS. --- Lens utilizes [Amazon Simple Notification Service](https://aws.amazon.com/sns/) (SNS) to push notification events, enabling easy integration for third-party providers. This service broadcasts data from the chain to your server based on the filters you apply when setting up the subscription. ## Before you get started Before setting up any subscriptions, you will need to have a webhook endpoint deployed and publicly accessible. This endpoint will be used to confirm the subscription and subsequently receive the notifications from Amazon SNS, so it should be able to handle POST requests and have a high uptime. It is highly recommended to serve this endpoint over a secure HTTPS connection, but both HTTP and HTTPS are supported. Once you have this set up, you can proceed to the next step. You can find examples below on how to create simple webhook using [Express.js](https://expressjs.com/) in TypeScript. ```ts import bodyParser from "body-parser"; import express from "express"; import fetch from "node-fetch"; const app = express(); const port = 8080; app.use(bodyParser.urlencoded({ extended: false })); app.use(bodyParser.json()); app.post("/lens/notifications", async (req, res) => { const buffers = []; for await (const chunk of req) { buffers.push(chunk); } const data = Buffer.concat(buffers).toString(); // example https://docs.aws.amazon.com/connect/latest/adminguide/sns-payload.html const payload = JSON.parse(data); // if you already done the handshake you will get a Notification type // example below: https://docs.aws.amazon.com/sns/latest/dg/sns-message-and-json-formats.html // { // "Type" : "Notification", // "MessageId" : "22b80b92-fdea-4c2c-8f9d-bdfb0c7bf324", // "TopicArn" : "arn:aws:sns:us-west-2:123456789012:MyTopic", // "Subject" : "My First Message", // "Message" : "Hello world!", // "Timestamp" : "2012-05-02T00:54:06.655Z", // "SignatureVersion" : "1", // "Signature" : "EXAMPLEw6JRN…", // "SigningCertURL" : "https://sns.us-west-2.amazonaws.com/SimpleNotificationService-f3ecfb7224c7233fe7bb5f59f96de52f.pem", // "UnsubscribeURL" : "https://sns.us-west-2.amazonaws.com/?Action=Unsubscribe SubscriptionArn=arn:aws:sns:us-west-2:123456789012:MyTopic:c9135db0-26c4-47ec-8998-413945fb5a96" // } if (payload.Type === "Notification") { console.log("SNS message is a notification ", payload); console.log("------------------------------------------------------"); console.log("------------------------------------------------------"); console.log("------------------------------------------------------"); res.sendStatus(200); return; } // only need to do this the first time this is doing an handshake with the sns client // example below: https://docs.aws.amazon.com/sns/latest/dg/sns-message-and-json-formats.html // { // "Type" : "SubscriptionConfirmation", // "MessageId" : "165545c9-2a5c-472c-8df2-7ff2be2b3b1b", // "Token" : "2336412f37…", // "TopicArn" : "arn:aws:sns:us-west-2:123456789012:MyTopic", // "Message" : "You have chosen to subscribe to the topic arn:aws:sns:us-west-2:123456789012:MyTopic.\nTo confirm the subscription, visit the SubscribeURL included in this message.", // "SubscribeURL" : "https://sns.us-west-2.amazonaws.com/?Action=ConfirmSubscription&TopicArn=arn:aws:sns:us-west-2:123456789012:MyTopic&Token=2336412f37…", // "Timestamp" : "2012-04-26T20:45:04.751Z", // "SignatureVersion" : "1", // "Signature" : "EXAMPLEpH+DcEwjAPg8O9mY8dReBSwksfg2S7WKQcikcNKWLQjwu6A4VbeS0QHVCkhRS7fUQvi2egU3N858fiTDN6bkkOxYDVrY0Ad8L10Hs3zH81mtnPk5uvvolIC1CXGu43obcgFxeL3khZl8IKvO61GWB6jI9b5+gLPoBc1Q=", // "SigningCertURL" : "https://sns.us-west-2.amazonaws.com/SimpleNotificationService-f3ecfb7224c7233fe7bb5f59f96de52f.pem" // } if (payload.Type === "SubscriptionConfirmation") { const url = payload.SubscribeURL; const response = await fetch(url); if (response.status === 200) { console.log("Subscription confirmed"); console.log("------------------------------------------------------"); console.log("------------------------------------------------------"); console.log("------------------------------------------------------"); res.sendStatus(200); return; } else { console.error("Subscription failed"); res.sendStatus(500); return; } } console.log("Received message from SNS", payload); // if it gets this far it is a unsubscribe request // { // "Type" : "UnsubscribeConfirmation", // "MessageId" : "47138184-6831-46b8-8f7c-afc488602d7d", // "Token" : "2336412f37…", // "TopicArn" : "arn:aws:sns:us-west-2:123456789012:MyTopic", // "Message" : "You have chosen to deactivate subscription arn:aws:sns:us-west-2:123456789012:MyTopic:2bcfbf39-05c3-41de-beaa-fcfcc21c8f55.\nTo cancel this operation and restore the subscription, visit the SubscribeURL included in this message.", // "SubscribeURL" : "https://sns.us-west-2.amazonaws.com/?Action=ConfirmSubscription&TopicArn=arn:aws:sns:us-west-2:123456789012:MyTopic&Token=2336412f37fb6…", // "Timestamp" : "2012-04-26T20:06:41.581Z", // "SignatureVersion" : "1", // "Signature" : "EXAMPLEHXgJm…", // "SigningCertURL" : "https://sns.us-west-2.amazonaws.com/SimpleNotificationService-f3ecfb7224c7233fe7bb5f59f96de52f.pem" // } }); app.listen(port, () => console.log("SNS notification listening on port " + port + "!"), ); ``` The above are simple demonstrations, any production-ready webhook should include handling for incoming messages as well as errors. ## Subscribing to SNS Topics Once you have the webhook endpoint deployed and accessible, you can now subscribe to Lens SNS notification topics via the Lens API. The example below demonstrates how to create a subscription to the `PostCreated` and `AccountCreated` events using GraphQL. For the mutation to succeed, you need to be authenticated on Lens with a `Builder` role. You can notice that the `postCreated` topic has two optional fields: `feed` and `app`. These fields, if supplied in the request, will configure the SNS subscription to filter the notifications to only those that are originating from a specific feed or app. There are more options available for filtering notifications, which can be found in the reference further down in this article. To highlight the contrast, the `accountCreated` topic does not have any optional filters which means you will receive notifications for all accounts created on Lens. ```graphql filename="GraphQL Mutation" mutation CreateSnsSubscriptions { createSnsSubscriptions( request: { webhook: "https://example.com/webhook", # Replace with your own webhook endpoint topics: [{ postCreated: { feed: ["0x0101010101010101010101010101010101010101"], app: ["0x0101010101010101010101010101010101010101"], }, accountCreated: {} } ] } ) } ``` ```json filename="Response" { "data": { "createSnsSubscriptions": [ { "id": "d0a6e0c0-e6e3-4b8a-9293-14b58392e3e3", "account": "0x0101010101010101010101010101010101010101", "webhook": "https://example.com/webhook", "topic": "PostCreated", "topicArn": "arn:aws:sns:us-west-2:123456789012:post-created", "attributes": { "feed": ["0x0101010101010101010101010101010101010101"], "app": ["0x0101010101010101010101010101010101010101"] } }, { "id": "d0a6e0c0-e6e3-4b8a-9293-14b58392e3e4", "account": "0x0101010101010101010101010101010101010101", "webhook": "https://example.com/webhook", "topic": "AccountCreated", "topicArn": "arn:aws:sns:us-west-2:123456789013:account-created", "attributes": {} } ] } } ``` ```json filename="SNS Confirmation Message" { "Type": "SubscriptionConfirmation", "MessageId": "165545c9-2a5c-472c-8df2-7ff2be2b3b1b", "Token": "2336412f37…", "TopicArn": "arn:aws:sns:us-west-2:123456789012:post-created", "Message": "You have chosen to subscribe to the topic arn:aws:sns:us-west-2:123456789012:post-created.\nTo confirm the subscription, visit the SubscribeURL included in this message.", "SubscribeURL": "https://sns.us-west-2.amazonaws.com/?Action=ConfirmSubscription&TopicArn=arn:aws:sns:us-west-2:123456789012:post-created&Token=2336412f37…", "Timestamp": "2012-04-26T20:45:04.751Z", "SignatureVersion": "1", "Signature": "EXAMPLEpH+DcEwjAPg8O9mY8dReBS…" } ``` A successful call to this mutation will return a list of subscriptions that were created as seen in the Response tab above. For each subscription, you will receive a POST request to the webhook URL you supplied, which will initiate a handshake with Amazon SNS to confirm the subscription. An example of this message is shown above in SNS Confirmation Message, you need to visit the URL that was provided in the `SubscribeURL` field of the response and confirm the subscription. From that point on, you will start receiving notifications for the topics you subscribed to. ## Filtering Every subscription topic offers a number of filtering attributes which you can use to fine-grain the notifications you receive. For example, you can filter the notifications to only those that are originating from a specific graph, app or a given account. More details on the filtering attributes for each topic can be found in the reference further down in this article. ## Getting your SNS subscriptions To get a list of your subscriptions, you can use the following query on the Lens API. First, authenticate with the builder role. Then, perform the query and the endpoint will return a list of all subscriptions linked to your account. You can optionally filter the subscriptions by the app they are linked to. ```graphql filename="GraphQL Query" query GetSnsSubscriptions { getSnsSubscriptions(request: { app: "0x0101010101010101010101010101010101010101" }) { items { id account webhook topic topicArn attributes } } } ``` ```json filename="Response" { "data": { "getSnsSubscriptions": [ { "id": "d0a6e0c0-e6e3-4b8a-9293-14b58392e3e3", "account": "0x0101010101010101010101010101010101010101", "webhook": "https://example.com/webhook", "topic": "PostCreated", "topicArn": "arn:aws:sns:us-west-2:123456789012:post-created", "attributes": {} }, { "id": "d0a6e0c0-e6e3-4b8a-9293-14b58392e3e3", "account": "0x0101010101010101010101010101010101010101", "webhook": "https://example.com/webhook", "topic": "AccountCreated", "topicArn": "arn:aws:sns:us-west-2:123456789012:account-followed", "attributes": {} } ] } } ``` ## Deleting a Subscription To delete a subscription, you can use the following mutation on the Lens API. First, authenticate with the builder role. Then, perform the mutation and the endpoint will delete the subscription from your account. ```graphql filename="GraphQL Mutation" mutation DeleteSnsSubscription { deleteSnsSubscription(request: { id: "d0a6e0c0-e6e3-4b8a-9293-14b58392e3e3" }) ``` ## SNS Topics Below is list of all the events fired by Lens, alongside their filtering attributes. For every SNS topic, you will find a working example of how to create a subscription to it, as well as the filtering attributes that can be applied on creation and the payload that will be sent to your webhook. ### Account These are all the events that tract any Account-related activity. #### AccountActionExecuted Fires when an action is executed on an Account. ```graphql filename="GraphQL Setup Example" mutation CreateAccountActionExecutedNotification { createSnsSubscriptions( request: { webhook: "https://example.com/webhook", topics: [{ accountActionExecuted: { # Optional, filter events by account # account: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by action # action: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by action type enum # actionType: ["TIPPING", "UNKNOWN"] # Optional, filter events by app # app: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by executing account # executingAccount: ["0x0101010101010101010101010101010101010101"], } }] } ) } ``` ```graphql filename="Filters" input AccountActionExecutedNotificationFilter { account: [EvmAddress] action: [EvmAddress] actionType: [AccountActionType] app: [EvmAddress] executingAccount: [EvmAddress] } ``` ```ts filename="Notification Type" interface AccountActionExecutedNotification { account: string; executing_account: string; action: string; action_type: string; config_params: string; execute_params: string; app?: string; timestamp: string; } // Example notification { "account": "0x0101010101010101010101010101010101010101", "executing_account": "0x0505050505050505050505050505050505050505", "action": "0x0202020202020202020202020202020202020202", "action_type": "TIPPING", "config_params": "{}", "execute_params": "{}", "app": "0x0404040404040404040404040404040404040404", "timestamp": "2024-01-01T00:00:00.000Z" } ``` #### AccountCreated Fires when a new Account is created. ```graphql filename="GraphQL Setup Example" mutation CreateAccountCreatedNotification { createSnsSubscriptions( request: { webhook: "https://example.com/webhook", topics: [{ accountCreated: { # Optional, filter events by app # app: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by graph # graph: ["0x0101010101010101010101010101010101010101"], } }] } ) } ``` ```graphql filename="Filters" input AccountCreatedNotificationFilter { graph: [EvmAddress] app: [EvmAddress] } ``` ```ts filename="Notification Type" interface AccountCreatedNotification { account: string; owned_by: string; graph: string; app?: string; timestamp: string; } // Example notification { "account": "0x0101010101010101010101010101010101010101", "owned_by": "0x0202020202020202020202020202020202020202", "graph": "0x0303030303030303030303030303030303030303", "app": "0x0404040404040404040404040404040404040404", "timestamp": "2024-01-01T00:00:00.000Z" } ``` #### AccountFollowed Fires when an Account gets followed. ```graphql filename="GraphQL Setup Example" mutation CreateAccountFollowedNotification { createSnsSubscriptions( request: { webhook: "https://example.com/webhook", topics: [{ accountFollowed: { # Optional, filter events by app # app: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by graph # graph: ["0x0101010101010101010101010101010101010102"], # Optional, filter events by follower # follower: "0x0101010101010101010101010101010101010103", # Optional, filter events by followedAccount # followedAccount: "0x0101010101010101010101010101010101010104", } }] } ) } ``` ```graphql filename="Filters" input AccountFollowedNotificationFilter { follower: [EvmAddress] followedAccount: [EvmAddress] graph: [EvmAddress] app: [EvmAddress] } ``` ```ts filename="Notification Type" interface AccountFollowedNotification { follower: string; followed_account: string; graph: string; app?: string; timestamp: string; } // Example notification { "follower": "0x0101010101010101010101010101010101010101", "followed_account": "0x0202020202020202020202020202020202020202", "graph": "0x0303030303030303030303030303030303030303", "app": "0x0404040404040404040404040404040404040404", "timestamp": "2024-01-01T00:00:00.000Z" } ``` #### Account Unfollowed Fires when an Account stops following another Account. ```graphql filename="GraphQL Setup Example" mutation CreateAccountUnfollowedNotification { createSnsSubscriptions( request: { webhook: "https://example.com/webhook", topics: [{ accountUnfollowed: { # Optional, filter events by app # app: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by graph # graph: ["0x0101010101010101010101010101010101010102"], # Optional, filter events by account unfollowing # unfollower: ["0x0101010101010101010101010101010101010103"], # Optional, filter events by unfollowed account # unfollowedAccount: ["0x0101010101010101010101010101010101010104"] } }] } ) } ``` ```graphql filename="Filters" input AccountUnfollowedNotificationFilter { unfollower: [EvmAddress] unfollowedAccount: [EvmAddress] graph: [EvmAddress] app: [EvmAddress] } ``` ```ts filename="Notification Type" interface AccountUnfollowedNotification { unfollower: string; unfollowed_account: string; graph: string; app?: string; timestamp: string; } // Example notification { "unfollower": "0x0101010101010101010101010101010101010101", "unfollowed_account": "0x0202020202020202020202020202020202020202", "graph": "0x0303030303030303030303030303030303030303", "app": "0x0404040404040404040404040404040404040404", "timestamp": "2024-01-01T00:00:00.000Z" } ``` #### AccountBlocked Fires when an Account blocks another Account. ```graphql filename="GraphQL Setup Example" mutation CreateAccountBlockedNotification { createSnsSubscriptions( request: { webhook: "https://example.com/webhook", topics: [{ accountBlocked: { # Optional, filter events by app # app: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by graph # graph: ["0x0101010101010101010101010101010101010102"], } }] } ) } ``` ```graphql filename="Filtering Attributes" input AccountBlockedNotificationFilter { graph: [EvmAddress] app: [EvmAddress] } ``` ```ts filename="Notification Type" interface AccountBlockedNotification { blockedAccount: string; blockingAccount: string; graph: string; app?: string; timestamp: string; } // Example notification { "blocked_account": "0x0101010101010101010101010101010101010101", "blocking_account": "0x0202020202020202020202020202020202020202", "graph": "0x0303030303030303030303030303030303030303", "app": "0x0404040404040404040404040404040404040404", "timestamp": "2024-01-01T00:00:00.000Z" } ``` #### AccountUnblocked Fires when an Account unblocks another Account. ```graphql filename="GraphQL Setup Example" mutation CreateAccountUnblockedNotification { createSnsSubscriptions( request: { webhook: "https://example.com/webhook", topics: [{ accountUnblocked: { # Optional, filter events by app # app: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by graph # graph: ["0x0101010101010101010101010101010101010102"], } }] } ) } ``` ```graphql filename="Filters" input AccountUnblockedNotificationFilter { graph: [EvmAddress] app: [EvmAddress] } ``` ```ts filename="Notification Type" interface AccountUnblockedNotification { blocked_account: string; blocking_account: string; graph: string; app?: string; timestamp: string; } // Example notification { "blocked_account": "0x0101010101010101010101010101010101010101", "blocking_account": "0x0202020202020202020202020202020202020202", "graph": "0x0303030303030303030303030303030303030303", "app": "0x0404040404040404040404040404040404040404", "timestamp": "2024-01-01T00:00:00.000Z" } ``` #### AccountUsernameCreated Fires when a username is created in a namespace. ```graphql filename="GraphQL Setup Example" mutation CreateAccountUsernameCreatedNotification { createSnsSubscriptions( request: { webhook: "https://example.com/webhook", topics: [{ accountUsernameCreated: { # Optional, filter events by namespace # namespace: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by account # account: ["0x0101010101010101010101010101010101010101"] } }] } ) } ``` ```graphql filename="Filters" input AccountUsernameCreatedNotificationFilter { namespace: [EvmAddress] account: [EvmAddress] } ``` ```ts filename="Notification Type" interface AccountUsernameCreatedNotification { namespace: string; account: string; local_name: string; timestamp: string; } // Example notification { "namespace": "0x0101010101010101010101010101010101010101", "account": "0x0202020202020202020202020202020202020202", "local_name": "username", "timestamp": "2024-01-01T00:00:00.000Z" } ``` #### AccountUsernameAssigned Fires when a username is assigned to an account. ```graphql filename="GraphQL Setup Example" mutation CreateAccountUsernameAssignedNotification { createSnsSubscriptions( request: { webhook: "https://example.com/webhook", topics: [{ accountUsernameAssigned: { # Optional, filter events by namespace # namespace: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by account # account: ["0x0101010101010101010101010101010101010101"] } }] } ) } ``` ```graphql filename="Filters" input AccountUsernameAssignedNotificationFilter { namespace: [EvmAddress] account: [EvmAddress] } ``` ```ts filename="Notification Type" interface AccountUsernameAssignedNotification { namespace: string; account: string; local_name: string; timestamp: string; } // Example notification { "namespace": "0x0101010101010101010101010101010101010101", "account": "0x0202020202020202020202020202020202020202", "local_name": "username", "timestamp": "2024-01-01T00:00:00.000Z" } ``` #### AccountUsernameUnassigned Fires when a username is unassigned from an account. ```graphql filename="GraphQL Setup Example" mutation CreateAccountUsernameUnassignedNotification { createSnsSubscriptions( request: { webhook: "https://example.com/webhook", topics: [{ accountUsernameUnassigned: { # Optional, filter events by namespace # namespace: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by previous account # previous_account: ["0x0101010101010101010101010101010101010102"] } }] } ) } ``` ```graphql filename="Filters" input AccountUsernameUnassignedNotificationFilter { namespace: [EvmAddress] previous_account: [EvmAddress] } ``` ```ts filename="Notification Type" interface AccountUsernameUnassignedNotification { namespace: string; previous_account: string; local_name: string; timestamp: string; } // Example notification { "namespace": "0x0101010101010101010101010101010101010101", "previous_account": "0x0202020202020202020202020202020202020202", "local_name": "username", "timestamp": "2024-01-01T00:00:00.000Z" } ``` #### AccountManagerAdded Fires when an account is added as a manager to another account. ```graphql filename="GraphQL Setup Example" mutation CreateAccountManagerAddedNotification { createSnsSubscriptions( request: { webhook: "https://example.com/webhook", topics: [{ accountManagerAdded: { # Optional, filter events by managed account # managed_account: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by manager address # manager: ["0x0101010101010101010101010101010101010101"] } }] } ) } ``` ```graphql filename="Filters" input AccountManagerAddedNotificationFilter { managed_account: [EvmAddress] manager: [EvmAddress] } ``` ```ts filename="Notification Type" interface AccountManagerAddedNotification { managed_account: string; manager: string; timestamp: string; } // Example notification { "managed_account": "0x0101010101010101010101010101010101010101", "manager": "0x0202020202020202020202020202020202020202", "timestamp": "2024-01-01T00:00:00.000Z" } ``` #### AccountManagerRemoved Fires when an account is removed as a manager from another account. ```graphql filename="GraphQL Setup Example" mutation CreateAccountManagerRemovedNotification { createSnsSubscriptions( request: { webhook: "https://example.com/webhook", topics: [{ accountManagerRemoved: { # Optional, filter events by managed account # managed_account: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by manager address # manager: ["0x0101010101010101010101010101010101010101"] } }] } ) } ``` ```graphql filename="Filters" input AccountManagerRemovedNotificationFilter { managed_account: [EvmAddress] manager: [EvmAddress] } ``` ```ts filename="Notification Type" interface AccountManagerRemovedNotification { managed_account: string; manager: string; timestamp: string; } // Example notification { "managed_account": "0x0101010101010101010101010101010101010101", "manager": "0x0202020202020202020202020202020202020202", "timestamp": "2024-01-01T00:00:00.000Z" } ``` #### AccountManagerUpdated Fires when an account's manager permissions are updated. ```graphql filename="GraphQL Setup Example" mutation CreateAccountManagerUpdatedNotification { createSnsSubscriptions( request: { webhook: "https://example.com/webhook", topics: [{ accountManagerUpdated: { # Optional, filter events by managed account # managed_account: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by manager address # manager: ["0x0101010101010101010101010101010101010101"] } }] } ) } ``` ```graphql filename="Filters" input AccountManagerUpdatedNotificationFilter { managed_account: [EvmAddress] manager: [EvmAddress] } ``` ```ts filename="Notification Type" interface AccountManagerUpdatedNotification { managed_account: string; manager: string; can_execute_transactions: boolean; can_transfer_tokens: boolean; can_transfer_native: boolean; can_set_metadata_uri: boolean; timestamp: string; } // Example notification { "managed_account": "0x0101010101010101010101010101010101010101", "manager": "0x0202020202020202020202020202020202020202", "can_execute_transactions": true, "can_transfer_tokens": true, "can_transfer_native": false, "can_set_metadata_uri": true, "timestamp": "2024-01-01T00:00:00.000Z" } ``` #### AccountOwnershipTransferred This notification fires when ownership of an account is transferred to a new owner. ```graphql filename="GraphQL Setup Example" mutation CreateAccountOwnershipTransferredNotification { createSnsSubscriptions( request: { webhook: "https://example.com/webhook", topics: [{ accountOwnershipTransferred: { # Optional, filter events by account # account: ["0x0101010101010101010101010101010101010101"] } }] } ) } ``` ```graphql filename="Filters" input AccountOwnershipTransferredNotificationFilter { account: [EvmAddress] } ``` ```ts filename="Notification Type" interface AccountOwnershipTransferredNotification { account: string; new_owner: string; timestamp: string; } // Example notification { "account": "0x0101010101010101010101010101010101010101", "new_owner": "0x0202020202020202020202020202020202020202", "timestamp": "2024-01-01T00:00:00.000Z" } ``` #### AccountReported This notification fires when an account is reported by another user. ```graphql filename="GraphQL Setup Example" mutation CreateAccountReportedNotification { createSnsSubscriptions( request: { webhook: "https://example.com/webhook", topics: [{ accountReported: { # Optional, filter events by reported account # reported_account: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by reporter # reporter: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by app # app: ["0x0101010101010101010101010101010101010101"], } }] } ) } ``` ```graphql filename="Filters" input AccountReportedNotificationFilter { reportedAccount: [EvmAddress] reporter: [EvmAddress] app: [EvmAddress] } ``` ```ts filename="Notification Type" interface AccountReportedNotification { reported_account: string; reporter: string; reason: string; additional_comment?: string; reference_posts?: string[]; app?: string; timestamp: string; } // Example notification { "reported_account": "0x0101010101010101010101010101010101010101", "reporter": "0x0202020202020202020202020202020202020202", "reason": "SPAM", "additional_comment": "This account is sending unwanted messages", "reference_posts": ["0x0303030303030303030303030303030303030303"], "app": "0x0404040404040404040404040404040404040404", "timestamp": "2024-01-01T00:00:00.000Z" } ``` #### AccountMentioned This notification fires when an account is mentioned in a post. ```graphql filename="GraphQL Setup Example" mutation CreateAccountMentionedNotification { createSnsSubscriptions( request: { webhook: "https://example.com/webhook", topics: [{ accountMentioned: { # Optional, filter events by author # author: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by feed # feed: "0x0101010101010101010101010101010101010101", # Optional, filter events by mentioned account # mentioned_account: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by mentioned username # mentioned_username: ["alice"], # Optional, filter events by app # app: ["0x0101010101010101010101010101010101010101"], } }] } ) } ``` ```graphql filename="Filters" input AccountMentionedNotificationFilter { author: [EvmAddress] feed: [EvmAddress] mentioned_account: [EvmAddress] mentioned_username: [EvmAddress] app: [EvmAddress] } ``` ```ts filename="Notification Type" interface AccountMentionedNotification { post_id: string; author: string; mentioned_account: string; mentioned_username: string; feed: string; app?: string; timestamp: string; } // Example notification { "post_id": "0101010101010101010101010101010101010101", "author": "0x0202020202020202020202020202020202020202", "mentioned_account": "0x0303030303030303030303030303030303030303", "mentioned_username": "alice", "feed": "0x0404040404040404040404040404040404040404", "app": "0x0505050505050505050505050505050505050505", "timestamp": "2024-01-01T00:00:00.000Z" } ``` ### Post All the events related to posts. #### PostActionExecuted This notification fires every time a post action is executed. ```graphql filename="GraphQL Setup Example" mutation CreatePostActionExecutedNotification { createSnsSubscriptions( request: { webhook: "https://example.com/webhook", topics: [{ postActionExecuted: { # Optional, filter events by app # app: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by post id # postId: ["0101010101010101010101010101010101010101"], # Optional, filter events by action # action: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by action type enum # actionType: ["TIPPING", "SIMPLE_COLLECT", "UNKNOWN"] # Optional, filter events by executing account # executingAccount: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by receiving account # receivingAccount: ["0x0101010101010101010101010101010101010101"], } }] } ) } ``` ```graphql filename="Filters" input PostActionExecutedNotificationFilter { postId: [PostId] action: [EvmAddress] actionType: [PostActionType] executingAccount: [EvmAddress] receivingAccount: [EvmAddress] app: [EvmAddress] } ``` ```ts filename="Notification Type" interface PostActionExecutedNotification { post_id: string; action: string; action_type: string; executing_account: string; receiving_account: string; execute_params: string; app?: string; timestamp: string; } // Example notification { "post_id": "0101010101010101010101010101010101010101", "action": "0x0202020202020202020202020202020202020202", "action_type": "TIPPING", "executing_account": "0x0303030303030303030303030303030303030303", "receiving_account": "0x0404040404040404040404040404040404040404", "app": "0x0505050505050505050505050505050505050505", "timestamp": "2024-01-01T00:00:00.000Z" } ``` #### PostCollected This notification fires every time a post is collected. ```graphql filename="GraphQL Setup Example" mutation CreatePostCollectedNotification { createSnsSubscriptions( request: { webhook: "https://example.com/webhook", topics: [{ postCollected: { # Optional, filter events by app # app: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by collector # collector: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by post author # postAuthor: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by post id # postId: ["0101010101010101010101010101010101010101"], } }] } ) } ``` ```graphql filename="Filters" input PostCollectedNotificationFilter { collector: [EvmAddress] postAuthor: [EvmAddress] postId: [PostId] app: [EvmAddress] } ``` ```ts filename="Notification Type" interface PostCollectedNotification { post_id: string; author: string; collector: string; collect_params: string; app?: string; timestamp: string; } // Example notification { "post_id": "0101010101010101010101010101010101010101", "author": "0x0202020202020202020202020202020202020202", "collector": "0x0303030303030303030303030303030303030303", "collect_params": "{}", "app": "0x0505050505050505050505050505050505050505", "timestamp": "2024-01-01T00:00:00.000Z" } ``` #### PostCreated This notification fires every time a new Post gets indexed. ```graphql filename="GraphQL Setup Example" mutation CreatePostCreatedNotification { createSnsSubscriptions( request: { webhook: "https://example.com/webhook", topics: [{ postCreated: { # Optional, filter events by feed # feed: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by app # app: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by parent post # parent_post: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by post types # postTypes: ["COMMENT"], # Optional, filter events by author # author: ["0x0101010101010101010101010101010101010101"] } }] } ) } ``` ```graphql filename="Filters" input PostCreatedNotificationFilter { author: [EvmAddress] parentPostId: [PostId] postTypes: [PostType!] feed: [EvmAddress] app: [EvmAddress] } ``` ```ts filename="Notification Type" interface PostCreatedNotification { post_id: string; parent_post?: string; post_types?: string[]; author: string; feed: string; app?: string; timestamp: string; metadata?: string; } // Example notification { "post_id": "0101010101010101010101010101010101010101", "parent_post": "0x0202020202020202020202020202020202020202", "author": "0x0303030303030303030303030303030303030303", "feed": "0x0404040404040404040404040404040404040404", "app": "0x0505050505050505050505050505050505050505", "metadata": "lens://0101010101010101010101010101010101010101", "timestamp": "2024-01-01T00:00:00.000Z" } ``` #### PostEdited This notification fires when a post is edited. ```graphql filename="GraphQL Setup Example" mutation CreatePostEditedNotification { createSnsSubscriptions( request: { webhook: "https://example.com/webhook", topics: [{ postEdited: { # Optional, filter events by feed # feed: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by app # app: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by parent post # parent_post: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by post types # postTypes: ["COMMENT"], # Optional, filter events by author # author: ["0x0101010101010101010101010101010101010101"] } }] } ) } ``` ```graphql filename="Filters" input PostEditedNotificationFilter { author: [EvmAddress] feed: [EvmAddress] app: [EvmAddress] parentPostId: [PostId] postTypes: [PostType!] } ``` ```ts filename="Notification Type" interface PostEditedNotification { post_id: string; author: string; feed: string; app?: string; metadata: string; timestamp: string; } // Example notification { "post_id": "0101010101010101010101010101010101010101", "author": "0x0202020202020202020202020202020202020202", "feed": "0x0303030303030303030303030303030303030303", "app": "0x0404040404040404040404040404040404040404", "metadata": "lens://0101010101010101010101010101010101010101", "timestamp": "2024-01-01T00:00:00.000Z" } ``` #### PostDeleted This notification fires when a post is deleted. ```graphql filename="GraphQL Setup Example" mutation CreatePostDeletedNotification { createSnsSubscriptions( request: { webhook: "https://example.com/webhook", topics: [{ postDeleted: { # Optional, filter events by feed # feed: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by app # app: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by parent post # parent_post: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by post types # postTypes: ["COMMENT"], # Optional, filter events by author # author: ["0x0101010101010101010101010101010101010101"] } }] } ) } ``` ```graphql filename="Filters" input PostDeletedNotificationFilter { author: [EvmAddress] feed: [EvmAddress] app: [EvmAddress] parentPostId: [PostId] postTypes: [PostType!] } ``` ```ts filename="Notification Type" interface PostDeletedNotification { post_id: string; author: string; feed: string; app?: string; metadata?: string; timestamp: string; } // Example notification { "post_id": "0101010101010101010101010101010101010101", "author": "0x0202020202020202020202020202020202020202", "feed": "0x0303030303030303030303030303030303030303", "app": "0x0404040404040404040404040404040404040404", "metadata": "lens://0101010101010101010101010101010101010101", "timestamp": "2024-01-01T00:00:00.000Z" } ``` #### PostReactionAdded This notification fires when a reaction is added to a post. ```graphql filename="GraphQL Setup Example" mutation CreatePostReactionAddedNotification { createSnsSubscriptions( request: { webhook: "https://example.com/webhook", topics: [{ postReactionAdded: { # Optional, filter events by post id # post_id: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by reacting account # reacting_account: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by reaction type # reaction_type: ["UPVOTE"], # Optional, filter events by app # app: ["0x0101010101010101010101010101010101010101"] } }] } ) } ``` ```graphql filename="Filters" input PostReactionAddedNotificationFilter { post_id: [PostId] reacting_account: [EvmAddress] reaction_type: [PostReactionType] app: [EvmAddress] } ``` ```ts filename="Notification Type" interface PostReactionAddedNotification { post_id: string; reacting_account: string; reaction_type: string; app?: string; timestamp: string; } // Example notification { "post_id": "0101010101010101010101010101010101010101", "reactor": "0x0202020202020202020202020202020202020202", "reaction": "UPVOTE", "app": "0x0303030303030303030303030303030303030303", "timestamp": "2024-01-01T00:00:00.000Z" } ``` #### PostReactionRemoved This notification fires when a reaction is removed from a post. ```graphql filename="GraphQL Setup Example" mutation CreatePostReactionRemovedNotification { createSnsSubscriptions( request: { webhook: "https://example.com/webhook", topics: [{ postReactionRemoved: { # Optional, filter events by post id # post_id: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by reacting account # reacting_account: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by reaction type # reaction_type: ["UPVOTE"], # Optional, filter events by app # app: ["0x0101010101010101010101010101010101010101"] } }] } ) } ``` ```graphql filename="Filters" input PostReactionRemovedNotificationFilter { post_id: [PostId] reacting_account: [EvmAddress] reaction_type: [PostReactionType] app: [EvmAddress] } ``` ```ts filename="Notification Type" interface PostReactionRemovedNotification { post_id: string; reacting_account: string; reaction_type: string; app: string; timestamp: string; } // Example notification { "post_id": "0101010101010101010101010101010101010101", "reactor": "0x0202020202020202020202020202020202020202", "reaction": "UPVOTE", "app": "0x0303030303030303030303030303030303030303", "timestamp": "2024-01-01T00:00:00.000Z" } ``` #### PostReported This notification fires when a post is reported. ```graphql filename="GraphQL Setup Example" mutation CreatePostReportedNotification { createSnsSubscriptions( request: { webhook: "https://example.com/webhook", topics: [{ postReported: { # Optional, filter events by author # author: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by reporter # reporter: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by feed # feed: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by app # app: ["0x0101010101010101010101010101010101010101"] } }] } ) } ``` ```graphql filename="Filters" input PostReportedNotificationFilter { author: [EvmAddress] reporter: [EvmAddress] feed: [EvmAddress] app: [EvmAddress] } ``` ```ts filename="Notification Type" interface PostReportedNotification { post_id: string; reporter: string; reason: string; additional_comment?: string; app?: string; timestamp: string; } // Example notification { "post_id": "0101010101010101010101010101010101010101", "reporter": "0x0202020202020202020202020202020202020202", "reason": "SPAM", "additional_comment": "This post contains inappropriate content", "app": "0x0303030303030303030303030303030303030303", "timestamp": "2024-01-01T00:00:00.000Z" } ``` ### Metadata All the events related to metadata snapshots, supports metadata snapshots of all Lens primitives. #### MetadataSnapshotSuccess This notification fires when a metadata snapshot is successfully created. ```graphql filename="GraphQL Setup Example" mutation CreateMetadataSnapshotSuccessNotification { createSnsSubscriptions( request: { webhook: "https://example.com/webhook", topics: [{ metadataSnapshotSuccess: { # Optional, filter events by source # source: ["post_01"], } }] } ) } ``` ```graphql filename="Filters" input MetadataSnapshotSuccessNotificationFilter { source: [EntityId] } ``` ```ts filename="Notification Type" interface MetadataSnapshotSuccessNotification { original_uri: string; snapshot_url: string; source: string; } // Example notification { "original_uri": "https://example.com/metadata/original", "snapshot_url": "https://example.com/metadata/snapshot", "source": "post_01" } ``` #### MetadataSnapshotError This notification fires when there's an error creating a metadata snapshot. ```graphql filename="GraphQL Setup Example" mutation CreateMetadataSnapshotErrorNotification { createSnsSubscriptions( request: { webhook: "https://example.com/webhook", topics: [{ metadataSnapshotError: { # Optional, filter events by source # source: ["post_01"], } }] } ) } ``` ```graphql filename="Filters" input MetadataSnapshotErrorNotificationFilter { source: [EntityId] } ``` ```ts filename="Notification Type" interface MetadataSnapshotErrorNotification { original_uri: string; source: string; reason: string; } // Example notification { "original_uri": "https://example.com/metadata/original", "reason": "Failed to process metadata", "source": "post_01" } ``` ### Media All the events related to media snapshots, these include images, audio and video coming from Posts as well as metadata from all primitives. #### MediaSnapshotSuccess This notification fires when a media snapshot is successfully created. ```graphql filename="GraphQL Setup Example" mutation CreateMediaSnapshotSuccessNotification { createSnsSubscriptions( request: { webhook: "https://example.com/webhook", topics: [{ mediaSnapshotSuccess: { # Optional, filter events by source # source: ["post_01020304"], } }] } ) } ``` ```graphql filename="Filters" input MediaSnapshotNotificationFilter { source: [EntityId] } ``` ```ts filename="Notification Type" interface MediaSnapshotSuccessNotification { original_uri: string; snapshot_url: string; source: string; } // Example notification { "original_uri": "https://example.com/media/original", "snapshot_url": "https://example.com/media/snapshot", "source": "post_01020304", } ``` #### MediaSnapshotError This notification fires when there's an error creating a media snapshot. ```graphql filename="GraphQL Setup Example" mutation CreateMediaSnapshotErrorNotification { createSnsSubscriptions( request: { webhook: "https://example.com/webhook", topics: [{ mediaSnapshotError: { # Optional, filter events by source # source: ["post_01020304"], } }] } ) } ``` ```graphql filename="Filters" input MediaSnapshotNotificationFilter { source: [EntityId] } ``` ```ts filename="Notification Type" interface MediaSnapshotErrorNotification { original_uri: string; reason: string; source: string; } // Example notification { "original_uri": "https://example.com/media/original", "reason": "Failed to process media", "source": "post_01020304", } ``` ### Token Distribution All the events related to token distribution. #### TokenDistributionSuccess This notification fires when an account is rewarded with tokens. ```graphql filename="GraphQL Setup Example" mutation CreateTokenDistributionSuccessNotification { createSnsSubscriptions( request: { webhook: "https://example.com/webhook", topics: [{ tokenDistributionSuccess: { # Optional, filter events by account # recipient: ["0x0101010101010101010101010101010101010101"], # Optional, filter events by tokens # tokens: ["0x0101010101010101010101010101010101010101"], } }] } ) } ``` ```graphql filename="Filters" input TokenDistributionSuccessNotificationFilter { recipient: [EvmAddress] tokens: [EvmAddress] } ``` ```ts filename="Notification Type" interface TokenDistributionSuccessNotification { recipient: string; token: string; amount: { asset: { address: string; symbol: string; decimals: number; name: string; }; amount: string; }; tx_hash: string; timestamp: string; } // Example notification { "recipient": "0x0101010101010101010101010101010101010101", "token": "0x0101010101010101010101010101010101010101", "amount": { "asset": { "address": "0x0101010101010101010101010101010101010101", "symbol": "ETH", "decimals": 18, "name": "Ethereum" }, "amount": "1000000000000000000" }, "tx_hash": "0x0101010101010101010101010101010101010101", "timestamp": "2024-01-01T00:00:00.000Z" } ``` ================ File: src/pages/protocol/authentication.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; export default ({ children }) => {children}; {/* Start of the page content */} # Authentication This guide will help you understand how to handle authentication in Lens. --- The Lens API uses authentication roles to define different levels of access and interaction: - **Account Owner**: An end-user who owns a Lens Account. - **Account Manager**: An end-user managing a Lens Account, either their own or one they have been delegated. See the [Account Managers](./accounts/manager) guide for more information. - **Onboarding User**: A end-user without a Lens Account, limited to features related to onboarding, such as creating an account. - **Builder**: A developer role used to authenticate and access configuration and management features. ## Log In to Lens End-user roles such as _Account Owner_, _Account Manager_, or _Onboarding User_ require the EVM address of the App they want to connect to in order to log in to Lens. In contrast, the _Builder_ role does not require an App address for authentication. For quick experimentation with Lens, a test App has been deployed on each network: - **Lens Mainnet**: `0x8A5Cc31180c37078e1EbA2A23c861Acf351a97cE` - **Lens Testnet**: `0xC75A89145d765c396fd75CbD16380Eb184Bd2ca7` You can learn how to create your own App in the [Apps guide](./apps) . We'll use viem's [Local Account](https://viem.sh/docs/clients/wallet#local-accounts-private-key-mnemonic-etc) in our example, but you can use a `WalletClient` instance from the [wagmi hook](https://wagmi.sh/react/api/hooks/useWalletClient) or an ethers.js [Signer](https://docs.ethers.org/v6/api/providers/#Signer). Chose the adapter for the library of your choice: ```ts filename="viem" import { signMessageWith } from "@lens-protocol/client/viem"; ``` ```ts filename="ethers" import { signMessageWith } from "@lens-protocol/client/ethers"; ``` And, use the `PublicClient` instance you created [earlier](./getting-started/typescript) to log in and acquire a `SessionClient` instance. ```ts filename="Onboarding User" import { client } from "./client"; import { signer } from "./signer"; const authenticated = await client.login({ onboardingUser: { app: "0x5678…", wallet: signer.address, }, signMessage: signMessageWith(signer), }); if (authenticated.isErr()) { return console.error(authenticated.error); } // SessionClient: { ... } const sessionClient = authenticated.value; ``` ```ts filename="Account Owner" import { client } from "./client"; import { signer } from "./signer"; const authenticated = await client.login({ accountOwner: { account: `0x1234…`, app: "0x5678…", owner: signer.address, }, signMessage: signMessageWith(signer), }); if (authenticated.isErr()) { return console.error(authenticated.error); } // SessionClient: { ... } const sessionClient = authenticated.value; ``` ```ts filename="Account Manager" import { client } from "./client"; import { signer } from "./signer"; const authenticated = await client.login({ accountManager: { account: `0x1234…`, app: "0x5678…", manager: signer.address, }, signMessage: signMessageWith(signer), }); if (authenticated.isErr()) { return console.error(authenticated.error); } // SessionClient: { ... } const sessionClient = authenticated.value; ``` ```ts filename="Builder" import { client } from "./client"; import { signer } from "./signer"; const authenticated = await client.login({ builder: { address: signer.address, }, signMessage: signMessageWith(signer), }); if (authenticated.isErr()) { return console.error(authenticated.error); } // SessionClient: { ... } const sessionClient = authenticated.value; ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; export const client = PublicClient.create({ environment: mainnet, origin: "https://myappdomain.xyz", // Omit if running in a browser }); ``` ```ts filename="signer.ts" import { privateKeyToAccount } from "viem/accounts"; export const signer = privateKeyToAccount(process.env.APP_PRIVATE_KEY); ``` Use the `SessionClient` to interact with `@lens-protocol/client/actions` that require authentication. The `@lens-protocol/client/actions` that require authentication are explicitly labelled by the type of client they accept as first argument. ### Generate an Authentication Challenge First, generate an authentication challenge. ```graphql filename="Account Owner" mutation { challenge( request: { accountOwner: { app: "" account: "" owner: "" } } ) { __typename id text } } ``` ```graphql filename="Account Manager" mutation { challenge( request: { accountManager: { app: "" account: "" manager: "" } } ) { __typename id text } } ``` ```graphql filename="Onboarding User" mutation { challenge( request: { onboardingUser: { app: "", wallet: "" } } ) { __typename id text } } ``` ```graphql filename="Builder" mutation { challenge(request: { builder: { address: "" } }) { __typename id text } } ``` If you are making this request from a non-browser environment, ensure that the HTTP [Origin](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin) header is set to the correct value for your application's domain. The challenge response will contain the `id` and the `text` of the challenge message. ```json filename="Response" { "data": { "challenge": { "id": "", "text": " wants you to sign in with your Ethereum account…" } } } ``` The challenge text is a [Sign-In with Ethereum](https://docs.login.xyz/general-information/siwe-overview/eip-4361) (SIWE) message used to verify the ownership of the signer address. It is issued only if the signer address is the Account itself or an [Account Manager](./accounts/manager) for the given Account. ### Sign the Challenge SIWE Message Next, sign the challenge message with the signer's private key. ```ts filename="viem" import { privateKeyToAccount } from "viem/accounts"; const account = privateKeyToAccount(process.env.APP_PRIVATE_KEY); const signature = account.signMessage({ message }); ``` ```ts filename="ethers" import { Wallet } from "ethers"; const signer = new Wallet(process.env.APP_PRIVATE_KEY); const signature = signer.signMessage(message); ``` ### Acquire Authentication Tokens Finally, you can use the `challenge.id` and the `signature` to obtain your authentication tokens. ```graphql filename="Mutation" mutation { authenticate(request: { id: "", signature: "" }) { ... on AuthenticationTokens { accessToken refreshToken idToken } ... on WrongSignerError { reason } ... on ExpiredChallengeError { reason } ... on ForbiddenError { reason } } } ``` ```json filename="AuthenticationTokens" { "data": { "authenticate": { "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6…", "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Ij…", "idToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Ij…" } } } ``` That's it—you are now authenticated. Coming soon ## List Available Accounts Often, when you are about to log in with an Account, you may want to list the available Accounts for the user's wallet. Use the paginated `fetchAccountsAvailable` action to list the Accounts available for the given wallet address. ```ts filename="Example" import { evmAddress } from "@lens-protocol/client"; import { fetchAccountsAvailable } from "@lens-protocol/client/actions"; import { client } from "./client"; const result = await fetchAccountsAvailable(client, { managedBy: evmAddress("0x1234…"), includeOwned: true, }); if (result.isErr()) { return console.error(result.error); } // items: Array const { items, pageInfo } = result.value; ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; export const client = PublicClient.create({ environment: mainnet, }); ``` Use the paginated `accountsAvailable` query to list the Accounts owned or managed by a given address. ```graphql filename="ListAvailableAccounts.graphql" query { accountsAvailable( request: { managedBy: "", includeOwned: true } ) { items { ... on AccountManaged { account { address username { value } metadata { name picture } } permissions { canExecuteTransactions canTransferTokens canTransferNative canSetMetadataUri } addedAt } ... on AccountOwned { account { address username { value } metadata { name picture } } addedAt } } pageInfo { prev next } } } ``` ```json filename="Response" { "data": { "accountsAvailable": { "items": [ { "account": { "address": "0x1234…", "username": { "value": "lens/jane" }, "metadata": { "name": "Bob", "picture": "https://example.com/bob.jpg" } }, "permissions": { "canExecuteTransactions": true, "canTransferTokens": false, "canTransferNative": false, "canSetMetadataUri": false }, "addedAt": "2021-10-01T00:00:00Z" }, { "account": { "address": "0x5678…", "username": { "value": "lens/alice" }, "metadata": { "name": "Alice", "picture": "https://example.com/alice.jpg" } }, "addedAt": "2021-10-01T00:00:00Z" } ], "pageInfo": { "prev": null, "next": null } } } } ``` Coming soon Continue with the [Pagination](./best-practices/pagination) guide for more information on how to handle paginated results. See the [Account Manager](./accounts/manager) guide for more information on how to manage accounts. ## Manage Sessions Once you have successfully authenticated, you can manage your authenticated sessions by: - Keeping your session alive - Getting details about the current session - Listing all authenticated sessions ### Keep Alive #### Resume Session By default, the `PublicClient` uses in-memory storage for the storing the authenticated session, which is lost when the current thread closes, like when refreshing a page in a browser. To keep the session persistent, supply a long-term storage solution to the `PublicClient` config. In a browser, for instance, you could use the [Web Storage API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API) like `window.localStorage`: ```ts filename="client.ts" highlight="6" import { PublicClient, mainnet } from "@lens-protocol/client"; const const client = PublicClient.create({ environment: mainnet storage: window.localStorage, }); ``` Then resume an authenticated `SessionClient` from long-term storage like so: ```ts filename="Resume Session" import { client } from "./client"; const resumed = await client.resumeSession(); if (resumed.isErr()) { return console.error(resumed.error); } // SessionClient: { ... } const sessionClient = resumed.value; ``` The `SessionClient` instance is now ready to be used for authenticated requests. #### Token Refresh You can refresh your authentication tokens by calling the `refreshAuthentication` mutation. ```graphql filename="Mutation" mutation { refresh(request: { refreshToken: "" }) { ... on AuthenticationTokens { accessToken refreshToken idToken } ... on ForbiddenError { reason } } } ``` ```json filename="Response" { "data": { "refresh": { "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6…", "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Ij…", "idToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Ij…" } } } ``` Coming soon ### Get Current Session Use the `currentSession` action: ```ts import { currentSession } from "@lens-protocol/client/actions"; ``` to get details about the current session. ```ts const result = await currentSession(sessionClient); if (result.isErr()) { return console.error(result.error); } // AuthenticatedSession: { authenticationId: UUID, app: EvmAddress, ... } const session = result.value; ``` You can get details about the current session by calling the `currentSession` query. You MUST be authenticated as Account Owner or Account Manager to make this request. ```graphql filename="Query" query { currentSession { authenticationId app browser device os origin signer createdAt updatedAt } } ``` ```json filename="Response" { "data": { "currentSession": { "id": "787b1905-6e20-4333-ae4a-475cd379e76e", "app": "0xa0182D914845ec1C3EF61a23C50D56370E23d94e", "browser": "Chrome", "device": "Other", "os": "Other", "signer" "0xC47Cccc2bf4CF2635a817C01c6A6d965045b06e6", "createdAt": "2021-10-01T00:00:00Z", "expiresAt": "2021-10-01T00:10:00Z" } } } ``` Coming soon ### List Authenticated Sessions Use the `fetchAuthenticatedSessions` action: ```ts import { fetchAuthenticatedSessions } from "@lens-protocol/client/actions"; ``` to get a paginated list of all authenticated sessions. ```ts filename="All Sessions" const result = await fetchAuthenticatedSessions(sessionClient); if (result.isErr()) { return console.error(result.error); } // Array: [{ authenticationId: UUID, app: EvmAddress, ... }, ... ] const sessions = result.value.items; ``` ```ts filename="By App" highlight="2" const result = await fetchAuthenticatedSessions(sessionClient, { app: "0x5678…", }); ``` You can list all your authenticated sessions by calling the paginated `authenticatedSessions` query. You MUST be authenticated as Account Owner or Account Manager to make this request. ```graphql filename="Query" query { authenticatedSessions( request: { # app: ", You can filter by app address pageSize: TEN } ) { items { authenticationId app browser device os origin signer createdAt updatedAt } pageInfo { prev next } } } ``` ```json filename="Response" { "data": { "authenticatedSessions": { "items": [ { "id": "787b1905-6e20-4333-ae4a-475cd379e76e", "app": "0xa0182D914845ec1C3EF61a23C50D56370E23d94e", "browser": "Chrome", "device": "Other", "os": "Other", "signer" "0xC47Cccc2bf4CF2635a817C01c6A6d965045b06e6", "createdAt": "2021-10-01T00:00:00Z", "expiresAt": "2021-10-01T00:10:00Z" } ], "pageInfo": { "prev": null, "next": null } } } } ``` Coming soon See the [Pagination](./best-practices/pagination) guide for more information on how to handle paginated results. ## Log Out You MUST be authenticated as Account Owner or Account Manager to make this request. Use the `client.logout` method to revoke any authenticated session and clear any client state. ```ts const result = await client.logout(); ``` Use the `revokeAuthentication` mutation to revoke a specific authenticated session. ```graphql filename="Mutation" mutation { revokeAuthentication(request: { authenticationId: "" }) } ``` ```json filename="Response" { "data": { "revokeAuthentication": null } } ``` This will invalidate the Refresh Token for the given authenticated session. Any attempt to use them will result in forbidden error. Any issued Access Tokens will remain valid for the remainder of their short lifetime. Coming soon ## Get Last Logged-In Account A simple way to fetch a user's last Lens account login, helping returning users quickly identify and access their previous account before authentication is required. Use the `lastLoggedInAccount` action to get the last logged-in account. ```ts filename="All Apps" import { evmAddress } from "@lens-protocol/client"; import { lastLoggedInAccount } from "@lens-protocol/client/actions"; const result = await lastLoggedInAccount(anyClient, { address: evmAddress("0x1234…"), }); if (result.isErr()) { return console.error(result.error); } ``` ```ts filename="Specific App" import { evmAddress } from "@lens-protocol/client"; import { lastLoggedInAccount } from "@lens-protocol/client/actions"; const result = await lastLoggedInAccount(anyClient, { app: evmAddress("0x1234…"), address: evmAddress("0x5678…"), }); if (result.isErr()) { return console.error(result.error); } ``` Use the `lastLoggedInAccount` query to get the last logged-in account. ```graphql filename="Query" query { lastLoggedInAccount(request: { address: "", app: "" }) { { address username { value } metadata { name picture } } } } ``` ```json filename="Response" { "data": { "lastLoggedInAccount": { "address": "0x1234…", "username": { "value": "username" }, "metadata": { "name": "name", "picture": "picture" } } } } ``` Coming soon --- ## Advanced Topics ### Authentication Storage If you need to store authentication tokens in a custom storage solution with the Lens SDK, implement the `IStorageProvider` interface. ```ts filename="IStorageProvider" interface IStorageProvider { getItem(key: string): Promise | string | null; setItem( key: string, value: string, ): Promise | Promise | void | string; removeItem(key: string): Promise | Promise | void; } ``` For example, you can use the `cookie-next` library for Next.js to store and pass the authentication tokens between client and server of a Next.js application. ```ts filename="storage.ts" import { getCookie, setCookie, deleteCookie } from "cookies-next"; import { IStorageProvider } from "@lens-protocol/client"; export const storage: IStorageProvider = { getItem(key: string) { return getCookie(key) ?? null; }, setItem(key: string, value: string) { await setCookie(key, value); }, removeItem(key: string) { await deleteCookie(key); }, }; ``` ```ts filename="client.ts" import { PublicClient, mainnet } from "@lens-protocol/client"; import { storage } from "./storage"; const const client = PublicClient.create({ environment: mainnet storage, }); ``` ### Authentication Tokens Lens API uses [JSON Web Tokens](https://jwt.io/introduction) (JWT) as format for Token-Based authentication. On successful authentication, Lens API issues three tokens: - Access Token - ID Token - Refresh Token Lens JWTs are signed with the RS256 algorithm and can be verified using JSON Web Key Sets (JWKS) from the `/.well-known/jwks.json` endpoint on the corresponding Lens API environment: - Mainnet: `https://api.lens.xyz/.well-known/jwks.json` - Testnet: `https://api.testnet.lens.xyz/.well-known/jwks.json` Signing keys could be rotated at any time. Make sure to cache the JWKS and update it periodically. #### Access Token Access Tokens are used to authenticate a user's identity when making requests to the Lens API. The Access Token is required in the `Authorization` or `x-access-token` header for all authenticated requests to the Lens API. ```http Authorization: Bearer # or x-access-token: ``` DO NOT share the Access Token with anyone. Keep it secure and confidential. If you are looking to identify a user's request on a backend service, use the [ID Token](#authentication-tokens-id-token) instead. Lens Access Tokens are valid for **10 minutes** from the time of issuance. #### Refresh Token A Refresh Token is a credential artifact used to obtain a new authentication tokens triplet without user interaction. This allows for a shorter Access Token lifetime for security purposes without involving the user when the access token expires. You can request new authentication tokens until the refresh token is added to a denylist or expires. DO NOT share the Refresh Token with anyone. Keep it secure and confidential, possibly on the client-side only. If you are looking to perform an operation in behalf of an Account, use the [Account Manager](./accounts/manager) feature instead. Lens Refresh Tokens are valid for **7 days** from the time of issuance. #### ID Token The ID Token is used to verify the user's identity on consumer's side. It contains a set of claims about the user and is signed by the Lens API. Lens ID Tokens are valid for **10 minutes** from the time of issuance, same as the Access Token. You can use the ID Token to verify the user's identity on a backend service like described in the [Consume Lens ID Tokens](#advanced-topics-consume-lens-id-tokens) section. ### Consume Lens ID Tokens As briefly mentioned earlier, Lens ID Tokens can be used to verify's user identity on a backend service. Lens ID Tokens are issued with the following claims: | Claim | Description | | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `sub` | Subject - the `signedBy` address used to sign the Authentication Challenge. This could be the Account or an Account Manager for it. Example: `0xC47Cccc2bf4CF2635a817C01c6A6d965045b06e6`. | | `iss` | Issuer - the Lens API endpoint that issued the token. Typically: `https://api.lens.xyz`. | | `aud` | Audience - the Lens App address that the token is intended for. Example: `0x00004747f7a56EE7Af7237220c960a7D06232626`. | | `iat` | Issued At - the timestamp when the token was issued. | | `exp` | Expiration - the timestamp indicating when the token will expire. This can be used to determine if the token is still valid. | | `sid` | Session ID - the unique identifier of the session that the token was issued for. | | `act` | Optional claim that allows the token to act on behalf of another Account. This is useful for Account Managers to specify the Account address they can act on behalf of. | | `tag:lens.dev,2024:sponsored` | Custom claim that indicates the authenticated session is enabled for sponsored transactions. | | `tag:lens.dev,2024:role` | Custom claim that indicates the role of the authenticated session. Possible values are `ACCOUNT_OWNER`, `ACCOUNT_MANAGER`, `ONBOARDING_USER`, and `BUILDER`. | A typical use case is to use Lens issued ID Token to verify the legitimacy of user's request before issuing your app specific credentials. The following diagram illustrates this flow: >API: Fetch /.well-known/jwks.json API-->>Backend: JWKS note over Backend: Caches JWKS note over User,Backend: Later on… User->>API: Authenticate API->>User: Issues ID Token User->>Backend: Authenticate w/ Lens ID Token note right of Backend: Verify Lens ID Token
w/ cached JWKS Backend-->>User: Issues app credentials `} /> Below is an example of a [Next.js middleware](https://nextjs.org/docs/app/building-your-application/routing/middleware) that demonstrates how to verify a Lens ID Token using the popular [jose](https://www.npmjs.com/package/jose) library: ```javascript filename="middleware.ts" import { NextResponse } from "next/server"; import { jwtVerify, createRemoteJWKSet } from "jose"; // Get JWKS URI from environment variables const jwksUri = process.env.NEXT_PUBLIC_JWKS_URI; const JWKS = createRemoteJWKSet(new URL(jwksUri)); export async function middleware(req) { const token = req.headers.get("authorization")?.split(" ")[1]; if (!token) { return new NextResponse( JSON.stringify({ error: "Authorization token missing" }), { status: 401, headers: { "Content-Type": "application/json" }, }, ); } try { // Verify the JWT using the JWKS const { payload } = await jwtVerify(token, JWKS); // Optionally, attach the payload to the request req.user = payload; // Proceed to the API route return NextResponse.next(); } catch (error) { console.error("JWT verification failed:", error); return new NextResponse( JSON.stringify({ error: "Invalid or expired token" }), { status: 401, headers: { "Content-Type": "application/json" }, }, ); } } export const config = { matcher: ["/api/:path*"], }; ``` The example works under the following assumptions: - The Lens ID Token is passed in the `Authorization` header as a Bearer token (e.g., `Authorization: Bearer `). - The JWKS URI is available in the `NEXT_PUBLIC_JWKS_URI` environment variable. - Your API routes are located under the `/api` path. Adapt it to your specific use case as needed. You can now use the `req.user` object in your API routes to access the user's identity. ```javascript filename="Example API Route" export default function handler(req, res) { // The JWT payload will be available as req.user if the token is valid if (req.user) { return res.status(200).json({ message: "Success", user: req.user }); } else { return res.status(401).json({ error: "Unauthorized" }); } } ``` ================ File: src/pages/storage/resources/changelog.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: false, showNext: false, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Changelog All notable changes to this project will be documented in this page. --- This page collects any major change to Grove API and library. While we will try to keep breaking changes to a minimum, we may need to introduce them as we iterate on the implementations. ## Increased Upload Size Limit The maximum upload size limit has been increased to **125MB**. Update the `@lens-chain/storage-client` package to the latest `1.0.6` version. ## New Edge Infrastructure {/* ### Added */} ### Changed #### Mandatory ACL Whenever you use the `@lens-chain/storage-client` to upload files/folder, you MUST specify an ACL even for the _immutable_ resources. ```diff filename="JSON Upload" -const response = await storageClient.uploadAsJson(data); +const acl = immutable(37111); +const response = await storageClient.uploadAsJson(data, { acl }); ``` ```diff filename="File Upload" -const response = await storageClient.uploadFile(file); +const acl = immutable(37111); +const response = await storageClient.uploadFile(file, { acl }); ``` ```diff filename="Folder Upload" -const response = await storageClient.uploadFolder(input.files); +const acl = immutable(37111); +const response = await storageClient.uploadFolder(input.files, { acl }); ``` #### Generic ACL Syntax Generic ACL now requires a `chain_id` to be specified and it follows a builder pattern. ```diff import { RECOVERED_ADDRESS_PARAM_MARKER } from "@lens-chain/storage-client"; // … -const acl = genericAcl( - "0x1234…", - "someFunction(address user) returns (bool)", - [""], -); +const acl = genericAcl("0x1234…") + .withFunction("someFunction(address user) returns (bool)") + .withChainId(1) + .withParams([RECOVERED_ADDRESS_PARAM_MARKER]) + .build(); ``` #### File Upload Response Edit a file now returns a `FileUploadResponse` like the `uploadFile` method. ```diff -const success = await storageClient.editFile("lens://af5225b…", updated, signer, { acl }); - -if (success) { - console.log("File edited successfully"); -} else { - console.log("Failed to edit file"); -} +try { + const response = await storageClient.editFile("lens://af5225b…", updated, signer, { acl }); + console.log("File edited successfully", response); +} catch (error) { + console.error("Failed to edit file", error); +} ``` {/* ### Removed */} ## New Package Home Install the new package `@lens-chain/storage-client` to interact with Grove. ```bash filename="npm" npm uninstall @lens-protocol/storage-node-client@next npm install @lens-chain/storage-client@next ``` ```bash filename="yarn" yarn remove @lens-protocol/storage-node-client@next yarn add @lens-chain/storage-client@next ``` ```bash filename="pnpm" pnpm remove @lens-protocol/storage-node-client@next pnpm add @lens-chain/storage-client@next ``` Instantiate the client with the following code: ```diff -import { StorageClient, testnet } from "@lens-protocol/storage-node-client"; +import { StorageClient } from "@lens-chain/storage-client"; -const storageClient = StorageClient.create(testnet); +const storageClient = StorageClient.create(); ``` No other changes are required to use the new package. ## Developer Preview Announcement ================ File: src/pages/storage/usage/download.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Downloading Content This guide will walk you through retrieving content from Grove. --- All uploaded contents are world-public readable. Privacy-settings will be implemented in the future. ## Direct Download Given a gateway URL (`https://api.grove.storage/af5225b…`), you can can simply use it to download the file. ```html filename="Link Example" Download ``` ```html filename="Image Example" ``` ## Resolving Lens URIs Given a `lens://af5225b…` URI, you can resolve its content to a URL. Use the `resolve` method to get the URL: ```ts filename="Example" const url = storageClient.resolve("lens://af5225b…"); // url: https://api.grove.storage/af5225b… ``` Replace the `lens://` prefix with the API URL: ```bash curl 'https://api.grove.storage/af5225b…' ``` ================ File: src/pages/storage/usage/upload.mdx ================ export const meta = { showBreadcrumbs: false, showTableOfContents: true, showPrev: true, showNext: true, }; import Layout from "@/components/Layouts/Mdx"; import { routes } from "@/utils/routes"; export default ({ children }) => {children}; {/* Start of the page content */} # Uploading Content This guide will walk you through uploading content to Grove. --- Grove supports both single-file uploads and bulk uploads in the form of folders. Currently, the maximum upload size is **125MB**. This is an initial limit and will be revised in the future. All uploaded content is publicly readable. Privacy settings will be implemented in the future. ## Permission Models Uploaded content on Grove can be **immutable** or **mutable**, depending on the Access Control Layer (ACL) configuration provided during the upload. A single ACL configuration can be used for both **edit** and **delete** actions, or separate configurations can be defined for each action individually. Grove supports four types of ACL configurations. Use this to make the content immutable. The only required parameter is the chain ID, which informs the content retention policy to use. Use this to restrict editing and/or deletion to a specific Lens Account address. **Parameters** | Name | Type | Description | | :------------- | :----------------------------: | :----------------------------------------------------------------------------- | | `chain_id` | `37111\|232` (testnet/mainnet) | Identifies the Lens Chain network where the Lens Account exists. | | `lens_account` | address | The Lens Account address that is authorized to edit and/or delete the content. | The `chain_id` also informs the content retention policy to use. Use this to restrict editing and/or deletion to a specific wallet address. **Parameters** | Name | Type | Description | | :--------------- | :-----: | :----------------------------------------------------------------------- | | `chain_id` | number | Informs the content retention policy to use. | | `wallet_address` | address | The wallet address that is authorized to edit and/or delete the content. | Use this if you want to have full control of your ACL validations. A Generic Contract Call ACL lets content owners define custom validation rules for editing and deleting content, using a smart contract function call for flexible, on-chain access control. **Parameters** | Name | Type | Description | | :----------------- | :------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `chain_id` | integer | Identifies the blockchain network where the contract is deployed. | | `network_type` | string | Specifies the blockchain type. Currently, only `evm` is supported. | | `contract_address` | address | The address of the smart contract responsible for validating permissions. | | `function_sig` | string | The function signature that will be called on the smart contract to determine access. The function MUST return a boolean value. Example: `is_allowed(uin256,address)` | | `params` | string[] | A list of parameters passed to the function, where the `` string MUST be included to specify the parameter that receives the address of the actor initiating the change. Example: `["0x42", ""]` | The `chain_id` also informs the content retention policy to use. Below the list of supported chains. - **Ethereum Mainnet** (`chain_id: 1`) - **Ethereum Sepolia Testnet** (`chain_id: 11155111`) - **Lens Testnet** (`chain_id: 37111`) - **zkSync Mainnet** (`chain_id: 324`) - **zkSync Sepolia Testnet** (`chain_id: 300`) - **Base Mainnet** (`chain_id: 8453`) - **Base Sepolia Testnet** (`chain_id: 84532`) - **Abstract Mainnet** (`chain_id: 2741`) - **Abstract Sepolia Testnet** (`chain_id: 11124`) - **Sophon Mainnet** (`chain_id: 50104`) - **Sophon Testnet** (`chain_id: 531050104`) Content associated with testnets follows different retention policies and may be deleted after a certain period of time. ## Uploading a File To upload a single file, follow these steps. ### Define an ACL First, define the ACL configuration to use. ```ts filename="Immutable" import { chains } from "@lens-chain/sdk/viem"; import { immutable } from "@lens-chain/storage-client"; const acl = immutable(chains.testnet.id); ``` ```ts filename="Lens Account" import { chains } from "@lens-chain/sdk/viem"; import { lensAccountOnly } from "@lens-chain/storage-client"; const acl = lensAccountOnly( "0x1234…", // Lens Account Address chains.testnet.id, ); ``` ```ts filename="Wallet Address" import { chains } from "@lens-chain/sdk/viem";, import { walletOnly } from "@lens-chain/storage-client"; const acl = walletOnly( "0x1234…", // Wallet Address chains.testnet.id, ); ``` ```ts filename="Generic ACL" import { chains } from "@lens-chain/sdk/viem"; import { genericAcl, RECOVERED_ADDRESS_PARAM_MARKER, } from "@lens-chain/storage-client"; const acl = genericAcl(chains.testnet.id) .withContractAddress("0x1234…") .withFunctionSig("someFunction(address)") .withParams([RECOVERED_ADDRESS_PARAM_MARKER]) .build(); ``` ### Upload the File Then, use the `uploadFile` method to upload the file. Let's go through an example. Suppose you have a form that allows users to upload an image file. ```html filename="index.html"
``` In the form’s submit event handler, you can upload the file by passing the `File` reference and the ACL configuration from the previous step: ```ts filename="Upload Example" highlight="7" async function onSubmit(event: SubmitEvent) { event.preventDefault(); const input = event.currentTarget.elements["image"]; const file = input.files[0]; const response = await storageClient.uploadFile(file, { acl }); // response.uri: 'lens://323c0e1ccebcfa70dc130772…' } ``` The response includes: - **`uri`**: A [Lens URI](../resources/glossary#lens-uri) (e.g., `lens://323c0e1c…`). - **`gatewayUrl`**: A direct link to the file (`https://api.grove.storage/323c0e1c…`). - **`storageKey`**: A unique [Storage Key](../resources/glossary#storage-key) allocated for the file. Use **`uri`** for Lens Posts and other Lens metadata objects. Use **`gatewayUrl`** for sharing on systems that do not support Lens URIs.
To upload a single file, follow these steps. ### Request a Storage Key First, request a new unique [Storage Key](../resources/glossary#storage-key). ```bash filename="curl" curl -X POST 'https://api.grove.storage/link/new' ``` ```http filename="HTTP" POST /link/new HTTP/1.1 Host: api.grove.storage Content-Length: 0 ``` This returns an array with a single entry. ```json filename="Response" [ { "storage_key": "323c0e1ccebcfa70dc130772…", "gateway_url": "https://api.grove.storage/323c0e1ccebcfa70dc130772…", "uri": "lens://323c0e1ccebcfa70dc130772…" } ] ``` Where: - **`storage_key`**: A unique [Storage Key](../resources/glossary#storage-key) allocated for the file. - **`gateway_url`**: A direct link to the file (`https://api.grove.storage/lens://323c0e1c…`). - **`uri`**: A [Lens URI](../resources/glossary#lens-uri) (e.g., `lens://323c0e1c…`). ### Define an ACL Next, define the ACL configuration to use. Create an `acl.json` file with the desired content. ```json filename="Immutable" { "template": "immutable", "chain_id": 37111 } ``` ```json filename="Lens Account" { "template": "lens_account", "lens_account": "0x1234…", "chain_id": 37111 } ``` ```json filename="Wallet Address" { "template": "wallet_address", "wallet_address": "0x1234…", "chain_id": 37111 } ``` ```json filename="Generic ACL" { "template": "generic", "chain_id": 37111, "contract_address": "", "function_sig": "someFunction(address)", "params": [""] } ``` ### Upload the File Finally, upload the file using a [multipart POST request](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST#multipart_form_submission) to the following endpoint: ```text https://api.grove.storage/ ``` where `` is the `storage_key` from step 1. Suppose you have a file named `watch_this.mp4` to upload. ```bash filename="curl" curl -X POST 'https://api.grove.storage/323c0e1ccebcfa70dc130772…' \ -F '323c0e1ccebcfa70dc130772…=/path/to/watch_this.mp4;type=video/mp4' \ -F 'lens-acl.json=/path/to/acl.json;type=application/json' ``` ```http filename="HTTP" POST /323c0e1ccebcfa70dc130772… HTTP/1.1 Host: api.grove.storage Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW ------WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; name="323c0e1ccebcfa70dc130772…"; filename="watch_this.mp4" Content-Type: video/mp4 ------WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; name="lens-acl.json"; filename="acl.json" Content-Type: application/json ------WebKitFormBoundary7MA4YWxkTrZu0gW-- ``` **What happens here:** 1. The file `watch_this.mp4` is addressed using the `name=` with the value from step 1. 2. The ACL configuration from step 2 is included as a separate multipart body and addressed under `name=lens-acl.json`. The server may respond with one of the following status codes: - `201 Created`: The file has been propagated to the underlying storage infrastructure. - `202 Accepted`: The file is being saved in the edge infrastructure and will be propagated to the underlying storage infrastructure asynchronously. In both cases, the file is immediately available for download. The response contains an array with a single entry. ```json filename="Response" [ { "storage_key": "323c0e1ccebcfa70dc130772…", "gateway_url": "https://api.grove.storage/323c0e1ccebcfa70dc130772…", "uri": "lens://323c0e1ccebcfa70dc130772…", "status_url": "https://api.grove.storage/status/323c0e1ccebcfa70dc130772…" } ] ``` Where: - **`storage_key`**: A unique [Storage Key](../resources/glossary#storage-key) allocated for the file. - **`gateway_url`**: A direct link to the file (`https://api.grove.storage/lens://323c0e1c…`). - **`uri`**: A [Lens URI](../resources/glossary#lens-uri) (e.g., `lens://323c0e1c…`). - **`status_url`**: A URL to check the file's propagation status. See [Propagation Status](#propagation-status) for more information. Use **`uri`** for Lens Posts and other Lens metadata objects. Use **`gateway_url`** for sharing on systems that do not support Lens URIs.
That's it—your file is now available for download. ## Quick Upload Methods This section covers alternative upload methods designed to improve flexibility and ease of use. ### Uploading as JSON If you need to upload a JSON file, you can use the `uploadAsJson` method to simplify the process. ```ts filename="JSON Upload" highlight="4" import { chains } from "@lens-chain/sdk/viem"; const data = { key: "value" }; const acl = immutable(chains.testnet.id); const response = await storageClient.uploadAsJson(data, { acl }); ``` ### One-Step Upload If you need to upload an immutable file, you can do so directly via the API without first requesting a storage key. This approach simplifies the process by allowing you to send the file in a single request. The `@lens-chain/storage-client` uses this as an internal optimization to avoid unnecessary API round-trips. For example, if you want to upload a file named `watch_this.mp4`, you can do it directly in one step. ```bash filename="curl" curl -s -X POST "https://api.grove.storage/?chain_id=37111" \ --data-binary @watch_this.mp4 \ -H 'Content-Type: video/mp4' ``` ```http filename="HTTP" POST /?chain_id=37111 HTTP/1.1 Host: api.grove.storage Content-Type: video/mp4 ``` **What happens here:** 1. The file `watch_this.mp4` is uploaded directly to the `https://api.grove.storage/` URL. 2. The provided `Content-Type` header determines the type of the file. 3. The query parameter `chain_id` specifies the chain ID used to secure the content as part of an immutable ACL configuration. This is exactly the same as with the full upload process with a multipart request involving an _immutable_ ACL configuration. Like with the full upload process, the server may respond with one of the following status codes: - `201 Created`: The file has been saved in the underlying storage infrastructure. - `202 Accepted`: The file is being saved in the edge infrastructure and will be propagated to the underlying storage infrastructure asynchronously. ```json filename="Response" { "storage_key": "323c0e1ccebcfa70dc130772…", "gateway_url": "https://api.grove.storage/323c0e1ccebcfa70dc130772…", "uri": "lens://323c0e1ccebcfa70dc130772…", "status_url": "https://api.grove.storage/status/323c0e1ccebcfa70dc130772…" } ``` Where the response includes the same fields as in the full upload process. ## Uploading a Folder To upload a folder, follow these steps. ### Define an ACL First, define the ACL configuration to use. This will be applied to all files in the folder. ```ts filename="Immutable" import { chains } from "@lens-chain/sdk/viem"; import { immutable } from "@lens-chain/storage-client"; const acl = immutable(chains.testnet.id); ``` ```ts filename="Lens Account" import { chains } from "@lens-chain/sdk/viem"; import { lensAccountOnly } from "@lens-chain/storage-client"; const acl = lensAccountOnly( "0x1234…", // Lens Account Address chains.testnet.id, ); ``` ```ts filename="Wallet Address" import { chains } from "@lens-chain/sdk/viem"; import { walletOnly } from "@lens-chain/storage-client"; const acl = walletOnly( "0x1234…", // Wallet Address chains.testnet.id, ); ``` ```ts filename="Generic ACL" import { chains } from "@lens-chain/sdk/viem"; import { genericAcl, RECOVERED_ADDRESS_PARAM_MARKER, } from "@lens-chain/storage-client"; const acl = genericAcl(chains.testnet.id) // Chain ID (e.g., Lens Testnet) .withContractAddress("0x1234…") .withFunctionSig("someFunction(address)") .withParams([RECOVERED_ADDRESS_PARAM_MARKER]) .build(); ``` ### Define a Folder Index Next, decide how you want the folder to be indexed. This determines what data will be returned when accessing the folder's URL. Currently, only a JSON representation of the folder's content is supported. You can choose between _static_ and _dynamic_ index files. **Static Index File** Allows you to specify a custom JSON file to be returned. ```ts filename="Example" const content = { name: "My Folder", description: "This is a folder", }; const index = new File([JSON.stringify(content)], "index.json", { type: "text/plain", }); ``` **Dynamic Index File** Generates a JSON file based on the URIs of the individual files. This is usually the best choice for storing the content of a Lens Post with media, as it allows defining a single URI that can be used as the Post's `contentURI`. This streamlines any delete operations, as one can simply delete the resource at that URI, and all associated content will be deleted. ```ts filename="Example" import type { CreateIndexContent, Resource } from "@lens-chain/storage-client"; const index: CreateIndexContent = (resources: Resource[]) => { return { name: "My Folder", files: resources.map((resource) => ({ uri: resource.uri, gatewayUrl: resource.gatewayUrl, storageKey: resource.storageKey, })), }; }; ``` Each `Resource` object contains: - **`uri`**: A [Lens URI](../resources/glossary#lens-uri) (e.g., `lens://323c0e1c…`). - **`gatewayUrl`**: A direct link to the file (`https://api.grove.storage/323c0e1c…`). - **`storageKey`**: A unique [Storage Key](../resources/glossary#storage-key) allocated for the file. Use **`uri`** for Lens Posts media and or Lens Account Metadata pictures. Use **`gatewayUrl`** for sharing on systems that do not support Lens URIs. ### Upload the Files Finally, use the `uploadFolder` method to upload all files in a folder. Let's go through an example. Suppose you have a form that allows users to upload multiple images. ```html filename="index.html" highlight="3"
``` In the form’s submit event handler, you can upload all files by passing the `FileList` reference, along with the ACL configuration and the index configuration from the previous steps: ```ts filename="Upload Example" highlight="6-9" async function onSubmit(event: SubmitEvent) { event.preventDefault(); const input = event.currentTarget.elements["images"]; const response = await storageClient.uploadFolder(input.files, { acl, index, }); // response.folder.uri: 'lens://af5225b6262…' // response.files[0].uri: 'lens://47ec69ef75122…' } ``` The response includes: - **`folder: Resource`**: The `Resource` object representing the uploaded folder. - **`files: Resource[]`**: An array of `Resource` objects, one for each uploaded file.
To upload a folder, follow these steps. ### Request Multiple Storage Keys First, request multiple new unique [Storage Keys](../resources/glossary#storage-key). You will need one for the folder, one for each file you intend to upload, and one for the folder index file (if needed). Let's make some examples: | Files | Index File | # of Storage Keys | | :---- | :--------: | ----------------: | | 2 | No | 3 | | 2 | Yes | 4 | | 3 | No | 4 | | 3 | Yes | 5 | For this example, we will assume you want to upload two files and an index file, requiring a total of four storage keys. ```bash filename="curl" curl -X POST 'https://api.grove.storage/link/new?amount=4' ``` ```http filename="HTTP" POST /link/new?amount=4 HTTP/1.1 Host: api.grove.storage Content-Length: 0 ``` This returns an array with four entries. ```json filename="Response" [ { "storage_key": "323c0e1ccebcfa70dc130772…", "gateway_url": "https://api.grove.storage/323c0e1ccebcfa70dc130772…", "uri": "lens://323c0e1ccebcfa70dc130772…" }, { "storage_key": "0cdb8f668f2eab6f3ed4d620…", "gateway_url": "https://api.grove.storage/0cdb8f668f2eab6f3ed4d620…", "uri": "lens://0cdb8f668f2eab6f3ed4d620…" }, { "storage_key": "3cb07f90c0af565d8009cb88…", "gateway_url": "https://api.grove.storage/3cb07f90c0af565d8009cb88…", "uri": "lens://3cb07f90c0af565d8009cb88…" }, { "storage_key": "47ec69ef75122f8b27590aa20…", "gateway_url": "https://api.grove.storage/47ec69ef75122f8b27590aa20…", "uri": "lens://47ec69ef75122f8b27590aa20…" } ] ``` Where each entry includes: - **`storage_key`**: A unique [Storage Key](../resources/glossary#storage-key) allocated for the resource. - **`gateway_url`**: A direct link to the resource (`https://api.grove.storage/lens://323c0e1c…`). - **`uri`**: A [Lens URI](../resources/glossary#lens-uri) (e.g., `lens://323c0e1c…`). ### Define an ACL Next, define the ACL configuration to use. Create an `acl.json` file with the desired content. ```json filename="Immutable" { "template": "immutable", "chain_id": 37111 } ``` ```json filename="Lens Account" { "template": "lens_account", "lens_account": "0x1234…", "chain_id": 37111 } ``` ```json filename="Wallet Address" { "template": "wallet_address", "wallet_address": "0x1234…", "chain_id": 37111 } ``` ```json filename="Generic ACL" { "template": "generic", "chain_id": 37111, "contract_address": "", "function_sig": "someFunction(address)", "params": [""] } ``` ### Define a Folder Index Next, decide how you want the folder to be indexed. This determines what data will be returned when accessing the folder's URL. Create an `index.json` file with the desired content. ```json filename="Example" { "name": "My Folder", "description": "This is a folder" } ``` You can also use URIs you indent to use for individual files to generate a self-describing index. ```json filename="Example" { "name": "My Folder", "cover": "lens://3cb07f90c0af565d8009cb88…", "video": "lens://0cdb8f668f2eab6f3ed4d620…" } ``` ### Upload the Files Finally, upload all the files using a [multipart POST request](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST#multipart_form_submission) to the following endpoint: ```text https://api.grove.storage/ ``` with the `storage_key` set to one of the storage keys from step 1. Suppose the two files we want to upload are named `watch_this.mp4` and `cover.jpg`. ```bash filename="curl" curl -X POST 'https://api.grove.storage/323c0e1ccebcfa70dc130772…' \ -F '0cdb8f668f2eab6f3ed4d620…=/path/to/watch_this.mp4;type=video/mp4' \ -F '3cb07f90c0af565d8009cb88…=/path/to/cover.jpg;type=image/jpeg' \ -F '47ec69ef75122f8b27590aa20…=/path/to/index.json;type=application/json' \ -F 'lens-acl.json=/path/to/acl.json;type=application/json' ``` ```http filename="HTTP" POST /323c0e1ccebcfa70dc130772… HTTP/1.1 Host: api.grove.storage Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW ------WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; name="0cdb8f668f2eab6f3ed4d620…"; filename="watch_this.mp4" Content-Type: video/mp4 ------WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; name="3cb07f90c0af565d8009cb88…"; filename="cover.jpg" Content-Type: image/jpeg ------WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; name="47ec69ef75122f8b27590aa20…"; filename="index.json" Content-Type: application/json ------WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; name="lens-acl.json"; filename="acl.json" Content-Type: application/json ------WebKitFormBoundary7MA4YWxkTrZu0gW-- ``` **What happens here:** 1. The files `watch_this.mp4` and `cover.jpg` are addressed using the `name=` with other two values from step 1. 2. The index file is addressed using the `name=` with the last available value from step 1. 3. The ACL configuration from step 2 is included as a separate multipart body and addressed under `name=lens-acl.json`. The server may respond with one of the following status codes: - `201 Created`: The folder content has been propagated to the underlying storage infrastructure. - `202 Accepted`: The folder content is being saved in the edge infrastructure and will be propagated to the underlying storage infrastructure asynchronously. In both cases, the folder and its content is immediately available for download. The response contains an array with four entries. ```json filename="Response" [ { "storage_key": "323c0e1ccebcfa70dc130772…", "gateway_url": "https://api.grove.storage/323c0e1ccebcfa70dc130772…", "uri": "lens://323c0e1ccebcfa70dc130772…", "status_url": "https://api.grove.storage/status/323c0e1ccebcfa70dc130772…" }, { "storage_key": "0cdb8f668f2eab6f3ed4d620…", "gateway_url": "https://api.grove.storage/0cdb8f668f2eab6f3ed4d620…", "uri": "lens://0cdb8f668f2eab6f3ed4d620…", "status_url": "https://api.grove.storage/status/0cdb8f668f2eab6f3ed4d620…" }, { "storage_key": "3cb07f90c0af565d8009cb88…", "gateway_url": "https://api.grove.storage/3cb07f90c0af565d8009cb88…", "uri": "lens://3cb07f90c0af565d8009cb88…", "status_url": "https://api.grove.storage/status/3cb07f90c0af565d8009cb88…" }, { "storage_key": "47ec69ef75122f8b27590aa20…", "gateway_url": "https://api.grove.storage/47ec69ef75122f8b27590aa20…", "uri": "lens://47ec69ef75122f8b27590aa20…", "status_url": "https://api.grove.storage/status/47ec69ef75122f8b27590aa20…" } ] ``` Where each entry includes: - **`storage_key`**: A unique [Storage Key](../resources/glossary#storage-key) allocated for the file. - **`gateway_url`**: A direct link to the file (`https://api.grove.storage/lens://323c0e1c…`). - **`uri`**: A [Lens URI](../resources/glossary#lens-uri) (e.g., `lens://323c0e1c…`). - **`status_url`**: A URL to check the resource's propagation status. See [Propagation Status](#propagation-status) for more information. Use **`uri`** for Lens Posts and other Lens metadata objects. Use **`gateway_url`** for sharing on systems that do not support Lens URIs.
That's it—your folder and its content are now available for download. --- ## Fine-Grained ACL As described earlier, edit and delete actions can share the same ACL, but they can also be configured separately. This allows for more granular control, enabling different permissions for modifying and removing content. When integrating directly with the API, you can define two separate ACL files—`acl-edit.json` and `acl-delete.json`—each specifying the desired configurations. These ACLs are then included as separate entries in the multipart request. ```bash filename="curl" curl -X POST 'https://api.grove.storage/323c0e1ccebcfa70dc130772…' \ -F '323c0e1ccebcfa70dc130772…=/path/to/watch_this.mp4;type=video/mp4' \ -F 'lens-acl-edit.json=/path/to/acl-edit.json;type=application/json' \ -F 'lens-acl-delete.json=/path/to/acl-delete.json;type=application/json' ``` ```http filename="HTTP" POST /323c0e1ccebcfa70dc130772… HTTP/1.1 Host: api.grove.storage Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW ------WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; name="323c0e1ccebcfa70dc130772…"; filename="watch_this.mp4" Content-Type: video/mp4 ------WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; name="lens-acl-edit.json"; filename="acl-edit.json" Content-Type: application/json ------WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; name="lens-acl-delete.json"; filename="acl-delete.json" Content-Type: application/json ------WebKitFormBoundary7MA4YWxkTrZu0gW-- ``` **What happens here:** 1. The edit ACL configuration is included as a separate multipart body and addressed under `name=lens-acl-edit.json`. 2. The delete ACL configuration is included as a separate multipart body and addressed under `name=lens-acl-delete.json`. For folder uploads, the provided ACL configurations will apply to all files in the folder. ## Propagation Status Whenever you upload a file or a folder, you can check the status of the resource to see if it has been fully propagated to the underlying storage infrastructure. Checking the status is usually unnecessary unless you need to edit or delete the resource soon after uploading. Persistence typically completes within **5 seconds**. Use the `response.waitForPropagation()` method to wait for the resource to be fully propagated to the underlying storage infrastructure. ```ts filename="Wait Until Persisted" highlight="3" const response = await storageClient.uploadFile(file, { acl }); await response.waitForPropagation(); ``` Use the `https://api.grove.storage/status/` endpoint to check the status of a resource. The `storage_key` is the part of the Lens URI that follows the `lens://` prefix. For example, in `lens://abc123xyz`, the `storage_key` is `abc123xyz`. ```bash filename="curl" curl -X GET 'https://api.grove.storage/status/323c0e1ccebcfa70dc130772…' ``` ```http filename="HTTP" GET /status/323c0e1ccebcfa70dc130772… HTTP/1.1 Host: api.grove.storage ``` The response provides the current status of the resource. ```json filename="Response" { "storage_key": "323c0e1ccebcfa70dc130772…", "status": "done", "progress": 100 } ``` Where: - **`storage_key`**: The storage key of the resource. - **`status`**: The current status, which can be - `new` - new request to be processed - `pending` - the resource is being processed - `done` - the resorce is fully persisted on the cluster - `dirty` - there is an ongoing operation on the resource and the status is not yet updated - `unauthorized` - the ACL criteria were not met - `error_upload` - indicates the upload request was rejected - `error_edit` - indicates the edit request was rejected - `error_delete` - indicates the delete request was rejected - **`progress`**: A percentage indicating the progress of the resource's propagation process. This is not linear and may experience jumps. ================================================================ End of Codebase ================================================================