Create comment typed data

📘

full code repo https://github.com/lens-protocol/lens-api-examples

This API call allows you to get the typed data to then call the withSig method to do a comment on a post from a profile on lens. Comments are still publications and have all the power a Post does.

🚧

This request is protected by authentication

hint: this means it requires an x-access-token header put in the request with your authentication token.

Typed data is a way to try to show the users what they are signing in a more readable format. You can read more about it here.

Constructing that type of data is normally difficult. On the type data, you also need to get the nonce, deadline, contract version, contract address, chain id, and the name of the contract for the signature to be able to be signed and verified.

When using this API the server checks every detail before it generates the typed data. For example: if you try to create typed data on an always failing transaction the server will throw an error in a human-readable form. This is great for debugging but also saves issues with users sending always failing transactions or a mismatch of a bad request.

We will show you the typed data approach using ethers and the API side by side. Keep in mind that with the typed data approach you use the withSig methods which can be called by you with your signature or with that signature any relay could call it for you on your behalf allowing gasless transactions.

API Design

📘

Hot tip

It's super easy to enable modules within your publication using this typed data approach as the server lifts all the encoding and decoding of the modules for you. This allows you to just supply it as you would if you were using a web2 API.

mutation CreateCommentTypedData {
  createCommentTypedData(request: {
    profileId: "0x03",
    publicationId: "0x01-0x01",
    contentURI: "ipfs://QmPogtffEF3oAbKERsoR4Ky8aTvLgBF5totp5AuF8YN6vl",
    collectModule: {
      revertCollectModule: true
    },
    referenceModule: {
      followerOnlyReferenceModule: false
    }
  }) {
    id
    expiresAt
    typedData {
      types {
        CommentWithSig {
          name
          type
        }
      }
      domain {
        name
        chainId
        version
        verifyingContract
      }
      value {
        nonce
        deadline
        profileId
        profileIdPointed
        pubIdPointed
        contentURI
        referenceModuleData
        collectModule
        collectModuleInitData
        referenceModule
        referenceModuleInitData
      }
    }
  }
}
{
  "data": {
    "createCommentTypedData": {
      "id": "b03ab4b8-580c-4c99-84d0-036480f7f0c4",
      "expiresAt": "2022-02-21T14:52:09.000Z",
      "typedData": {
        "types": {
          "CommentWithSig": [
            {
              "name": "profileId",
              "type": "uint256"
            },
            {
              "name": "contentURI",
              "type": "string"
            },
            {
              "name": "profileIdPointed",
              "type": "uint256"
            },
            {
              "name": "pubIdPointed",
              "type": "uint256"
            },
            {
              "name": "referenceModuleData",
              "type": "bytes"
            },
            {
              "name": "collectModule",
              "type": "address"
            },
            {
              "name": "collectModuleInitData",
              "type": "bytes"
            },
            {
              "name": "referenceModule",
              "type": "address"
            },
            {
              "name": "referenceModuleInitData",
              "type": "bytes"
            },
            {
              "name": "nonce",
              "type": "uint256"
            },
            {
              "name": "deadline",
              "type": "uint256"
            }
          ]
        },
        "domain": {
          "name": "Lens Protocol Profile",
          "chainId": 80001,
          "version": "1",
          "verifyingContract": "0x23C1ce2b0865406955Da08F1D31c13fcc3f72A3a"
        },
        "value": {
          "nonce": 0,
          "deadline": 1645455129,
          "profileId": "0x03",
          "profileIdPointed": "0x01",
          "pubIdPointed": "0x01",
          "referenceModuleData": "0x",
          "contentURI": "ipfs://QmPogtffEF3oAbKERsoR4Ky8aTvLgBF5totp5AuF8YN6vl.json",
          "collectModule": "0x2732FfD7f7352c9492089C40A0C3368220a438D4",
          "collectModuleInitData": "0x",
          "referenceModule": "0x0000000000000000000000000000000000000000",
          "referenceModuleInitData": "0x"
        }
      }
    }
  }
}
type Mutation {
  createCommentTypedData(
    request: CreatePublicCommentRequest!
  ): CreateCommentBroadcastItemResult!
}

Request

Let's touch on this request so it's super clear.

profileId - required

You have to pass in a profileId that is mandatory.

