Part one of this article highlighted the features and components of Farcaster, explained the difference between Farcaster and Warpcast, and discussed the importance of Farcaster Frames in the world of decentralized applications.

A guide was provided to create a Farcaster account, connect the Warpcast mobile application with the desktop browser, upload NFT images and metadata on Pinata, and build and deploy the NFT smart contract that will be shared.

In this second part of the article, the focus shifts to building the Farcaster Frame using the Frog framework. Without further ado, let's get into the main topic.

Building the Farcaster Frame

A Farcaster Frame can be built in different ways, with libraries and frameworks. In this tutorial, we will build a Farcaster Frame using frog.

Frog is a lightweight framework for building Farcaster Frames. With frog, you can build frames using JSX which can be integrated onto any platform like Next.js and Vercel.

1a

Scaffold a Frog Project with Next.js

Open your terminal on a folder where you want to create your project. Run the following command in your terminal:

npm init frog -- -t next
1b

Enter the name of your project (e.g nft-frame) and hit enter. This will scaffold a project inside the folder where the terminal was opened.

1c

Change directory (cd) into your project and run `npm install`. This will install all the necessary dependencies needed for your project. Open your project in a code editor (VSCode).

2a

Add Script to Package.json and Run Server

After opening your project on VSCode, click on the package.json file. Inside the scripts object, immediately under the dev property, add the following line of code:

“frog-dev”: “frog dev”,
2b

Open your VSCode terminal and run `npm run dev` to start your dev server. You can access the application page at http://localhost:3000

Open another VSCode terminal and run `npm run frog-dev` to start a frame dev tool, where you can see the frame in action as you build them. This can be accessed via http://localhost:5173/

3a

Open Application in Browser

Now that we have both servers (Next.js server and Frog server for Frames) running. Let’s view the application in our browser.

3b

In another browser tab, open http://localhost:5173/ you should a page with an input field.

Copy http://localhost:3000/api and paste it inside the input field, click on the small right arrow.

Note: The frame can only be accessed in the /api route.

3c

This will open a dev environment for the Farcaster frame. The highlighted area in the image below is the Frame of the default code that came with the frog scaffold. All changes made on the code will be previewed on this page.

4a

Working on the Frame Codebase

In this section we will be writing the code for our frame that will interact with the NFT smart contract we had deployed earlier on the Base Sepolia network. To interact with the smart contract, we need the contract address of the deployed smart contract and the ABI.

  • Create a file named abi.js in the app/api/ directory

4b

Inside the abi.js file, create and export a constant named abi and set the value to the ABI of your smart contract.

4c

If you use the same smart contract as the one we used in this tutorial, your ABI should be the same as mine. Copy the code below and paste it inside your abi.js file instead:

export const abi = [
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "approve",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "burn",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "initialOwner",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "sender",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "ERC721IncorrectOwner",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "operator",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "ERC721InsufficientApproval",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "approver",
        "type": "address"
      }
    ],
    "name": "ERC721InvalidApprover",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "operator",
        "type": "address"
      }
    ],
    "name": "ERC721InvalidOperator",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "ERC721InvalidOwner",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "receiver",
        "type": "address"
      }
    ],
    "name": "ERC721InvalidReceiver",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "sender",
        "type": "address"
      }
    ],
    "name": "ERC721InvalidSender",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "ERC721NonexistentToken",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "EnforcedPause",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ExpectedPause",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "OwnableInvalidOwner",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "OwnableUnauthorizedAccount",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "approved",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "Approval",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "operator",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "approved",
        "type": "bool"
      }
    ],
    "name": "ApprovalForAll",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "_fromTokenId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "_toTokenId",
        "type": "uint256"
      }
    ],
    "name": "BatchMetadataUpdate",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "_tokenId",
        "type": "uint256"
      }
    ],
    "name": "MetadataUpdate",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferred",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "pause",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "Paused",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "string",
        "name": "uri",
        "type": "string"
      }
    ],
    "name": "safeMint",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "safeTransferFrom",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "internalType": "bytes",
        "name": "data",
        "type": "bytes"
      }
    ],
    "name": "safeTransferFrom",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "operator",
        "type": "address"
      },
      {
        "internalType": "bool",
        "name": "approved",
        "type": "bool"
      }
    ],
    "name": "setApprovalForAll",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "Transfer",
    "type": "event"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "transferFrom",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "unpause",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "Unpaused",
    "type": "event"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "balanceOf",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "getApproved",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "operator",
        "type": "address"
      }
    ],
    "name": "isApprovedForAll",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "name",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "ownerOf",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "paused",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes4",
        "name": "interfaceId",
        "type": "bytes4"
      }
    ],
    "name": "supportsInterface",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "symbol",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "tokenURI",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  }
]
4d

