Gated publications

Lens Protocol now supports the creation of gated content experiences so you can share your garden with just the people you wish. Gated content on Lens allows a trustless way to control content accessibility, as encryption and decryption happen on the client side. The Gated Publications feature uses LIT Protocol decentralized access control behind the scenes.

So what can I do with it?

When you create a new Gated Publication, Lens users can now specify access conditions. Access controls mean that decrypting the publication content and media will only be available to (for example):

  • Users who have collected your publication
  • Your followers
  • Users that own any NFT from a specific collection (examples: a DAO membership NFT, an NFT of a collected publication posted on Lens, a Nouns DAO NFT etc.).
  • Users that own an NFT with a given TokenId, or a range of TokenIds (example: Your first 100 followers)
  • The owner of a specific Lens Profile or EOA address, for exclusive content.
  • Users who own some balance of an ERC20 token (example: People who have more than X stETH or who hold Y amount of a DAO governance token)
  • You can also combine the above using boolean AND and OR conditions.

The possibility of creating gated experiences enables various kinds of interesting use-cases for gating and monetizing your content in the Lens Ecosystem, giving creators and publishers more control over how content is consumed.

Privacy

The hidden metadata stays private end-to-end as it will be encrypted and decrypted only by the user who fulfills the access control condition. The only public piece of information is the encryptionKey which cannot decrypt data by itself, but only when combined with a second key obtained by LIT Protocol, which is only possible when a user successfully satisfies the access conditions.

🚧

Dispatcher concern

Since the Lens API handles metadata upload when using the dispatcher, posting gated content using the dispatcher is currently unavailable for privacy reasons.

How to use

To post or decrypt gated content, you will need the @lens-protocol/sdk-gated NPM package that you can find here. It works on both browser and node.js contexts, and allows you to encrypt metadata and upload them to your preferred storage, as well as decrypting any existing metadata as long as you satisfy the conditions.

First of all, you should know that when posting any gated content, what is actually gated is certain parts of its metadata, specifically the fields:

'content' , 'image', 'media', 'animation_url, 'external_url'

This means that the posting process remains the same as with posting regular content, but in this case, the Lens API indexer picks up on some additional properties that will exist on any encrypted metadata generated by the @lens-protocol/sdk-gated package.

That said, from a client perspective, there are couple of things to take care of. First off, create an instance of the LensSDKGated class.

const client = await LensGatedSDK.create({
  provider: new Web3Provider(window.ethereum), // or a jsonrpcsigner
  signer: someSigner, //from wagmi or a wallet
  env: LensEnvironment.Mumbai,
});

You can choose between Polygon, Mumbai and MumbaiSandbox environments.

You can optionally call the connect method to initialize it with a given address and environment, otherwise it will get called automatically the first time you try to encrypt or decrypt content with the values it gets from your Provider.

Encrypting content

To encrypt content, you need the encryptMetadata method. Gets some metadata, your profileId (to make sure your encrypted data is accessible by you), the access conditions and a file upload handler with the following signature:

(encryptedMetadata: EncryptedMetadata): Promise<string>

This gives you the freedom to reuse your existing code in order to upload metadata to your preferred storage. It's the final argument the encrypted metadata call in the code example below. It's omitted from the code example for clarity.

📘

Keep in mind

Gated content only supports the Metadata V2 format, see here.

import { Web3Provider } from '@ethersproject/providers'
import {AndCondition, OrCondition, FollowCondition, CollectCondition, EncryptedMetadata, EoaOwnership, Erc20TokenOwnership, MetadataV2, NftOwnership, ProfileOwnership, PublicationMainFocus, ContractType, ScalarOperator, LensGatedSDK, LensEnvironment,  } from '@lens-protocol/sdk-gated'

let metadata: MetadataV2 = {
  version: '2.0.0',
  name: 'name',
  description: 'description',
  attributes: [],
  content: 'content',
  metadata_id: '1',
  appId: 'app_id',
  mainContentFocus: PublicationMainFocus.TextOnly,
  locale: 'en',
}

const uploadMetadataHandler = async (data: EncryptedMetadata): Promise<string> => {
  // Upload the encrypted metadata to your server and return a publicly accessible url
  return Promise.resolve('test')
}

