Open Actions

Learn how to leverage the power of Open Actions to create more dynamic content.


Open Actions are Smart Contracts that can be attached to a Lens Publication to expand its functionality.

When a publication is created, the creator has the option to attach any number of Open Actions along with parameters to customize their behaviour.

Throughout this guide, we'll use the variable openActionContract to represent the contract address of the specific Open Action we're discussing.

Discover Modules

This section details how to explore the available Open Action Modules for the protocol.

These modules include known supported modules with detailed information and unknown supported modules with basic contract information.

You can use the client.modules.supportedOpenActionModules method to list all the supported Open Action Modules.

import { LensClient, development } from '@lens-protocol/client';
const client = new LensClient({  environment: development,});
const page = await client.modules.supportedOpenActionModules({  includeUnknown: true,  onlyVerified: true});

The method returns a PaginatedResult<T> where T is KnownSupportedModuleFragment or UnknownSupportedModuleFragment>. For more information on pagination, refer to this guide.


Get Module Metadata

Often, you'll need the information contained in the Module Metadata to utilize an Open Action Module.

import { useLazyModuleMetadata } from "@lens-protocol/react-web";
// ...
const { execute } = useLazyModuleMetadata();
// ... in an async functionconst result = await execute({ implementation: openActionContract });
// handle retrieval errorsif (result.isFailure()) {  console.error(result.error.message);  return;}
const { metadata, sponsoredApproved, signlessApproved, verified } =  result.value;

Unverified modules, denoted by verified: false, have not undergone review by the Lens Protocol team. These modules may contain bugs or malicious code. Avoid integrating them into production unless you fully understand their functionality and risk associated. For more information, refer to the Verified Modules guide.

See Retrieve Module Metadata for details.


Embed Open Actions

This section assumes you have familiariy with the Content Creation process.

First, encode the Open Action initialization data into a DataHexString. You will need the initializeCalldataABI from the Module metadata. The specifics of this data will depend on the chosen Open Action Module.

import { encodeData, ModuleParam } from "@lens-protocol/react-web";
const abi = JSON.parse(metadata.initializeCalldataABI) as ModuleParam[];const calldata = encodeData(abi, [  /* data according to Open Action Module initialization spec */]);

Next, create the publication and link it to the appropriate Open Action.

Keep in mind that a single publication can be linked to multiple Open Actions, providing a variety of interactive options for users.

import { OpenActionType, useCreatePost } from "@lens-protocol/react-web";
// ...
const { execute, loading, error } = useCreatePost();
// ...
const result = await execute({  metadata:    "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",  actions: [    {      type: OpenActionType.UNKNOWN_OPEN_ACTION,      address: openActionContract,      data: calldata,    },  ],});
// continue as usual

Next, follow the usual process for creating on-chain Publications.

Read Initialization Data

Now, let's look at a Publication configured with our Open Action, but from the perspective of a consumer.

Let's assume that you have retrieved the specific Lens Publication data.

import {  isPostPublication,  publicationId,  usePublication,} from "@lens-protocol/react-web";
const { data, error, loading } = usePublication({  forId: publicationId("0x32-0x55"),});
// ...
if (!isPostPublication(data)) {  return null;}
const post = data;

The first step is to locate the Open Action settings within the Publication data.

import { UnknownOpenActionModuleSettings } from "@lens-protocol/react-web";
// ...
const settings = post.openActionModules.find(  (module): module is UnknownOpenActionModuleSettings =>    module.__typename === "UnknownOpenActionModuleSettings" &&    module.contract.address === openActionContract);

If found, the UnknownOpenActionModuleSettings object contains the following information:

  • initializeCalldata: This contains the byte data used to initialize the Open Action Module for this Publication. It can be decoded using the module's metadata.initializeCalldataABI.

  • initializeResultData: This contains the data returned by the initialization process. It can be decoded using the module's metadata.initializeResultDataABI.

  • verified, signlessApproved, and sponsoredApproved are the same values that you can retrieve from the module metadata. They are reported here for convenience.

initializeResultData is only present if the Open Action Module returned data during initialization.

You can decode initializeCalldata and initializeResultData using the ABIs retrieved from the corresponding module metadata:

import { decodeData, ModuleParam } from "@lens-protocol/react-web";
// decode init dataconst initData = decodeData(  JSON.parse(result.metadata.initializeCalldataABI) as ModuleParam[],  settings.initializeCalldata);
// decode init result, if presentconst initResult = decodeData(  JSON.parse(result.metadata.initializeResultDataABI) as ModuleParam[],  settings.initializeResultData);

