Quickstart Guide to Develop on ZKSync : Build NFT ERC-1155 Ticket Minter with Paymaster feature and DApp for Ticket Checker (Chapter 4)

Ridho IzzulhaqRidho Izzulhaq
6 min read

In the previous chapter, we discussed about to how build basic paymaster on ZKSync detailed on sending messages and executing transactions on the voting smart contract, where the gas fees are covered by the ZKSync Paymaster.

In this chapter, we will further discuss paymasters by creating program that enables NFT minting processes where the gas fees are covered by the paymaster. And also DApp for checking availability of specified nft in user account

Build Our ERC-1155 Project on ATLAS IDE

ERC-1155 is advantageous for ticketing due to its efficiency in batch transfers, supporting both fungible and non-fungible tokens within a single contract. This capability reduces gas costs and enhances operational efficiency, making it ideal for managing various ticket types and bundles in ticketing platforms. The standard's flexibility also allows for scalable token issuance and transaction management, accommodating different ticketing needs while optimizing blockchain resources. Additionally, ERC-1155's support for advanced features like royalties ensures creators and issuers can benefit from secondary market transactions, further enhancing its appeal for modern ticketing applications.

Therefore, we will apply ERC-1155 for the ticketing application we are creating. Use ATLAS IDE to run this basic ERC1155 smart contract.

In the next step, we will use ERC-20 for custom payments. To make it easier, you can use the template workspace and follow the tutorial from the previous chapter.

Our NFT.sol :

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract NFT is ERC1155, Ownable {
    uint256 public constant TOKEN_ID = 1;

    constructor(address initialOwner)
        ERC1155(
            "https://lavender-adorable-hummingbird-774.mypinata.cloud/ipfs/QmenP8UoVuTb7bsLpUppFABPFdusMQqb3kVFFT5svTRcFu"
        )
        Ownable(initialOwner)
    {}

    function mint(address to, uint256 amount, bytes memory data) external {
        _mint(to, TOKEN_ID, amount, data);
    }
}

For deployment scripts, use :

import * as zk from "zksync-ethers";
import { default as hre, ethers } from "hardhat";
import { getDeployer } from "@atlas-labs-inc/hardhat-atlas-zksync-deploy";

// Address of the ERC20 token contract
const TOKEN_CONTRACT_ADDRESS = "";
// Address of the NFT contract
const NFT_CONTRACT_ADDRESS = "";
// Amount of NFTs to mint
const MINT_AMOUNT = 1;