const nftAccessCondition: NftOwnership = {
  contractAddress: '0x0000000000000000000000000000000000000000', // the address of the NFT collection, make sure it is a valid address depending on the chosen network
  chainID: 80001, // the chain ID of the network the NFT collection is deployed on;
  contractType: ContractType.Erc721, // the type of the NFT collection, ERC721 and ERC1155 are supported
  tokenIds: ['1', '2', '3'], // OPTIONAL - the token IDs of the NFTs that grant access to the metadata, if ommitted, owning any NFT from the collection will grant access
}

(async () => {
  const sdk = await LensGatedSDK.create({
    provider: new Web3Provider(window.ethereum),
    signer: someSigner,
    env: LensEnvironment.Mumbai,
  });

  // this must be called anytime you change networks, exposed so you can add this to your Web3Provider event handling
  // but not necessary to call explicitly
  await sdk.connect({
    address: '0x1234123412341234123412341234123412341234', // your signer's wallet address
    env: LensEnvironment.Mumbai
  })

  const { contentURI, encryptedMetadata } = await sdk.gated.encryptMetadata(
          metadata,
          '0x01', // the signed in user's profile id
          {
            nft: nftAccessCondition
          }, // or any other access condition object
          uploadMetadataHandler,
  )
  console.log(contentURI)
  console.log(encryptedMetadata)
  // contentURI is ready to be used in the `contentURI` field of your `createPostTypedMetadata` call
  // also exposing the encrypted metadata in case you want to do something with it
  // ... create post using the Lens API ...

})();

{
  "version": "2.0.0",
  "metadata_id": "0e2db793-001d-4bf4-a7e2-04788761d065",
  "description": "Description",
  "content": "... encrypted content",
  "external_url": null,
  "image": null,
  "imageMimeType": null,
  "name": "Name",
  "attributes": [],
  "media": [],
  "appId": "api_examples_github",
  "animation_url": null,
  "encryptionParams": {
    "encryptionProvider": "lit-protocol",
    "accessCondition": {
      "type": "NFT",
      "chainID": 80001,
      "contractAddress": "0x5832bE646A8a7A1A7a7843efD6B8165aC06e360D",
      "contractType": "ERC721",
      "tokenIds": []
    },
    "providerSpecificParams": {
      "encryptionKey": "a loooong hex string that LIT will use to decrypt your content as long as you satisfy the conditions",
    },
    "encryptedFields": {
      "content": "WhpXUao7QU7iQg79UsCw0-ptkoy_fSbfEoqEs6_Zj2s=", // encrypted content
      "image": null,
      "animation_url": null,
      "external_url": null
    }
  },
  "locale": "en-us",
  "tags": [
    "using_api_examples"
  ],
  "mainContentFocus": "TEXT_ONLY"
}

This will give you a contentURI that you can then use on your regular posting flow. It also exposes the encryptedMetadata object because you will need to supply the encryptionKey on your upcoming call to createPostTypedData. Don't worry, this key is not enough to decrypt your data, it is used to query LIT Protocol, which upon verification that you do fulfill the access conditions, will then expose the actual decryption key.

  const createPostRequest = {
    profileId,
    contentURI: 'ipfs://' + contentURI.path,
    collectModule: {
      freeCollectModule: { followerOnly: false },
    },
    referenceModule: {
      followerOnlyReferenceModule: false,
        },
   // ******* added this
    gated: {
      nft: nftAccessConditions,
      encryptedSymmetricKey:
        encryptedMetadata.encryptionParams.providerSpecificParams.encryptionKey,
    },
   // *******
  };

  // the remaining code is the same as in regular posting
  const signedResult = await signCreatePostTypedData(createPostRequest);
  console.log('create post: signedResult', signedResult);

    const typedData = signedResult.result.typedData;

  const { v, r, s } = splitSignature(signedResult.signature);

  const tx = await lensHub.postWithSig({
    profileId: typedData.value.profileId,
    contentURI: typedData.value.contentURI,
    collectModule: typedData.value.collectModule,
    collectModuleInitData: typedData.value.collectModuleInitData,
    referenceModule: typedData.value.referenceModule,
    referenceModuleInitData: typedData.value.referenceModuleInitData,
    sig: {
      v,
      r,
      s,
      deadline: typedData.value.deadline,
    },
  });

Any content you post with the code above will have its metadata fields encrypted and replaced with placeholders. The actual content will only be available to people satisfying the access conditions, after calling decryptMetadata on the @lens-protocol/sdk-gated package.