You can now use the decoded data to tailor the user experience of your application. The specifics of this data will depend on the chosen Open Action.

For instance, this data can be used to inform the user about the configuration of the Open Action Module associated with this Publication.

Executing Open Actions

Now, let's explore how a user would interact with a Publication that's configured with our Open Action.

The user experience you provide will hinge on two factors:

  • The type of authentication used: Profile or Wallet-only

  • The sponsorship status of the Open Action Module, as indicated by the sponsoredApproved flag

If the user is authenticated with a Profile and the Open Action Module is sponsored (i.e., sponsoredApproved: true), the user can execute the Open Action using a Sponsored Transaction. If not, a Self-Funded Transaction is used.

If the Open Action can be executed as a Sponsored Transaction, the signlessApproved flag of the Open Action Module will determine whether a Signless Experience can be utilized.

Preparing Process Data

Regardless of the chosen path, you'll need to prepare the Open Action Module process data. This preparation is the same for both Self-funded and Sponsored execution, so we'll only demonstrate it once.

Encode the Open Action process data into a DataHexString. The processCalldataABI from the Open Action Module metadata is required for this step. The specifics of this data will depend on the Open Action you've selected.

import { encodeData, ModuleParam } from "@lens-protocol/react-web";
const abi = JSON.parse(metadata.processCalldataABI) as ModuleParam[];const calldata = encodeData(abi, [  /* data according to Open Action process spec */]);

Sponsored Execution

If you've arrived at this point, it means you've logged in with a Profile and the Open Action Module is sponsored.

The useOpenAction hook allows you to execute the Open Action on the target Publication.

The hook prioritizes the Signless Experience when available; if not, it resorts to a signed experience.

Available in @lens-protocol/react-web and @lens-protocol/react-native

OpenActionButton.tsx
import { AnyPublication, OpenActionKind, useOpenAction } from '@lens-protocol/react-web';
type OpenActionButtonProps = {  /**   * The Open Action contract address   */  address: string;  /**   * The process calldata encoded as a hex string   */  data: string;  /**   * The Publication to execute the Open Action on   */  publication: AnyPublication;}
export function OpenActionButton(props: OpenActionButtonProps) {  const { execute, loading } = useOpenAction({    action: {      kind: OpenActionKind.UNKNOWN,      address: props.address,      data: props.data,    }  });
  const run = async () => {    // execute the Open Action    const result = await execute({      publication: props.publication,    });
    // handle relaying errors    if (result.isFailure()) {      window.alert(result.error.message);      return;    }
    // optional: wait for the transaction to be mined and indexed    const completion = await result.value.waitForCompletion();
    // handle minining/indexing errors    if (completion.isFailure()) {      window.alert(completion.error.message);      return;    }
    window.alert("Open Action executed");  };
  return (    <button onClick={run} disabled={loading}>      Execute    </button>  );}

Self-Funded Execution

If you've reached this point, it means you've either logged in with just a Wallet, or the Open Action Module is not sponsored.

The useOpenAction hook enables the execution of the Open Action on the target Publication using a self-funded approach.

The difference with the sponsored approach is minimal so we will show only the relevant parts.

Example
const { execute, loading } = useOpenAction({  action: {    kind: OpenActionKind.UNKNOWN,    address: props.address,    data: props.data,  }});
// ...
// execute the Open Actionconst result = await execute({  publication: props.publication,  sponsored: false});
// continue as usual

We utilized the sponsored flag to force a direct contract call to the PublicActProxy contract. As a result, the gas cost for the corresponding transaction is charged to the user's wallet.


Creating Open Actions

In this guide, we will use a hypothetical Tipping Action as an example to demonstrate the creation of an Open Action Module.

Once you've finished reading this guide, you can refer to Registering a Module to learn how to register your module with the protocol.

Quick Start

Start experimenting with open actions by running:

npx create-open-action

When It's Worthwhile

Open Actions are advantageous when you want to associate the origin of an action with a publication, potentially distributing rewards to users who contributed to the action.

Let's consider two profiles: lens/alice and lens/bob. Imagine lens/alice has made an outstanding post and lens/bob wants to tip them.

