Integrate your Open Action in a website using the widgets SDK

The easiest way to integrate your Smart Post in a website

Our Lens React Widgets SDK (forked from lens-protocol) allows anyone to render a Publication component that supports Smart Posts (open actions).

<Publication
  publicationId={"0x21c0-0x6d"}
  authenticatedProfile={authenticatedProfile || undefined}
  walletClient={walletClient || undefined}
/>

Every open action module that is supported by the SDK renders the appropriate call-to-action, takes in the necessary inputs, and encodes the data necessary for the module's processPublicationAction.

The development flow to get an open action module supported by the SDK is generally

  • deploy your open action to mumbai or polygon, and register the module metadata

  • add a new handler class in src/actions/handlers.ts that implements the abstract HandlerBase class

  • implement the necessary functions, including fetchActionModuleData, getActionModuleConfig, and others.

  • add your handler to the registry in src/actions/registry.ts

  • add a new modal component in src/actions/modals to handle the "act" process - taking input form data, fetching state, and sending the act transaction

  • test that everything works

  • submit a PR to the widgets repo and see your open action featured on MadFi + other front-ends

For the rest of this guide, we'll be looking at our Crosschain Zora Mint action. Try it on testnet:

Getting started

Install the SDK and get it set up with your local Nextjs/React app. There are some important peer dependencies listed in the readme, including a necessary nextjs config.

Add a new handler class

If you look at the src/actions/handlers/ directory, you'll see HandlerBase and then a handler for each supported open action. Instances of these classes will manage everything around your open action, including

  • module metadata (name, description, button labels)

  • fetching any onchain / api data needed to render the correct state

  • how to encode init data + module act data

⚠️ Since this SDK does not export any CreatePost component, it's expected that your app will handle this. You can still include the necessary functions in your handler class, to be imported in your app.

Let's look at the HandlerBase class that all handlers must implement

abstract class HandlerBase {
  constructor(_environment: Environment, profileId: string, publicationId: string, authenticatedProfileId?: string) {
   // setup shared variables
  }

  // fetch any module data you wish to render / use for transactions
  abstract fetchActionModuleData(data: DefaultFetchActionModuleDataParams): Promise<any>;

  // returns data necessary to render info
  abstract getActionModuleConfig(): ActionModuleConfig;

  // returns the form data for module init; as a zod object
  abstract getModuleInitDataSchema(): z.ZodObject<any>;

  // encodes the form data for module init
  abstract encodeModuleInitData(data: any): string;

  // returns the form data for module act; as a zod object
  abstract getModuleActDataSchema(): z.ZodObject<any>;

  // encodes the form data for module act
  abstract encodeModuleActData(data: any): string;

  // returns the label for the act button, based on whatever state you determine outside the class
  abstract getActButtonLabel(): string;
}

You'll see a list of functions and their expected return types, and any custom functions are defined in each handler class. For example, let's look at the implementation for getActionModuleConfig in the ZoraLzMintAction handler

// returns data necessary to render info, including the Lens API-registered module metadata
getActionModuleConfig(): ActionModuleConfig {
  return {
    displayName: `Crosschain Zora Mint`,
    description: 'Mint this crosschain Zora NFT'
    address: {
      mumbai: ZORA_LZ_MINT_TESTNET_ADDRESS,
      polygon: ZORA_LZ_MINT_MAINNET_ADDRESS
    },
    metadata: this.metadata // registered metadata for the action module
  }
}

We assume you're registered your module's metadata with the Lens ModuleRegistry (see their docs).

Another important function to implement is getActButtonLabel. This function is called from within the Publication component to render the "act" button on the bottom right.

getActButtonLabel(): string {
  if (this.hasMinted) return "Minted";
  return "Mint"
}

Notice this function accesses a class variable this.hasMinted which is actually set within the fetchActionModuleData function.

async fetchActionModuleData(data: DefaultFetchActionModuleDataParams): Promise<any> {
  this.authenticatedProfileId = data.authenticatedProfileId;
  this.metadata = await this.lensClient.modules.fetchMetadata({ implementation: this.address });

  // fetch data from the contract
  this.remoteMintData = await this.publicClient.readContract({ ... })
}

Generally speaking, you should view your handler as a client which has all the data + utilities necessary to render a useful modal to the user before sending the act transaction. The full source code for the ZoraLzMintAction handler can be found here.

We didn't go over every single function in the base class, but you can see what types are expected to be returned, and it'll be easier to fill in when you view it from the perspective of the modal.

Adding your handler to the registry

When the Publication component is rendered with publication data (a Lens post) it will look at the open action modules it was initialized with, and check whether the SDK has that address in the registry at src/actions/registry.ts

Here is the meat of that file

// 1. import your handler class
import {
  SIMPLE_COLLECTION_MINT_TESTNET_ADDRESS,
  SIMPLE_COLLECTION_MINT_MAINNET_ADDRESS,
  SimpleCollectionMintAction
} from "./handlers/SimpleCollectionMintAction";
import {
  ZORA_LZ_MINT_TESTNET_ADDRESS,
  ZORA_LZ_MINT_MAINNET_ADDRESS,
  ZoraLzMintAction
} from "./handlers/ZoraLzMintAction";

