Open Action | Cross-chain Zora Mint

Mint Zora NFTs from any chain, right from your Lens feed or website

Overview

This is a technical guide on how to integrate our ZoraLzMintActionV1 open action. More details about the open action can be found here: Crosschain Zora Mint

ℹī¸ It is arguably easier to simply process the open action on your client or website, and rely on the MadFi web app to initialize these kinds of posts (MadFi v2 out early Jan. 2024). A future guide will be released for initializing, but this guide will focus on two ways to process our open action

  • EASIER: with the Publication component exported from @madfi/widgets-react

  • MORE WORK: fetching the open action metadata to act on the publication via the Lens API

💰Any client that processes our Zora open action is eligible for Zora Mint Referral Rewards as long as the correct referrer data is passed in. Details around this are in step 4.

Using the Lens Widgets SDK

Our exported Publication component handles rendering the social post and processing the act.

Install the necessary dependencies

yarn add @lens-protocol/client @madfi/widgets-react wagmi

This is what it looks like to import and render the component

import { production } from "@lens-protocol/client";
import { Publication, Theme, ProfileFragment } from "@madfi/widgets-react";

// within your nextjs page / react component
<Publication
  publicationId={"0x01a6-0x01ae"}
  theme={Theme.dark}
  environment={production}
  walletClient={walletClient || undefined}
  authenticatedProfile={authenticatedProfile as ProfileFragment || undefined}
  appDomainWhitelistedGasless={true}
/>

That will render the component seen at the top of this guide. Notice we pass in three extra props

  1. walletClient is the connected wallet client returned from wagmi's useWalletClient hook

  2. authenticatedProfile is an object of type ProfileFragment which is the lens profile that is authenticated in your app; check the Lens docs for authenticating

  3. appDomainWhitelistedGasless signals that your app is whitelisted with the Lens API to use the gasless API

🚀 That's it! As long as those props are passed in, and the publication was initialized with this open action, the `Mint` button will be rendered and will handle the act - onchain or with the gasless API

Of course if you want to handle all reactions, render whether the profile has reacted, etc - you can pass in more props. The full Publication component source code can be found on github.

Process a Publication with the Lens API

Using the Lens module metadata and broadcast APIs to integrate our open action offers you complete control when it comes to rendering your own components and processing the act. It's arguably more work, so we'll illustrate the steps required.

Generally speaking, it looks something like this

  • Fetch the open action module metadata from the Lens API (using @lens-protocol/client)

  • Fetch the onchain data necessary to process from polygon and the remote chain (ie Zora, Base)

  • Get the desired payment currency from the user (or default to WETH) and fetch an onchain quote for the swap to pay the sales price + fees in the desired currency

  • Have the user approve the fee transfer to the module contract

  • Encode the data needed to process the act

  • Send the act transaction using typed data + the Lens broadcast API

1. Fetch the open action module metadata

Our ZoraLzMintActionV1 action module is deployed and verified on polygon: https://polygonscan.com/address/0x5f377e3e9BE56Ff72588323Df6a4ecd5cEedc56A#code

It's also a registered/verified Lens module - so you can query the metadata for it using the lens client like so

import { production, LensClient } from "@lens-protocol/client";

const ZORA_LZ_MINT_ACTION = "0x5f377e3e9BE56Ff72588323Df6a4ecd5cEedc56A";

const lensClient = new LensClient({ environment: production });

const data = await lensClient.modules.fetchMetadata({
  implementation: ZORA_LZ_MINT_ACTION
});

const { metadata } = data;

console.log(metadata)

// {
//   "metadata": {
//     "attributes": [],
//     "authors": [
//       "carlos@madfinance.xyz"
//     ],
//     "description": "https://docs.madfi.xyz/protocol-overview/smart-posts/crosschain-zora-mint",
//     "initializeCalldataABI": "[{\"type\":\"address\",\"name\":\"remoteContract1155\"},{\"type\":\"uint256\",\"name\":\"remoteTokenId\"},{\"type\":\"uint256\",\"name\":\"estimatedNativeFee\"},{\"type\":\"uint96\",\"name\":\"salePrice\"},{\"type\":\"uint64\",\"name\":\"maxTokens\"},{\"type\":\"uint16\",\"name\":\"lzChainId\"},{\"type\":\"string\",\"name\":\"uri\"}]",
//     "initializeResultDataABI": null,
//     "name": "ZoraLzMintActionV1",
//     "processCalldataABI": "[{\"type\":\"address\",\"name\":\"currency\"},{\"type\":\"uint256\",\"name\":\"quantity\"},{\"type\":\"uint256\",\"name\":\"qtAmountIn\"},{\"type\":\"uint24\",\"name\":\"uniFee\"}]",
//     "title": "Cross-chain Zora Mint"
//   },
//   "moduleType": "OPEN_ACTION",
//   "signlessApproved": false,
//   "sponsoredApproved": true,
//   "verified": true
// }

2. Fetch the onchain data necessary to process

Since this open action enables NFT sales on another chain, we have to fetch all the relevant information. Also, since the sale price is defined in ETH, and the user can pay in any Lens whitelisted currency - we must fetch a quote for the swap of payment currency to the sale price in ETH.

This is the data we must fetch

  • the remote chain data for the NFT

  • the sale price

  • total quoted price (includes mint, relay, swap)

To make this easier, we export a useful hook from the widgets SDK called useSupportedActionModule that returns a self-contained client with all the state and contract data needed to encode data