While this interaction could be facilitated through a simple ERC-20 transfer, a Tipping Action can enrich the experience:

  • Provenance: Open Actions are executed by Lens Profiles. As a result, lens/alice can see that lens/bob has tipped them, rather than receiving a tip from an unidentified address.

  • Stats: Using a Tipping Open Action allows Lens Indexers to associate the ERC-20 transfer with the publication where the action was executed. This enables lens/alice to monitor a tipping revenue stream, identify top tippers, and derive other useful statistics.

  • Referrals: Open Actions enable rewards through a referral system. For example, lens/alice can configure the tipping action to reward users who interact with or share their publications by giving them a percentage of the tip if they contributed to its occurrence.
    So, if lens/carl quotes lens/alice's publication, and lens/bob discovers lens/alice's publication because of lens/carl's quote, then when lens/bob tips lens/alice, lens/carl receives a portion of it as a commission.

Here are some additional examples of Open Actions:

  • Minting an NFT from a public collection, as recommended or announced in a publication.

  • Buying an NFT that a publication has listed for sale.

  • Casting a vote in a poll that a publication features.

  • Joining an on-chain raffle that a publication promotes.

The Basics

To create a new Open Action, you must construct a smart contract that adheres to the publication action module interface.

/** * @title IPublicationAction * @author Lens Protocol * * @notice This is the standard interface for all Lens-compatible Publication Actions. * Publication action modules allow users to execute actions directly from a publication, like: *  - Minting NFTs. *  - Collecting a publication. *  - Sending funds to the publication author (e.g. tipping). *  - Etc. * Referrers are supported, so any publication or profile that references the publication can receive a share from the * publication's action if the action module supports it. */interface IPublicationActionModule {    /**     * @notice Initializes the action module for the given publication being published with this Action module.     * @custom:permissions LensHub.     *     * @param profileId The profile ID of the author publishing the content with this Publication Action.     * @param pubId The publication ID being published.     * @param transactionExecutor The address of the transaction executor (e.g. for any funds to transferFrom).     * @param data Arbitrary data passed from the user to be decoded by the Action Module during initialization.     *     * @return bytes Any custom ABI-encoded data. This will be a LensHub event params that can be used by     * indexers or UIs.     */    function initializePublicationAction(        uint256 profileId,        uint256 pubId,        address transactionExecutor,        bytes calldata data    ) external returns (bytes memory);
    /**     * @notice Processes the action for a given publication. This includes the action's logic and any monetary/token     * operations.     * @custom:permissions LensHub.     *     * @param processActionParams The parameters needed to execute the publication action.     *     * @return bytes Any custom ABI-encoded data. This will be a LensHub event params that can be used by     * indexers or UIs.     */    function processPublicationAction(        Types.ProcessActionParams calldata processActionParams    ) external returns (bytes memory);}

The interface mentioned above involves the following types:

/** * @notice An enum specifically used in a helper function to easily retrieve the publication type for integrations. * * @param Nonexistent An indicator showing the queried publication does not exist. * @param Post A standard post, having an URI, action modules and no pointer to another publication. * @param Comment A comment, having an URI, action modules and a pointer to another publication. * @param Mirror A mirror, having a pointer to another publication, but no URI or action modules. * @param Quote A quote, having an URI, action modules, and a pointer to another publication. */enum PublicationType {    Nonexistent,    Post,    Comment,    Mirror,    Quote}
/** * @notice A struct containing the parameters required for the execution of Publication Action via `act()` function. * * @param publicationActedProfileId publisher of the publication that is being acted on * @param publicationActedId ID of the publication that is being acted on * @param actorProfileId profile of the user who is acting on the publication * @param actorProfileOwner owner of the profile of the user who is acting on the publication * @param transactionExecutor address that is executing the transaction * @param referrerProfileIds profile ids array of the referrer chain * @param referrerPubIds publication ids array of the referrer chain * @param referrerPubTypes publication types array of the referrer chain * @param actionModuleData arbitrary data to pass to the actionModule if needed */struct ProcessActionParams {    uint256 publicationActedProfileId;    uint256 publicationActedId;    uint256 actorProfileId;    address actorProfileOwner;    address transactionExecutor;    uint256[] referrerProfileIds;    uint256[] referrerPubIds;    Types.PublicationType[] referrerPubTypes;    bytes actionModuleData;}

The interface consists of two functions, each serving a specific purpose:

  • initializePublicationAction is invoked when a publication using this action is being published. Its role is to initialize any state that the publication action module may require.

  • processPublicationAction is triggered when a profile activates the action. It contains the logic of the action itself and executes it.