// 2. add the entry for MAINNET, where `handler` is the exported handler class
const MAINNET = [
  // { address: SIMPLE_COLLECTION_MINT_MAINNET_ADDRESS, handler: SimpleCollectionMintAction, name: "SimpleCollectionMintAction" },
];

// 2. add the entry for TESTNET
const TESTNET = [
  { address: SIMPLE_COLLECTION_MINT_TESTNET_ADDRESS, handler: SimpleCollectionMintAction, name: "SimpleCollectionMintAction" },
  { address: ZORA_LZ_MINT_TESTNET_ADDRESS, handler: ZoraLzMintAction, name: "ZoraLzMintAction" },
];

// 3. you are gucci

Now, in the Publication component you'll find that it uses a hook called useSupportedActionModule

const {
  isActionModuleSupported,
  actionModuleHandler,
  isLoading: isLoadingActionModuleState,
} = useSupportedActionModule(
  environment, // Environment imported from @lens-protocol/client
  publication, // the publication data
  authenticatedProfile?.id, // an authenticated profile (if any)
  walletClient, // a connected wallet client (if any)
  focusedOpenActionModuleName, // the open action to focus on (if any)
);

This hook returns 3 variables

variabletypedescription

isActionModuleSupported

boolean

whether the action module that the publication was initialized is supported by the sdk, ie its address was found in the registry

actionModuleHandler

implementation of HandlerBase

an instance of our handler class

isLoading

boolean

whether we are fetching the state data needed by the module to render the correct info

If isActionModuleSupported is true , that hook is already fetching the state data needed using the implemented function fetchActionModuleData in our handler.

Once isLoading flips to false, we'll see the CTA button rendered on the Publication component.

Add a Modal to handle the "act"

Most of the time, we'll need to show the user more information before expecting them to sign / send any transaction - especially for the more complicated open actions.

For example, with ZoraLzMintAction, we need to show the user the estimated quote for the cost of their crosschain mint - which includes service fees + a relay fee. Moreover, we'll need to show them some information after they've submitted the act transaction.

This is what our modal for ZoraLzMintAction looks like

Again, the input needed for the action module encoded data is defined in the handler class.

const MODULE_ACT_DATA_SCHEMA = z.object({
  currency: z.string(),
  quantity: z.number(),
  quotedAmountIn: z.string(),
  uniFee: z.number().optional().nullable(),
});

type ModuleActDataSchema = z.infer<typeof MODULE_ACT_DATA_SCHEMA>;

// returns a zod object to help render a form
getModuleActDataSchema() {
  return MODULE_ACT_DATA_SCHEMA;
}

// expects an object of the same types to encode for the `processPublicationAction`
encodeModuleActData(data: ModuleActDataSchema): string {
  const quantity = data.quantity || DEFAULT_QTY;
  const uniFee = data.uniFee || DEFAULT_UNI_FEE;

  return encodeData(
    JSON.parse(this.metadata!.metadata.processCalldataABI),
    [data.currency, quantity.toString(), data.quotedAmountIn, uniFee.toString()]
  );
}

The full source code for our modal can be found here, and the general template for the rendered component is

<div className="flex flex-col w-full mt-8">
  {/* Crosschain Zora Mint */}
  <h2 className="text-3xl uppercase text-center font-owners font-bold">
    {actionModuleMetadata.displayName}
  </h2>
  {/* Mint this crosschain Zora NFT */}
  <h2 className="text-lg text-center font-owners font-light mt-2">
    {actionModuleMetadata.description}
  </h2>
  {/* Using state data to render contextual info */}
  {handler.hasMinted && (
  <h2 className="text-lg font-bold text-center font-owners mt-4">
    Already minted 🎉
  </h2>
  )}
  {/* The input form + "act" button */}
  <div>{/** ... */}</div>
</div>

The most important thing this modal does is handle the "act" transaction, and in the case of this ZoraLzMintAction, we can see its handleAct function here broken into 3 steps

const handleAct = async () => {
  setIsMinting(true);
  let toastId;
  try {
    if (toast) toastId = toast.loading('Sending');
    
    // 1. encode the module data
    const encodedActionModuleData = handler.encodeModuleActData({
      currency: currency.address,
      quantity: 1,
      quotedAmountIn: quoteData!.quotedAmountIn.toString()
    });
    // 2. send the act transaction onchain
    const txReceipt: TransactionReceipt = await actOnchain(
      walletClient,
      handler,
      encodedActionModuleData,
      { gasLimit: 750_000 }
    );
    // 3. set state to render success info
    setTxHash(txReceipt.transactionHash);
    if (toast) toastId = toast.success('Sent!', { id: toastId });
  } catch (error) {
    console.log(error);
    if (toast) toastId = toast.error('Failed', { id: toastId });
  }
  setIsMinting(false);
}

Conclusion

If you followed along with this guide, you should have a general idea for the steps necessary to have your open action module supported by the widgets sdk. Of course, the best way to fully grasp all the pieces is to run the project locally and see how the current open actions are implemented.

Both the ZoraLzMintAction and the Promote Social Club actions are part of the SDK and live on MadFi, and you can find their respective handlers and modals in the repo.

Finally, please submit any feedback/issues in the github issues.

Last updated