How to Share a Smart Contract in a Farcaster Frame (Part 2)
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
1b
1c
2a
Add Script to Package.json and Run Server
2b
3a
Open Application in Browser
- Navigate to http://localhost:3000 in your browser. It should open a Next.js default application page.
3b
Note: The frame can only be accessed in the /api route.
3c
4a
Working on the Frame Codebase
- Create a file named abi.js in the app/api/ directory
4b
4c
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
/** @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.