publicationId - required

You have to pass in a publicationId that is mandatory.

📘

Did you know...

The publication id is not unique in the smart contract its a counter per each profile. So if @josh posts a publication that will be publication 1 for his profile and then if @josh2 posts a publication that will be publication 1 for his profile. Our backend generates what we call an InternalPublicationId which is built up from {profileId}-{publicationId} creating a unique ID that can be queried against our database. You will see that InternalPublicationId is used on all our responses and also used in any request you which to do.

contentURI - required

The metadata holds the main context of a publication, it holds your content, the media items attached to it, and is the metadata that people get when they collect. Metadata standards are defined here if you want to read what standard we have set. The link passed to use must be able to be called from our server and hold the standards we set out or we will not index the publication.

collectModule - required

Modules are quite complex, each module needs to be encoded in the correct way for the contracts not to throw. We tried to abstract any complex stuff out for you here and allow you to just pass in the params in web2 style.

input CollectModuleParams {
  # The collect free collect module
  freeCollectModule: FreeCollectModuleParams

  # The collect revert collect module
  revertCollectModule: Boolean

  # The collect fee collect module
  feeCollectModule: FeeCollectModuleParams

  # The collect limited fee collect module
  limitedFeeCollectModule: LimitedFeeCollectModuleParams

  # The collect limited timed fee collect module
  limitedTimedFeeCollectModule: LimitedTimedFeeCollectModuleParams

  # The collect timed fee collect module
  timedFeeCollectModule: TimedFeeCollectModuleParams
}

Please note you can only supply one of these if you supply more than one the API will throw. We have to do it this way with optional parameters as GraphQL does not support unions on request yet.

freeCollectModule

This module works by allowing anyone to collect with no fee or no limit or no time. It just allows anyone to collect your publication.

📘

freeCollectModule object constraints

  • followerOnly allow or disable the ability to collect by all profiles or only the followers.

Usage:

{
    "profileId": "0x03",
    "publicationId": "0x01-0x01",
    "contentURI": "ipfs://QmPogtffEF3oAbKERsoR4Ky8aTvLgBF5totp5AuF8YN6vl",
    "collectModule": {
        "freeCollectModule": { 
             "followerOnly": false 
         }
    },
    referenceModule: {
        "followerOnlyReferenceModule": false
    }
 }
revertCollectModule

This module works by disallowing all collects. If set if someone tried to collect from the contract level it would throw and revert.

Usage:

{
    "profileId": "0x03",
    "publicationId": "0x01-0x01",
    "contentURI": "ipfs://QmPogtffEF3oAbKERsoR4Ky8aTvLgBF5totp5AuF8YN6vl",
    "collectModule": {
        "revertCollectModule": true
    },
    "referenceModule": {
        "followerOnlyReferenceModule": false
    }
 }
feeCollectModule

This collect module has no time limit, followers only unlimited mints, and an optional referral fee.

📘

feeCollectModule object constraints

  • unlimited collects can be done
  • currency must be a whitelisted module currency or it will throw an error
  • value which should be passed in as the normal amount not shifted to the decimal places as our server does this for you. So if you want 1 WETH you would enter 1 as a value.
  • recipient is where do you want the funds to go to
  • referralFee is forced here for a clear interface, if you do not want any referral fee put 0. The referral fee is a percent out of 100 so a number is fine but it only supports 2 decimal places aka 10.45 is fine but 10.234 is not. The max amount is 100.
  • followerOnly allow or disable the ability to collect by all profiles or only the followers.

Usage:

{
    "profileId": "0x03",
    "publicationId": "0x01-0x01",
    "contentURI": "ipfs://QmPogtffEF3oAbKERsoR4Ky8aTvLgBF5totp5AuF8YN6vl",
    "collectModule": {
        "feeCollectModule": {
            "amount": {
               "currency": "0xD40282e050723Ae26Aeb0F77022dB14470f4e011",
               "value": "0.01"
             },
             "recipient": "0xEEA0C1f5ab0159dba749Dc0BAee462E5e293daaF",
             "referralFee": 10.5,
             "followerOnly": false 
         }
    },
    "referenceModule": {
        "followerOnlyReferenceModule": false
    }
 }
limitedFeeCollectModule