Decrypting content

Given you have instantiated a LensGatedSDK client with your wallet, and you have fetched some gated publication via the API, you can then simply:

const { error, decrypted } = await sdk.gated.decryptMetadata(encryptedMetadata)
console.log(error) // in case something went wrong or you dont fullfill the criteria
console.log(decrypted) // otherwise, the decrypted MetadataV2 will be here

Access Condition types

Here are the supported access condition types. Check out the remaining examples to see how they are used in action.

📘

Remember!

When you supply a condition to the encryptMetadata function, it takes a AccessConditionOutput object as input, so make sure to wrap it in its corresponding type attribute as described in the following object:

export type AccessConditionOutput = {
  __typename?: 'AccessConditionOutput';
  /** AND condition */
  and?: Maybe<AndConditionOutput>;
  /** Profile follow condition */
  collect?: Maybe<CollectConditionOutput>;
  /** EOA ownership condition */
  eoa?: Maybe<EoaOwnershipOutput>;
  /** Profile follow condition */
  follow?: Maybe<FollowConditionOutput>;
  /** NFT ownership condition */
  nft?: Maybe<NftOwnershipOutput>;
  /** OR condition */
  or?: Maybe<OrConditionOutput>;
  /** Profile ownership condition */
  profile?: Maybe<ProfileOwnershipOutput>;
  /** ERC20 token ownership condition */
  token?: Maybe<Erc20OwnershipOutput>;
};

NFT Ownership

Will evaluate to true if the decryptor owns an NFT from that collection. If tokenIds are not provided, then owning any NFT from the collection will satisfy the condition.

const nftAccessCondition = {
  contractAddress: '0x0000000000000000000000000000000000000000', // the address of the NFT collection, make sure it is a valid address depending on the chosen network
  chainID: 80001, // the chain ID of the network the NFT collection is deployed on;
  contractType: ContractType.Erc721, // the type of the NFT collection, ERC721 and ERC1155 are supported
  tokenIds: ['1', '2', '3'], // OPTIONAL - the token IDs of the NFTs that grant access to the metadata, if ommitted, owning any NFT from the collection will grant access
}

EOA Address Ownership

Evaluates to true if the decryptor has signed in from the provided EOA address.

const eoaAccessCondition: EoaOwnership = {
  address: '0x0000000000000000000000000000000000000000', // the address of the EOA that grants access to the metadata
}

ERC20 Token Ownership

Will evaluate to true if the decryptor satisfies some condition regarding the given ERC20 token.

const erc20AccessCondition: Erc20TokenOwnership = {
  contractAddress: '0x0000000000000000000000000000000000000000', // the address of the ERC20 token that signers should own
  chainID: 80001, // the chain ID of the network
  amount: '1000000000000000000', // the amount of the ERC20 token that grants access to the metadata
  decimals: 18, // the decimals of the ERC20 token that grants access to the metadata
  condition: ScalarOperator.GreaterThanOrEqual // the condition that must be met to grant access to the metadata, supported conditions are: '==', '!=', '>', '<', '>=', '<='
}

Profile Ownership

Will evaluate to true if the decryptor owns that ProfileId

const profileAccessCondition: ProfileOwnership = {  
  profileId: '0x01'  
}

Profile Follow

Will evaluate to true if you follow that ProfileId

const followAccessCondition: FollowCondition = {  
  profileId: '0x01',  
}

Collected publication

Will evaluate to true if the person trying to decrypt the content owns a collected NFT of the given publication. Can take thisPublication=true when encrypting metadata for people who will collect that specific pub. You can use either of these 2 properties, but not both of them.

const collectAccessCondition: CollectCondition = {  
  thisPublication: true,  
}

const collectAccessCondition2: CollectCondition = {  
  publicationId: '0x01-0x01',  
}

Boolean conditions

These support up to 5 conditions and they cannot have nested boolean conditions right now.

const andCondition: AndCondition = {  
  criteria: [  
    {  
      token: erc20AccessCondition  
    },  
    {  
      follow: followAccessCondition  
    }  
  ]  
}

const orCondition: OrCondition = {  
  criteria: [  
    {  
      collect: collectAccessCondition,  
    },  
    {  
      profile: profileAccessCondition  
    }  
  ]  
}