async function main() {
  // Connect to the wallet connected in the network tab
  const { provider, signer } = await getDeployer(hre);

  const zkProvider = new zk.Provider("https://sepolia.era.zksync.dev");
  const signerAddress = await signer.getAddress();

  console.log("Minting NFTs via the testnet paymaster");

  const tokenContract = new zk.Contract(
    TOKEN_CONTRACT_ADDRESS,
    (await ethers.getContractFactory("TestToken")).interface,
    signer
  );

  const nftContract = new zk.Contract(
    NFT_CONTRACT_ADDRESS,
    (await ethers.getContractFactory("NFT")).interface,
    signer
  );

  // Retrieve and print the current balance of the wallet
  let ethBalance = await provider.getBalance(signerAddress);
  let tokenBalance = await tokenContract.balanceOf(signerAddress);
  console.log(`Account ${signerAddress} has ${ethers.formatEther(ethBalance)} ETH`);
  console.log(`Account ${signerAddress} has ${ethers.formatUnits(tokenBalance, 18)} tokens`);

  // Retrieve the testnet paymaster address
  const testnetPaymasterAddress = await zkProvider.getTestnetPaymasterAddress();
  console.log(`Testnet paymaster address is ${testnetPaymasterAddress}`);

  const gasPrice = await zkProvider.getGasPrice();

  // Define paymaster parameters for gas estimation
  const paramsForFeeEstimation = zk.utils.getPaymasterParams(testnetPaymasterAddress, {
    type: "ApprovalBased",
    token: TOKEN_CONTRACT_ADDRESS,
    // Set minimalAllowance to 1 for estimation
    minimalAllowance: ethers.toBigInt(1),
    // Empty bytes as testnet paymaster does not use innerInput
    innerInput: new Uint8Array(0),
  });

  // Mint NFT
  console.log(`Minting ${MINT_AMOUNT} NFT(s) to ${signerAddress}`);
  const mintGasLimit = await nftContract.mint.estimateGas(signerAddress, MINT_AMOUNT, "0x", {
    customData: {
      gasPerPubdata: zk.utils.DEFAULT_GAS_PER_PUBDATA_LIMIT,
      paymasterParams: paramsForFeeEstimation,
    },
  });

  // Fee calculated in ETH will be the same in ERC20 token using the testnet paymaster
  const mintFee = gasPrice * mintGasLimit;

  // New paymaster params with fee as minimalAllowance for minting
  const mintPaymasterParams = zk.utils.getPaymasterParams(testnetPaymasterAddress, {
    type: "ApprovalBased",
    token: TOKEN_CONTRACT_ADDRESS,
    // Provide estimated fee as allowance
    minimalAllowance: mintFee,
    // Empty bytes as testnet paymaster does not use innerInput
    innerInput: new Uint8Array(0),
  });

  // Full overrides object including maxFeePerGas and maxPriorityFeePerGas for minting
  const mintTxOverrides = {
    maxFeePerGas: gasPrice,
    maxPriorityFeePerGas: ethers.toBigInt(1),
    gasLimit: mintGasLimit,
    customData: {
      gasPerPubdata: zk.utils.DEFAULT_GAS_PER_PUBDATA_LIMIT,
      paymasterParams: mintPaymasterParams,
    },
  };

  const mintTx = await nftContract.mint(signerAddress, MINT_AMOUNT, "0x", mintTxOverrides);
  await mintTx.wait();
  console.log(`Minted ${MINT_AMOUNT} NFT(s) to ${signerAddress} with fee ${ethers.formatUnits(mintFee, 18)} ERC20 tokens, sent via paymaster ${testnetPaymasterAddress}`);

  ethBalance = await provider.getBalance(signerAddress);
  tokenBalance = await tokenContract.balanceOf(signerAddress);
  console.log(`Account ${signerAddress} now has ${ethers.formatEther(ethBalance)} ETH`);
  console.log(`Account ${signerAddress} now has ${ethers.formatUnits(tokenBalance, 18)} tokens`);

  console.log(`Done!`);
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

Please configure the code :

// Address of the ERC20 token contract
const TOKEN_CONTRACT_ADDRESS = "your custom erc20 contract";
// Address of the NFT contract
const NFT_CONTRACT_ADDRESS = "your erc1155 contract";
// Amount of NFTs to mint
const MINT_AMOUNT = 1; //how much NFT that you want to deploy

Result on blockchain explorer :

we also able to check the NFT transaction on https://sepolia-era.zksync.network/tx/(your transaction hash)

Build the DApp

DApp usage simulation :

We use Alchemy to get data from NFTs, specifically this doc.

npx create-react-app ticket-checker
cd ticket-checker 
npm install web3

create file src/App.js

import React, { useState } from 'react';
import NFTDisplay from './components/NFTDisplay';
import './App.css';

function App() {
  const [userAddress, setUserAddress] = useState('');
  const [nfts, setNfts] = useState([]);
  const [error, setError] = useState('');

  const connectMetaMask = async () => {
    if (window.ethereum) {
      try {
        const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
        setUserAddress(accounts[0]);
        await fetchNFTs(accounts[0]); // Fetch NFTs after connecting
      } catch (error) {
        setError('User rejected request');
        console.error('User rejected request:', error);
      }
    } else {
      setError('MetaMask is not installed!');
      console.error('MetaMask is not installed!');
    }
  };

  const fetchNFTs = async (address) => {
    if (!address) return;

    const options = {
      method: 'GET',
      headers: { accept: 'application/json' },
    };

    const url = `https://zksync-sepolia.g.alchemy.com/nft/v3/(put your api key)/getNFTsForOwner?owner=${address}&contractAddresses[]=(put your nft contract)&withMetadata=true&pageSize=100`;

    try {
      const response = await fetch(url, options);
      const data = await response.json();
      console.log('Fetched NFTs:', data); // Log the response
      setNfts(data.ownedNfts || []);
    } catch (error) {
      setError('Error fetching NFTs');
      console.error('Error fetching NFTs:', error);
    }
  };

  return (
    <div className="App">
      <button onClick={connectMetaMask}>Connect MetaMask</button>
      {error && <p>{error}</p>}
      <NFTDisplay nfts={nfts} />
    </div>
  );
}

export default App;

please config code with : const url = `https://zksync-sepolia.g.alchemy.com/nft/v3/(put your api key)/getNFTsForOwner?owner=${address}&contractAddresses[]=(put your nft contract)&withMetadata=true&pageSize=100`;
create file src/components/NFTDisplay.js

import React from 'react';

const NFTDisplay = ({ nfts }) => {
  // Check if nfts is defined and an array
  if (!nfts || !Array.isArray(nfts) || nfts.length === 0) {
    return <p>There is no valid ticket.</p>;
  }

  // Check if there are any valid tickets
  const hasValidTicket = nfts.some(nft => nft.name && nft.description && nft.image && nft.image.cachedUrl);

  // Display message if no valid tickets are found
  if (!hasValidTicket) {
    return <p>There is no valid ticket.</p>;
  }

  // Map through the NFTs and display their details
  return (
    <div>
      <h3>Valid Ticket Details:</h3>
      {nfts.map((nft, index) => (
        <div key={index}>
          <h4>{nft.name || 'No Name'}</h4>
          <p>{nft.description || 'No Description'}</p>
          {nft.image && nft.image.cachedUrl ? (
            <img src={nft.image.cachedUrl} alt={nft.name || 'NFT Image'} style={{ maxWidth: '300px' }} />
          ) : (
            <p>No Image Available</p>
          )}
        </div>
      ))}
    </div>
  );
};

export default NFTDisplay;

Run the project with npm start

Closing

Thank you for your support and interest in this series of articles, which is part of a grant program available at Wave Hacks on Akindo.

Structure :

WAVETITLELINK
1stQuickstart Guide to Develop on zkSync : Fundamental Concept and Development with ZKSync Remix IDE 'Instant Way' (Chapter 1)https://ridhoizzulhaq.hashnode.dev/quickstart-guide-to-develop-on-zksync-fundamental-concept-and-development-with-zksync-remix-ide-instant-way-chapter-1
3rdQuickstart Guide to Develop on zkSync : Build a Complete Voting DApp on the ZKSync (Chapter 2)https://ridhoizzulhaq.hashnode.dev/quickstart-guide-to-develop-on-zksync-build-a-complete-voting-dapp-on-the-zksync-chapter-2
4thQuickstart Guide to Develop on ZKSync : Explore Basic Paymaster (Chapter 3)https://ridhoizzulhaq.hashnode.dev/quickstart-guide-to-develop-on-zksync-explore-basic-paymaster-chapter-3
5thQuickstart Guide to Develop on ZKSync : Build NFT ERC-1155 Ticket Minter with Paymaster feature and DApp for Ticket Checker (Chapter 4)https://ridhoizzulhaq.hashnode.dev/quickstart-guide-to-develop-on-zksync-build-nft-erc-1155-ticket-minter-with-paymaster-feature-and-dapp-for-ticket-checker-chapter-4
0
Subscribe to my newsletter

Read articles from Ridho Izzulhaq directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ridho Izzulhaq
Ridho Izzulhaq