import { PostFragment, production } from "@lens-protocol/client"
import { useSupportedActionModule } from "@madfi/widgets-react";
import { useAccount, useWalletClient, useContractWrite, usePrepareContractWrite } from "wagmi";

const POLYGON_CURRENCY_WETH = "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619";

// within your nextjs page / react component

// actionModuleHandler is initalized with the data / public clients we need
const {
  isActionModuleSupported,
  actionModuleHandler,
  isLoading: isLoadingActionModuleState,
} = useSupportedActionModule(
  production,
  publication as PostFragment
);

// useful wagmi hooks
const { address } = useAccount();
const { data: walletClient } = useWalletClient();
const { config } = usePrepareContractWrite({
  address: POLYGON_CURRENCY_WETH,
  abi: IERC20Abi,
  functionName: "approve",
});
const { data, isLoading, isSuccess, write } = useContractWrite(config);

// fetch the quote, which includes the sale price and fees in whatever currency
let quotePriceToRender;
if (isActionModuleSupported && !isLoadingActionModuleState) {
  const { quotedAmountIn } = await actionModuleHandler.getQuotesForCreatorPublication(
    address, // connected wallet to simulate from
    POLYGON_CURRENCY_WETH, // whitelisted currency
    1 // quantity of NFTs to mint
  );
  quotePriceToRender = quotedAmountIn;
}

// user approves the fee for `ZoraLzMintActionV1` to transfer from
write({ args: [actionModuleHandler.address, quotePriceToRender] });

ℹī¸ If you don't want to rely on the madfi/widgets-react sdk, then you can see how we fetch the necessary data by checking the fetchActionModuleData and getQuotesForCreatorPublication function found here.

3. Encode the process action module data

Now that you have the module metadata, quote and params - you can encode the necessary calldata

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

const ZORA_LZ_MINT_ACTION = "0x5f377e3e9BE56Ff72588323Df6a4ecd5cEedc56A";

const params = {
  currency: POLYGON_CURRENCY_WETH as `0x${string}`, // get from user
  quantity: '1', // get from user
  quotedAmountIn, // from step 2
  uniFee: '500' // configurable, see uniswapv3 docs
};

const calldata = encodeData(
  JSON.parse(metadata.processCalldataABI), // got `metadata` from step 1
  [params.currency, params.quantity, params.quotedAmountIn, params.uniFee]
);

4. Send the act transaction using the Lens API

There's a lot of steps involved here, so we'll defer to the Lens docs - but here is the general code.

⚠ī¸ This function assumes your app domain is whitelisted to use gasless

⚠ī¸ This function assumes that lensClient is authenticated with a profile

import { WalletClient } from "viem";
import { OnchainReferrer, RelaySuccessFragment, LensClient } from "@lens-protocol/client";
import { omit } from "lodash/object";

// NOTE: this assume the given `actionModule` has `metadata.sponsoredApproved` = true
// NOTE: this assumes that the passed in `lensClient` is authenticated (see: https://docs.lens.xyz/docs/login)
// NOTE: this assumes the app is whitelisted to use gasless
export const actWithSignedTypedata = async (
  lensClient: LensClient,
  walletClient: WalletClient,
  publicationId: string,
  actionModule: `0x${string}`,
  actionModuleData: string,
  referrers?: OnchainReferrer[] // clients wishing to earn referral fees should mirror the `publicationId` and pass in that data
): Promise<any> => {
  try {
    // get typed data
    const typedDataResult = await lensClient.publication.actions.createActOnTypedData({
      actOn: {
        unknownOpenAction: {
          address: actionModule,
          data: actionModuleData
        }
      },
      for: publicationId,
      referrers: referrers || []
    });

    const { id, typedData } = typedDataResult.unwrap();

    // sign it
    const [account] = await walletClient.getAddresses();
    const signedTypedData = await walletClient.signTypedData({
      account,
      domain: omit(typedData.domain, "__typename"),
      types: omit(typedData.types, "__typename"),
      primaryType: "Act",
      message: omit(typedData.value, "__typename"),
    });

    // broadcast onchain, gasless
    const broadcastResult = await lensClient.transaction.broadcastOnchain({ id, signature: signedTypedData });
    const broadcastResultValue = broadcastResult.unwrap();

    if (broadcastResultValue.__typename === "RelayError") throw new Error("RelayError");

    // return the tx hash to link to layerzero scan
    return (broadcastResultValue as RelaySuccessFragment).txId;
  } catch (error) {
    console.log(error);
  }
}

🚀 There you go! You have all the steps necessary in order to process the open action yourself.

💰 A note on client referral rewards. The function above shows how to pass in referrers data - which when processed by our open action - will include the referrers[0].profileId profile owner as the recipient for Zora Mint Referral rewards. The catch is that the given profile must have mirrored the original post (the one initialized with the open action) and the resulting publicationId provided in the object for referrers. We know this is not an ideal flow and will improve this in v2! For lens clients with more traffic, we will share details around our plans in due time.

Conclusion

Enabling crosschain Zora mints through a social post is only possible with Lens v2, and we think it's a great distribution channel for creators to monetize their work - and for brands that want to run crypto-native social campaigns. We built this as the first step towards our onchain ads product (coming out in q1 2024).

If you have any questions, doubts, or suggestions, please email us at contact@madfinance.xyz.

Last updated