Guides

Integrate Modules

If you want to integrate a module in your dApp, you have all the information you need to do this without even speaking to the module creator.

Understanding the module metadata

We talked about the Module Metadata Standards, which talks about what information the module will expose to allow the integrators to know how to set them up and also allow consumers to execute them.

You can retrieve Module Metadata of registered modules by:

const result = await client.modules.fetchMetadata({
  implementation: '0x345Cc3A3F9127DE2C69819C2E07bB748dE6E45ee'
});

if (result === null) {
  // specified address is not registered
  return
}

// use result
query {
  moduleMetadata(request: { implementation: "0x345Cc3A3F9127DE2C69819C2E07bB748dE6E45ee" }) {
    metadata {
      name
      title
      description
      authors
      initializeCalldataABI
      initializeResultDataABI
      processCalldataABI
      attributes {
        type
        key
        value
      }
    }
    moduleType
    verified
    signlessApproved
    sponsoredApproved
  }
}

The ModuleMetadataResult object contains important informations about a given module.

type ModuleMetadataResultFragment = {
  moduleType: ModuleType;
  signlessApproved: boolean;
  sponsoredApproved: boolean;
  verified: boolean;
  metadata: {
    authors: string[];
    description: string;
    initializeCalldataABI: string;
    initializeResultDataABI: string | null;
    name: string;
    processCalldataABI: string;
    title: string;
    attributes: MetadataAttribute;
  };
};

type MetadataAttribute = {
  type: MetadataAttributeType;
  key: string;
  value: string;
}

enum ModuleType {
  Follow = 'FOLLOW',
  OpenAction = 'OPEN_ACTION',
  Reference = 'REFERENCE',
}
  • moduleType specify the type of the module between FOLLOW, OPEN_ACTION, REFERENCE
  • signlessApproved tells you if you can execute its logic via Lens Profile Manager. Each module type belong to their workflow so:
    • for Open Action module via the actOnOpenAction mutation
    • for Follow module via the follow mutation
    • for Reference module via commentOnchain in the case of comments
  • sponsoredApproved tells you if tx gas costs can be subsidized by the Lens API via the broadcastOnChain mutation
  • verified tells you the module has been verified in https://github.com/lens-protocol/verified-modules/tree/master and has been approved safe for usage by the Lens team.
  • metadata contains the information the author provided via the Module Metadata Standard.

Of the metadata fields thename, title, authors, description, attributes provides useful information to developers on the module capabilities, and a way to contact the author(s).

For all operational needs the main thing you care about here as a consumer are the ABIs. ABIs are ways to decode and encode data so the chain can understand it.

These values are strings containing Solidity JSON ABI which describe how to encode/decode simple byte arrays (buffers of data).

  • initializeCalldataABI: describes how to encode the initialization data when setting up the module.
  • initializeResultDataABI: describes how to decode the initialization result to so know contract specific outcome at initialization time. This is optional and some modules can leverage it to return you some more informations.
  • processCalldataABI: describes how to encode the execution data when the module logic is ultimately executed.

To aid the process of encoding/decoding data we provided some helper functions as part of the @lens-protocol/client package which will be used in the following examples.

import { encodeData, decodeData } from '@lens-protocol/client';

Using custom modules in your app

Every module type has an Unknown<type>ModuleSettings object:

  • UnknownOpenActionModuleSettings
  • UnknownFollowModuleSettings
  • UnknownReferenceModuleSettings

which is returned by the API as part of the relevant objects.

This object holds information on the data used to set up this module.

In the following, we use Open Actions as example but Follow modules, and Reference modules adopt the same consuming process.

Consuming a custom modules

If a publication is linked to a custom Open Action module and you want to integrate that module in your dApp, you will get information you care about in the correspondingUnknownOpenActionModuleSettings which is returned as part of the openActionModules array in the publication object.

The example below fetches it from a single publication query, but this is included anywhere a publication is brought back.

const post = await client.publication.fetch({
  forId: '0x32-0x1d',
});
query {
  publication(request: {
    forId: "0x32-0x1d"
  }) {
    ... on Post {
      openActionModules {
        ... on UnknownOpenActionModuleSettings {
          initializeCalldata
          initializeResultData
          verified
          signlessApproved
          sponsoredApproved
          type
          contract {
            address
            chainId
          }
          collectNft
        }
      }
    }
  }
}

You can use the @lens-protocol/client type guards to help you find and narrow down to desired settings.

import {isUnknownOpenActionModuleSettings, UnknownOpenActionModuleSettingsFragment } from '@lens-protocol/client';

const openActionContract = '0xc0ffee254729296a45a3885639AC7E10F9d54979';

const settings = publication.openActionModules.find(
  (module): module is UnknownOpenActionModuleSettingsFragment =>
    isUnknownOpenActionModuleSettings(module) && module.contract.address === openActionContract,
);

The UnknownOpenActionModuleSettingsobject looks like this:

type UnknownOpenActionModuleSettings = {
  type: OpenActionModuleType;
  collectNft: EvmAddress | null;
  initializeCalldata: string | null;
  initializeResultData: string | null;
  signlessApproved: boolean;
  sponsoredApproved: boolean;
  verified: boolean;
  contract: NetworkAddress;
};
{
  "initializeCalldata": "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001a0000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003d1f8a6d6584a1672d2817368783b9a2a36ae361000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000006aaf7c8516d0c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000566d63f1cc7f45bfc9b2bdc785ffcc6f858f0997000000000000000000000000f87b6343c172720ac9cc7d1c9465d63454a8ef3000000000000000000000000007b722856369f6b923e1f276abca58dd3d15243d0000000000000000000000000000000000000000000000000000000000000035697066733a2f2f516d51446455596779615975777a6e4b646131797236716255514e32676334567659684b5a3773765344417061730000000000000000000000",
  "initializeResultData": "0x000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000006aaf7c8516d0c00000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000566d63f1cc7f45bfc9b2bdc785ffcc6f858f0997000000000000000000000000f87b6343c172720ac9cc7d1c9465d63454a8ef3000000000000000000000000007b722856369f6b923e1f276abca58dd3d15243d0000000000000000000000000000000000000000000000000000000000000035697066733a2f2f516d51446455596779615975777a6e4b646131797236716255514e32676334567659684b5a3773765344417061730000000000000000000000",
  "verified": false,
  "signlessApproved": false,
  "sponsoredApproved": false,
  "type": "UnknownOpenActionModule",
  "contract": {
    "address": "0x23Bace2E9571B7A8598c3314e5f0d8C12DBc674A",
    "chainId": 80001
  },
  "collectNft": null
}
  • initializeCalldata: contains the data as bytes used to initialize the Open Action module on this Publication. It can be decoded with the module metadata.initializeCalldataABI.
  • initializeResultData: contains the data returned the initialization process. It can be decoded with the module metadata.initializeResultDataABI.
  • verified, signlessApproved, and sponsoredApproved are the same you can retrieve from the module metadata and reported here for convenience.

With the ABIs retrieved from the corresponging Module Metadata you can decode initializeCalldata and initializeResultData like so:

import { decodeData } from '@lens-protocol/client';

const settings = ... // retrieved from a publication like explained before

const result = await client.modules.fetchMetadata({
  implementation: openActionContract
});

// decode init data
const initData = decodeData(
  JSON.parse(result.metadata.initializeCalldataABI),
  settings.initializeCalldata
);

// decode init result if present
const initResult = decodeData(
  JSON.parse(result.metadata.initializeResultDataABI),
  settings.initializeResultData
);

Depending on the Open Action used you can use these informations to inform the user about the specific configuration of the Open Action for the current publication.

Processing a custom module

The above explains how you consume an Open Action custom modules. In the following we will show how to process the Open Action logic.

First of all you need to assemble the process calldata.

import { encodeData } from '@lens-protocol/client';

const openActionContract = '0xc0ffee254729296a45a3885639AC7E10F9d54979';

const result = await client.modules.fetchMetadata({
  implementation: openActionContract
});

const calldata = encodeData(
  JSON.parse(result.metadata.processCalldataABI),
  [ /* data according to ABI spec */ ]
)

Then you can process the Open Action:

const result = await client.publication.actions.actOn({
  actOn: {
    unknownOpenAction: {
      address: openActionContract,
      data: calldata
    }
  },
  for: post.id,
});

// continue as usual
mutation {
  actOnOpenAction(request: {
    for: "0x32-0x1d",
    actOn: {
      unknownOpenAction: {
        address: "0xc0ffee254729296a45a3885639AC7E10F9d54979",
        data: "0x0000000000000000000000000000000001"
      }
    }
  }) {
    ... on RelaySuccess {
      txHash
      txId
    }
    ... on LensProfileManagerRelayError {
      reason
    }
  }
}

The example above assumes that your profile has Lens Profile Manager enabled and that the given Open Action module has signlessApproved set to true.

If you don't want to use a signless experience you can also process the Open Action by creating typed data and broadcasting it manually like explained in the Open Actions section.

You can find an e2e example with all the steps above here.

As said before we focused on Open Action module but the same mechanics applies to Follow and Reference modules in their respective use cases.

Attaching a custom module

With the above information, you can consume and process a custom module. You understand how to decode and encode the data with the ABIs and how it holds together. You now may want to allow your users to be able to use the module for their own content/profile. As before we will focus on Open Action module in the examples.

The first step requires to assemble the initialization calldata.

import { encodeData } from '@lens-protocol/client';

const openActionContract = '0xc0ffee254729296a45a3885639AC7E10F9d54979';

const result = await client.modules.fetchMetadata({
  implementation: openActionContract
});

const calldata = encodeData(
  JSON.parse(result.metadata.initializeCalldataABI),
  [ /* data according to ABI spec */ ]
)

The you can create the publication with the desired Open Action.

import { encodeData } from '@lens-protocol/client';

const result = await client.publication.postOnchain({
  contentURI: 'ipfs://Qm...', // or arweave

  openActionModules: [
    {
      unknownOpenAction: {
        address: openActionContract,
        data: calldata
      }
    }
  ]
});

// continue as usual
mutation {
  postOnchain(request: {
    contentURI: "ar://ID",
    openActionModules: [
      {
        unknownOpenAction: {
          address: "0xc0ffee254729296a45a3885639AC7E10F9d54979",
          data: "0x0000000000000000000000000000000002"
        }
      }
    ]
  }) {
    ... on RelaySuccess {
      txHash
      txId
    }
    ... on LensProfileManagerRelayError {
      reason
    }
  }
}

The example above assumes that your profile has Lens Profile Manager enabled and that the given Open Action module has signlessApproved set to true.

If you don't want to use a signless experience you can also create the publication by creating typed data and broadcasting it manually like explained in the Create a post section.

The same mechanics applies to custom Reference and Follow modules in their respective use cases.