This collect module has no time limit, follower only limited mints, and an optional referral fee

📘

limitedFeeCollectModule object constraints

  • collect limit is how many you want the max amount to be collected, this is a string number because it can overflow in javascript.
  • currency must be a whitelisted module currency or it will throw
  • value which should be passed in as the normal amount not shifted to the decimal places as our server does this for you. So if you want 1 WETH you would enter 1 as a value.
  • recipient is where do you want the funds to go to
  • referralFee is forced here for a clear interface, if you do not want any referral fee put 0. The referral fee is a percent out of 100 so a number is fine but it only supports 2 decimal places aka 10.45 is fine but 10.234 is not. The max amount is 100 you can enter.
  • followerOnly allow or disable the ability to collect by all profiles or only the followers.

Usage:

{
    "profileId": "0x03",
    "publicationId": "0x01-0x01",
    "contentURI": "ipfs://QmPogtffEF3oAbKERsoR4Ky8aTvLgBF5totp5AuF8YN6vl",
    "collectModule": {
        "limitedFeeCollectModule": {
            "collectLimit": "100000",
            "amount": {
               "currency": "0xD40282e050723Ae26Aeb0F77022dB14470f4e011",
               "value": "0.01"
             },
             "recipient": "0xEEA0C1f5ab0159dba749Dc0BAee462E5e293daaF",
             "referralFee": 10.5,
             "followerOnly": false
         }
    },
    "referenceModule": {
        "followerOnlyReferenceModule": false
    }
 }
limitedTimedFeeCollectModule

This collect module has 24 hours with a fee and optional referral fee, follower only limited mints

📘

limitedTimedFeeCollectModule object constraints

  • time is hardcoded in the contract as 24 hours you can not edit this time
  • collect limit is how many you want the max amount to be collected, this is a string number because it can overflow in javascript.
  • currency must be a whitelisted module currency or it will throw
  • value which should be passed in as the normal amount not shifted to the decimal places as our server does this for you. So if you want 1 WETH you would enter 1 as a value.
  • recipient is where do you want the funds to go to
  • referralFee is forced here for a clear interface, if you do not want any referral fee put 0. The referral fee is a percent out of 100 so a number is fine but it only supports 2 decimal places aka 10.45 is fine but 10.234 is not. The max amount is 100 you can enter.
  • followerOnly allow or disable the ability to collect by all profiles or only the followers.

Usage:

{
    "profileId": "0x03",
    "publicationId": "0x01-0x01",
    "contentURI": "ipfs://QmPogtffEF3oAbKERsoR4Ky8aTvLgBF5totp5AuF8YN6vl",
    "collectModule": {
        "limitedTimedFeeCollectModule": {
            "collectLimit": "100000",
            "amount": {
               "currency": "0xD40282e050723Ae26Aeb0F77022dB14470f4e011",
               "value": "0.01"
             },
             "recipient": "0xEEA0C1f5ab0159dba749Dc0BAee462E5e293daaF",
             "referralFee": 10.5,
             "followerOnly": false
         }
    },
    "referenceModule": {
        "followerOnlyReferenceModule": false
    }
 }
timedFeeCollectModule

This collect module has 24 hours with a fee and optional referral fee, follower only unlimited mints

📘

timedFeeCollectModule object constraints

  • time is hardcoded in the contract as 24 hours you can not edit this time
  • unlimited collects can be done within the time period
  • currency must be a whitelisted module currency or it will throw
  • value which should be passed in as the normal amount not shifted to the decimal places as our server does this for you. So if you want 1 WETH you would enter 1 as a value.
  • recipient is where do you want the funds to go to
  • referralFee is forced here for a clear interface, if you do not want any referral fee put 0. The referral fee is a percent out of 100 so a number is fine but it only supports 2 decimal places aka 10.45 is fine but 10.234 is not. The max amount is 100 you can enter.
  • followerOnly allow or disable the ability to collect by all profiles or only the followers.

Usage:

{
    "profileId": "0x03",
    "publicationId": "0x01-0x01",
    "contentURI": "ipfs://QmPogtffEF3oAbKERsoR4Ky8aTvLgBF5totp5AuF8YN6vl",
    "collectModule": {
        "timedFeeCollectModule": {
            "amount": {
               "currency": "0xD40282e050723Ae26Aeb0F77022dB14470f4e011",
               "value": "0.01"
             },
             "recipient": "0xEEA0C1f5ab0159dba749Dc0BAee462E5e293daaF",
             "referralFee": 10.5,
             "followerOnly": false
         }
    },
    "referenceModule": {
        "followerOnlyReferenceModule": false
    }
 }

referenceModule - required

Modules are quite complex, each module needs to be encoded in the correct way for the contracts not to throw. We tried to abstract any complex stuff out for you here and allow you to just pass in the params in web2 style.

input ReferenceModuleParams {
 # The follower only reference module
 followerOnlyReferenceModule: Boolean
}
followerOnlyReferenceModule

A simple reference module that validates that comments or mirrors originate from a profile owned by a follower.

This is super easy to toggle just pass in the boolean in the followerOnlyReferenceModule property and it turn it on and off for that publication.

Usage:

{
    "profileId": "0x03",
    "publicationId": "0x01-0x01",
    "contentURI": "ipfs://QmPogtffEF3oAbKERsoR4Ky8aTvLgBF5totp5AuF8YN6vl",
    "collectModule": {
        "emptyCollectModule": true
    },
    "referenceModule": {
        "followerOnlyReferenceModule": true
    }
 }

📘

The API will support all modules which get whitelisted as they get approved.

as they do this doc will be updated alongside it.

Full code example

import { apolloClient } from './apollo-client';
import { gql } from '@apollo/client'

const CREATE_COMMENT_TYPED_DATA = `
  mutation($request: CreatePublicCommentRequest!) { 
    createCommentTypedData(request: $request) {
      id
      expiresAt
      typedData {
        types {
          CommentWithSig {
            name
            type
          }
        }
      domain {
        name
        chainId
        version
        verifyingContract
      }
      value {
        nonce
        deadline
        profileId
        profileIdPointed
        pubIdPointed
                referenceModuleData
        contentURI
        collectModule
        collectModuleInitData
        referenceModule
        referenceModuleInitData
      }
     }
   }
 }
`

export const createCommenntTypedData = (createCommentTypedDataRequest) => {
   return apolloClient.mutate({
    mutation: gql(CREATE_COMMENT_TYPED_DATA),
    variables: {
      request: createCommentTypedDataRequest
    },
  })
}
import { ApolloClient, InMemoryCache, HttpLink, ApolloLink } from '@apollo/client'

const httpLink = new HttpLink({ uri: 'https://api-mumbai.lens.dev/' });

// example how you can pass in the x-access-token into requests using `ApolloLink`
const authLink = new ApolloLink((operation, forward) => {
  // Retrieve the authorization token from local storage.
  const token = localStorage.getItem('auth_token');

  // Use the setContext method to set the HTTP headers.
  operation.setContext({
    headers: {
      'x-access-token': token ? `Bearer ${token}` : ''
    }
  });

  // Call the next link in the middleware chain.
  return forward(operation);
});

export const apolloClient = new ApolloClient({
  link: authLink.concat(httpLink),
  cache: new InMemoryCache(),
})

Hooking typed data in with ethers

import { signedTypeData, getAddressFromSigner, splitSignature } from './ethers.service';
import { createCommentTypedData } from './create-comment-typed-data';
import { lensHub } from './lens-hub';