Note that core validation rules, such as the existence of the publication and the actor profile, are handled by the LensHub contract before delegating execution to the publication action module contract.

Example

To demonstrate let's consider a tipping action. This action allows any user to enable tipping on their publication. They can specify a custom receiver for the funds, which could be an individual, organization, social cause, charity, etc.

Initialization

This function decodes the tip receiver's address from the custom initialization data and associates it with the publication being initialized. Each time a publication is published, the LensHub contract emits an event. This event contains all the information needed to reconstruct the publication action initialization state off-chain.

function initializePublicationAction(  uint256 profileId,  uint256 pubId,  address /* transactionExecutor */,  bytes calldata data) external override onlyHub returns (bytes memory) {  address tipReceiver = abi.decode(data, (address));
  _tipReceivers[profileId][pubId] = tipReceiver;
  return data;}

Processing

function processPublicationAction(    Types.ProcessActionParams calldata params) external override onlyHub returns (bytes memory) {  (address currency, uint96 tipAmount) = abi.decode(params.actionModuleData, (address, uint96));
  if (!MODULE_GLOBALS.isCurrencyWhitelisted(currency)) {    revert CurrencyNotWhitelisted();  }
  if (tipAmount == 0) {    revert TipAmountCannotBeZero();  }
  address tipReceiver = _tipReceivers[params.publicationActedProfileId][params.publicationActedId];
  IERC20(currency).transferFrom(    params.transactionExecutor,    tipReceiver,    tipAmount  );
  return abi.encode(tipReceiver, currency, tipAmount);}

What's happening here?

  1. decode the currency and tip amount from the custom data included in the action execution parameters

  2. check that the currency is allowlisted, as well as that the tip amount is not zero

  3. get the receiver address associated with the publication where the action is performed on

  4. execute the funds transfer

  5. return the tip receiver, currency and tip amount as custom data

Full contract

https://polygonscan.deth.net/address/0x22cb67432C101a9b6fE0F9ab542c8ADD5DD48153


Best Practices

Deployment Checklist

Permissions Model

The functions of the Open Action Module are assumed to be called ONLY by the LensHub contract, which is the entry point for every protocol interaction.

For that, we provide a convenient HubRestricted.sol base contract that includes the onlyHub modifier, which restricts the call to the LensHub address passed on the constructor.

Verified Modules

To enable an Open Action to be discovered and interacted with through the Lens API, the module should be added to the verified modules list.

Public Act Proxy

The Public Act Proxy is a smart contract which enables any address to execute an Open Action without needing a Lens Profile. When an action is performed through the Public Act Proxy, the values of actorProfileId and transactionExecutor are used to represent the public contract, so in order to support this interaction it is recommended to follow the pattern of the Lens Collect Action and pass the executing address as a parameter.

If an Open Action is performing actions such as token transfer, minting NFT to an address, etc. it is recommended to use this custom parameter to denote the address performing the action instead of transactionExecutor to allow the Public Act Proxy to perform actions on any addresses behalf.

Integrating Open Actions

Open Actions explicitly define smart contract calls to be executed, but are un-opinionated with how they are displayed within frontend integrations.

In order for a Lens application to be able to initialize and execute an Open Action, the application needs context for how to properly display the create and execute steps.

Embed Step

There are a variety of ways that an application can attach an Open Action to a publication including:

  • Background: An example of this would be if a user's publication has an embedded URL, the application can determine the action a user wants to take such as minting an NFT and automatically embed this action into the publication.

  • Checkbox: The free collect Open Action is an example of this, where a user checks an option to add a certain functionality to the publication.

  • Custom User Inputs: The tipping Open Action is an example of this, where a user is inputting the tip receiver address. This concept can be extended to add any additional context that an action might need to make it unique to a user's intents.

Execution Step

The execution step typically involves a call-to-action to initiate the act, actWithSig, or publicFreeAct call (the latter is used when executing without a Lens Profile). The integration may also require additional user inputs, such as amount inputs or checkboxes, or additional context about the action being executed.

Additional context for execution is supplied when an application identifies a specific UnknownOpenActionModuleSettings using its contract.address. This allows the application to decode initializeCalldata and initializeResultData, and determine the information to be displayed.

Examples

Examples of frontend integrations include the tipping Open Action PR and MadFi SDK.


Resources