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

  • 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

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

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] });

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.

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);
  }
}

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