export const createComment = async () => {
  // hard coded to make the code example clear
  const createCommentRequest = {
    profileId: "0x03",
    publicationId: "0x01-0x01",
    contentURI: "ipfs://QmPogtffEF3oAbKERsoR4Ky8aTvLgBF5totp5AuF8YN6vl",
    collectModule: {
        timedFeeCollectModule: {
            amount: {
               currency: "0xD40282e050723Ae26Aeb0F77022dB14470f4e011",
               value: "0.01"
             },
             recipient: "0xEEA0C1f5ab0159dba749Dc0BAee462E5e293daaF",
             referralFee: 10.5
         }
    },
    referenceModule: {
        followerOnlyReferenceModule: false
    }
  };
        
  const result = await createCommentTypedData(createCommentRequest);
  const typedData = result.data.createCommentTypedData.typedData;
  
  const signature = await signedTypeData(typedData.domain, typedData.types, typedData.value);
  const { v, r, s } = splitSignature(signature);
  
  const tx = await lensHub.commentWithSig({
    profileId: typedData.value.profileId,
    contentURI: typedData.value.contentURI,
    profileIdPointed: typedData.value.profileIdPointed,
    pubIdPointed: typedData.value.pubIdPointed,
    referenceModuleData: typedData.value.referenceModuleData,
    collectModule: typedData.value.collectModule,
    collectModuleInitData: typedData.value.collectModuleInitData,
    referenceModule: typedData.value.referenceModule,
    referenceModuleInitData: typedData.value.referenceModuleInitData,
    sig: {
      v,
      r,
      s,
      deadline: typedData.value.deadline,
    },
  });
  console.log(tx.hash);
  // 0x64464dc0de5aac614a82dfd946fc0e17105ff6ed177b7d677ddb88ec772c52d3
  // you can look at how to know when its been indexed here: 
  //   - https://docs.lens.dev/docs/has-transaction-been-indexed
}
import { apolloClient } from './apollo-client';
// this is showing you how you use it with react for example
// if your using node or something else you can import using
// @apollo/client/core!
import { gql } from '@apollo/client'

const CREATE_COMMENT_TYPED_DATA = `
  mutation($request: CreatePublicCommentRequest!) { 
    createCommentTypedData(request: $request) {
      id
      expiresAt
      typedData {
        types {
          CommentWithSig {
            name
            type
          }
        }
      domain {
        name
        chainId
        version
        verifyingContract
      }
      value {
        nonce
        deadline
        profileId
        profileIdPointed
        pubIdPointed
        referenceModuleData
        contentURI
        collectModule
        collectModuleInitData
        referenceModule
        referenceModuleInitData
      }
     }
   }
 }
`

export const createCommentTypedData = (createCommentTypedDataRequest) => {
   return apolloClient.mutate({
    mutation: gql(CREATE_COMMENT_TYPED_DATA),
    variables: {
      request: createCommentTypedDataRequest
    },
  })
}
// this is showing you how you use it with react for example
// if your using node or something else you can import using
// @apollo/client/core!
import { ApolloClient, InMemoryCache, HttpLink, ApolloLink } from '@apollo/client'

const httpLink = new HttpLink({ uri: 'https://api-mumbai.lens.dev/' });

// example how you can pass in the x-access-token into requests using `ApolloLink`
const authLink = new ApolloLink((operation, forward) => {
  // Retrieve the authorization token from local storage.
  // if your using node etc you have to handle your auth different
  const token = localStorage.getItem('auth_token');

  // Use the setContext method to set the HTTP headers.
  operation.setContext({
    headers: {
      'x-access-token': token ? `Bearer ${token}` : ''
    }
  });

  // Call the next link in the middleware chain.
  return forward(operation);
});

export const apolloClient = new ApolloClient({
  link: authLink.concat(httpLink),
  cache: new InMemoryCache(),
})
import { ethers, utils } from 'ethers';
import omitDeep from 'omit-deep';

// This code will assume you are using MetaMask.
// It will also assume that you have already done all the connecting to metamask
// this is purely here to show you how the public API hooks together
export const ethersProvider = new ethers.providers.Web3Provider(window.ethereum);

export const getSigner = () => {
    return ethersProvider.getSigner();
}

export const getAddressFromSigner = () => {
  return getSigner().address;
}

export const init = async() => {
    const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
  return accounts[0];
}

export const signedTypeData = (domain, types, value) => {
  const signer = getSigner();
  // remove the __typedname from the signature!
  return signer._signTypedData(
    omitDeep(domain, '__typename'),
    omitDeep(types, '__typename'),
    omitDeep(value, '__typename')
  );
}

export const splitSignature = (signature) => {
    return utils.splitSignature(signature)
}

export const sendTx = (transaction) => {
  const signer = ethersProvider.getSigner();
  return signer.sendTransaction(transaction);
}
import { getSigner } from './ethers.service';

// lens contract info can all be found on the deployed
// contract address on polygon.
// not defining here as it will bloat the code example
export const lensHub = new ethers.Contract(
  LENS_HUB_CONTRACT_ADDRESS,
  LENS_HUB_ABI,
  getSigner()
)

Did this page help you?