Quickstart Guide to Develop on ZKSync : Build NFT ERC-1155 Ticket Minter with Paymaster feature and DApp for Ticket Checker (Chapter 4)
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 :
Subscribe to my newsletter
Read articles from Ridho Izzulhaq directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by