Frontend integration
Step 3 — Frontend Development
/frontend
directory.Let’s first break down the project structure within /frontend
:
.
├── README.md
├── app
│ ├── assets
│ ├── components
│ ├── constants
│ ├── context
│ ├── favicon.ico
│ ├── globals.css
│ ├── hooks
│ ├── layout.tsx
│ ├── page.tsx
│ └── types
├── next-env.d.ts
├── next.config.js
├── package.json
├── postcss.config.js
├── public
├── tailwind.config.js
└── tsconfig.json
- Let's spin up our frontend to see what we need to implement:
yarn dev
This will start a local server running onhttp://localhost:3000/
. You should see something that looks similar to:
Connecting our WalletButton
Component
The first component we are going to interact with is the WalletButton
component.
- Navigate to
app/components/WalletButton.tsx
cd app/components/WalletButton.tsx
- Implement the requirements for the
connectWallet
function
TheconnectWallet
function requirements indicates that we need to ensure we are connected to the correct network, connect to the users MetaMask account, store some variables using React's Context API, and then callinitContracts
function to instantiate our contracts. Let's proceed to do that:app/components/WalletButton.tsxconst connectWallet = async () => { if (!networkOk) await switchNetwork(); try { if ((window as any).ethereum) { const provider = new Web3Provider((window as any).ethereum); web3Context.setProvider(provider); const data = await provider.send("eth_requestAccounts", []); const signerInstance = provider.getSigner(); web3Context.setSigner(signerInstance); setWallet({ address: data[0], acc_short: shortenAddress(data[0]) }); await initContracts(provider, signerInstance); } } catch (error) { console.error("Error connecting DApp to your wallet"); console.error(error); } };
- Great! Let's continue within our
WalletButton
component and implement theinitContracts
function.
TheinitContracts
function should instantiate our Greeter and NFT token contracts, check if the connected wallet contains our specific NFT, and if so fetch the NFT metadata from IPFS and store some variables using React's Context API. Copy and paste the below code snippet.app/components/WalletButton.tsxconst initContracts = async (provider: Web3Provider, signer: Signer) => { if (provider && signer) { const greeterContract = new Contract(GREETER_ADDRESS, GREETER_CONTRACT_ABI, signer); web3Context.setGreeterContractInstance(greeterContract); const fetchedGreeting = await greeterContract.greet(); web3Context.setGreetingMessage(fetchedGreeting); const nftContract = new Contract(NFT_CONTRACT_ADDRESS, NFT_CONTRACT_ABI, signer); const address = await signer.getAddress(); const balance = await nftContract.balanceOf(address); if (balance > 0) { let ownedStones: PowerStoneNft[] = []; const ownedTokensResponse = await nftContract.tokensOfOwner(address); for (let i = 0; i < ownedTokensResponse.length; i++) { const tokenId = ownedTokensResponse[i]; const tokenURI = await nftContract.tokenURI(tokenId); if (tokenURI == undefined || tokenURI == "") { continue; } const response = await fetch(tokenURI); if (!response.ok) { continue; } ownedStones.push((await response.json()) as PowerStoneNft); } web3Context.setNfts(ownedStones); } else { web3Context.setNfts([]); } } };
We can now return to our running local page and click the 'Connect Wallet' button which should connect to your MetaMask account as depicted in the below image.
We have connected our wallet to our application but we now need to add our GreeterMessage
and Input
components.
Importing the GreeterMessage
and Input
Components
- Navigate to
app/page.tsx
Scrolling to the button you can see the requirements outlined. We need to import our components and pass the specified props. Let's first import our components at the top of the file:app/page.tsximport Greeting from "./components/GreeterMessage"; import Input from "./components/Input";
- Add the components to the return statement and pass desired propsapp/page.tsx
<Greeting greeting={web3Context.greeting} /> <Input greeterInstance={web3Context.greeterContractInstance} setGreetingMessage={web3Context.setGreetingMessage} provider={web3Context.provider} nfts={web3Context.nfts} />
Now if we check our local page we can see our rendered Greeter message and Input box!
Fetching the Gas details and adding the Modal
Component
- Navigate to
app/components/Input.tsx
component
We need to write ourestimateGas
function as we will want to pass those details to ourModal
component to display. - Implement
estimateGas
function
We want to display the current gas price, estimate the amount of gas required to execute oursetGreeting
transaction, and store these variables to be used later.app/components/Input.tsxasync function getEstimate() { if (!provider) return; let gasPrice = await provider.getGasPrice(); let price = ethers.utils.formatEther(gasPrice.toString()); setPrice(price); if (!greeterInstance) return; let gasEstimate = await greeterInstance.estimateGas["setGreeting"](message); let gas = ethers.utils.formatEther(gasEstimate.toString()); setGas(gas); let transactionCost = gasPrice.mul(gasEstimate); let cost = ethers.utils.formatEther(transactionCost.toString()); setCost(cost); }
- Add
Modal
component to return statementapp/components/Input.tsx{ isOpen && ( <Modal closeModal={closeModal} greeterInstance={greeterInstance} message={message} setGreetingMessage={setGreetingMessage} cost={cost} price={price} gas={gas} nfts={nfts} /> ); }
This will open the Modal
component once the "Change message" button is clicked.
Setup our Paymaster Hook
We are ready to implement our paymaster hook which will be used if the connected wallet possesses one of the applicable NFT's we minted earlier.
- Navigate to
app/hooks/usePaymaster.tsx
The requirements outline we need to prepare and return thepaymasterParams
to then be passed alongside thesetGreeting
transaction.app/hooks/usePaymaster.tsxconst usePaymaster = async ({ greeterInstance, message, price }: PaymasterProps) => { let gasPrice = ethers.utils.parseEther(price); const paymasterParams = utils.getPaymasterParams(PAYMASTER_CONTRACT_ADDRESS, { type: "General", innerInput: new Uint8Array(), }); const gasLimit = await greeterInstance.estimateGas.setGreeting(message, { customData: { gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT, paymasterParams: paymasterParams, }, }); return { maxFeePerGas: gasPrice, maxPriorityFeePerGas: ethers.BigNumber.from(0), gasLimit: gasLimit, customData: { gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT, paymasterParams: paymasterParams, }, }; };
We have prepared our paymasterParams
to be used in our application!
Let's navigate to our last component that needs to be implemented, the Checkout
component.
Implement updateGreeting
function in the Checkout
component
This function is responsible for initiating the transaction that will interact with our Greeter.sol contract to update the message.
It will check if the connected wallet contains our specific NFT and if so,
call our usePaymaster
hook created above to pass along the paymasterParams
to facilitate a gasless transaction for the user.
Without the NFT, the user will be required to pay the gas fee.
- Import
usePaymaster
hook in theCheckout
componentimport usePaymaster from "../hooks/usePaymaster";
- Implement the
updateGreeting
functionconst updateGreeting = async ({ message }: GreeterData) => { try { if (greeterInstance == null) { return; } let txHandle; if (hasNFT) { const params = await usePaymaster({ greeterInstance, message, price }); txHandle = await greeterInstance.setGreeting(message, params); } else { txHandle = await greeterInstance.setGreeting(message); } await txHandle.wait(); const updatedGreeting = await greeterInstance.greet(); setGreetingMessage(updatedGreeting); } catch (error) { console.error("Failed to update greeting: ", error); } };
Amazing we have successfully implemented all our frontend requirements! Now its time to test the application.
Step 4 — Test the Application
Navigate to http://localhost:3000
and refresh the page.
Click on "Connect Wallet" to link your MetaMask account.
Ensure you connect the address that received the minted NFT during contract deployment; otherwise, you'll bear the gas fees!
- Input your own message into the
Input
component - Click the "Change message" button
- If you have the right NFT you should be presented with the below image:
- Enjoy a gasless transaction!
Conclusion
As we conclude, you've not only set up and run a seamless integration between a frontend and ZKsync Sepolia Testnet but have also gained hands-on expertise. You've connected your MetaMask, engaged with smart contracts, and seen the magic of paymasters unfold right on your local server.
If you want to continue to learn about paymaster or see additional examples checkout the paymaster-examples repo for further inspiration.