Go to the app/api/route.tsx replace the code inside the route.tsx file with the code below:

/** @jsxImportSource frog/jsx */

import { Button, Frog, TextInput } from 'frog'
import { devtools } from 'frog/dev'
// import { neynar } from 'frog/hubs'
import { handle } from 'frog/next'
import { serveStatic } from 'frog/serve-static'
import { colors } from 'frog/ui'
import { abi } from './abi'

const app = new Frog({
  assetsPath: '/',
  basePath: '/api',
  // Supply a Hub to enable frame verification.
  // hub: neynar({ apiKey: 'NEYNAR_FROG_FM' })
  title: 'Frog Frame',
})

// Uncomment to use Edge Runtime
// export const runtime = 'edge'

app.frame('/', (c) => {
  return c.res({
    image: '/makeitwork.jpeg',
    intents: [
      <Button.Transaction target='/mint'>Mint NFT</Button.Transaction>
    ],
  })
})


app.transaction("/mint", (c) => {
  const { address } = c;

  return c.contract({
    abi,
    chainId: "eip155:84532",
    functionName: "safeMint",
    to: "0x40392ea6cd095c5505B1EEAcd6Bf8066E19E77F0", // NFT contract address
    args: [address, "ipfs://QmcW255SdUxVXEBB5QcYPYVMfk3jU9YiUHV4BVai2ULA9L"],
  })
})

devtools(app, { serveStatic })

export const GET = handle(app)
export const POST = handle(app)

// NOTE: That if you are using the devtools and enable Edge Runtime, you will need to copy the devtools
// static assets to the public folder. You can do this by adding a script to your package.json:
// ```json
// {
//   scripts: {
//     "copy-static": "cp -r ./node_modules/frog/_lib/ui/.frog ./public/.frog"
//   }
// }
// ```
// Next, you'll want to set up the devtools to use the correct assets path:
// ```ts
// devtools(app, { assetsPath: '/.frog' })
// ```

Explaining the Frame Codebase

Let’s explain the Frame codebase to fully understand what is happening behind the scene. It is also important to note at this point that the route.tsx file contains the code that renders the frames, hence more focus will be on the route.tsx file.

Imports

The image above shows imported components needed for frame development.

Creating a Frog app instance

Line 11 - 17 is the default Frog application setup. This is where an instance of the Frog class is being initialized with properties assetsPath, basePath and title. The title is the name of the application, you can always change it to a preferred application name.

Creating a Frame

Developing frames with Frog makes use of a concept that contains two items which are the image and the intent. Understanding this concept will make building frames with Frog easier.

The image represents the big rectangular image that is displayed in Farcaster frames, while the intent refers to all the different ways a user can interact with the frame, it could be buttons, external links and/or input fields. The image below shows the frame image and the intent as represented in the code.

The app.frame method has two arguments. The first argument `’/’` specifies the path where this frame will be accessible. The second argument is a callback function `(c) => {...}` that takes a context object (c) as a parameter. The context object contains information about the request, and provides methods to generate responses.

Line 24 is the image that is shown in the frame, and line 26 is a transaction button inside the intent array, which will be used to interact with the NFT smart contract. Notice that the target property of the button is pointing to `/mint`. That is the transaction that will interact with the smart contract to mint an NFT.

Interacting with deployed Smart Contract

Line 32 defines a transaction handler for the `/mint` endpoint that was passed as target property in the transaction button of the frame. The first argument of app.transaction method specifies the endpoint path, the second argument is a callback function that takes a context object as its parameter. This context object contains information about the request and provides methods to interact with blockchain smart contracts. For instance, you can read the wallet address of the user who initiated the transaction just like we did on line 33.

Line 35 - 41, we are returning a contract transaction using `c.contract({` to construct a transaction object with specific blockchain details. The `abi` property is the smart contract ABI we intend to interact with, which has been imported earlier on line 9. The `chainId` property specifies the chain ID of the blockchain network where the smart contract is deployed. The `functionName` property is the name of the function to be called in the smart contract, which is `safeMint`, the to property is the contract address of the deployed smart contract to be interacted with. The `args` property specifies arguments to be passed to the safeMint function. The arguments are:

  • `address`: The wallet address of the user who initiated the smart contract interaction call, indicating the recipient of the minted NFT.

  • `"ipfs://QmcW255SdUxVXEBB5QcYPYVMfk3jU9YiUHV4BVai2ULA9L"`: A URI pointing to the metadata of the NFT stored on Pinata (IPFS)


Devtools Initialization

Line 44 initializes developer tools for the `app` instance. The `devtools` function is used to provide debugging and development utilities that help developers to monitor and manage their application. The `app` parameter represents the application instance, while the `{serveStatic}` is used to serve static files like HTML, CSS, JavaScript and images, during development.

Line 46 is exporting a `GET` handler while line 47 is exporting a `POST` handler.

Testing the Smart Contract Frame

We have completed building the frame, let’s test it out.

  • Refresh your Frame dev environment, you should see the following frame and Mint NFT button.

  • Click on the Mint NFT button. This will show a list of wallets, click on Metamask.

  • Metamask will pop-up, connect your wallet.

  • After connecting your Metamask wallet, you will see a Send Transaction button, click on it to mint the NFT to your wallet address.

  • After clicking the Send Transaction button, confirm the transaction on your Metamask wallet when it pops up and wait, you will see a Transaction Confirmed message.

  • Navigate to OpenSea testnet here.

  • Paste your wallet address in the OpenSea search bar and click on the result.

You should see the minted NFT in your OpenSea profile, just like in the image below:

Validating the Frame on Warpcast Validator

We have completed building the Farcaster frame, we need to validate it on the Farcaster Validator. This requires deploying your frame on Vercel.

  • To deploy your frame on Vercel, you will need to push it first to GitHub, then add the GitHub repository to Vercel and deploy the frame.

  • After deploying your frame on Vercel, click on Warpcast Frame Validator on your Frame dev environment as shown below:

  • It will open a Warpcast Frame Validator page, in the validator, copy the application URL of your frame that is hosted on Vercel and paste it in the URL input field, add `/api` at the end of the URL in the input field and click the button beside the input, to load the frame. Once the frame loads, it means everything is working well.

Sharing Your Frame on Warpcast

To share your frame on Warpcast

  • Go to Warpcast on your desktop browser here

  • Click on cast, to make a post.

  • Paste YOUR-URL.vercel.app/api in the textarea, it will load your frame.

  • You can add some text and then cast/post it by clicking the Cast button.

You have successfully shared your smart contract on a Farcaster Frame, and users can interact with your smart contract directly from Warpcast to mint NFTs.

Your shared smart contract on Warpcast should look like the following:

Conclusion

In this article, we have explored the process of sharing a smart contract in a Farcaster Frame, highlighting the necessary processes and components required to achieve this. We also differentiated between Farcaster and Warpcast, explaining that Farcaster is the social network protocol that’s built on blockchain while Warpcast is an application that is built on the Farcaster protocol, upon which Farcaster Frames can be shared. Farcaster Frames on the other hand enables users to seamlessly embed and interact with smart contracts, making decentralized applications more accessible and engaging.

By following the tutorial, you can comfortably share smart contracts within a Farcaster Frame, allowing others to interact with them directly. You can experiment and explore more of the Farcaster Frame features using the Frog framework.

References & Further Readings