Abstract Gasless Nft Mint

Sign in with account abstraction and mint NFTs without gas fees

AbstractNFTsMintSmart contract
Loading

Technologies

Installation

Deploy Paymaster Smart Contract

Deploy the paymaster smart contract to handle transaction gas sponsoring.

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

import {IPaymaster, ExecutionResult, PAYMASTER_VALIDATION_SUCCESS_MAGIC} from "@matterlabs/zksync-contracts/l2/system-contracts/interfaces/IPaymaster.sol";
import {IPaymasterFlow} from "@matterlabs/zksync-contracts/l2/system-contracts/interfaces/IPaymasterFlow.sol";
import {Transaction} from "@matterlabs/zksync-contracts/l2/system-contracts/libraries/TransactionHelper.sol";
import {BOOTLOADER_FORMAL_ADDRESS} from "@matterlabs/zksync-contracts/l2/system-contracts/Constants.sol";

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

contract MyPaymaster is IPaymaster, Ownable {
    modifier onlyBootloader() {
        require(
            msg.sender == BOOTLOADER_FORMAL_ADDRESS,
            "Only bootloader can call this method"
        );
        // Continue execution if called from the bootloader.
        _;
    }

    /// @dev Called by the bootloader to verify that the paymaster agrees to pay for the
    /// fee for the transaction. This transaction should also send the necessary amount of funds onto the bootloader
    /// address.
    /// @param _txHash The hash of the transaction
    /// @param _suggestedSignedHash The hash of the transaction that is signed by an EOA
    /// @param _transaction The transaction itself.
    /// @return magic The value that should be equal to the signature of the validateAndPayForPaymasterTransaction
    /// if the paymaster agrees to pay for the transaction.
    /// @return context The "context" of the transaction: an array of bytes of length at most 1024 bytes, which will be
    /// passed to the `postTransaction` method of the account.
    /// @dev The developer should strive to preserve as many steps as possible both for valid
    /// and invalid transactions as this very method is also used during the gas fee estimation
    /// (without some of the necessary data, e.g. signature).
    function validateAndPayForPaymasterTransaction(
        bytes32 _txHash,
        bytes32 _suggestedSignedHash,
        Transaction calldata _transaction
    )
        external
        payable
        onlyBootloader
        returns (bytes4 magic, bytes memory context)
    {
        magic = PAYMASTER_VALIDATION_SUCCESS_MAGIC;

        uint256 requiredETH = _transaction.gasLimit * _transaction.maxFeePerGas;

        (bool success, ) = payable(BOOTLOADER_FORMAL_ADDRESS).call{
            value: requiredETH
        }("");
        require(success, "Failed to transfer tx fee to the Bootloader");
    }

    /// @dev Called by the bootloader after the execution of the transaction. Please note that
    /// there is no guarantee that this method will be called at all. Unlike the original EIP4337,
    /// this method won't be called if the transaction execution results in out-of-gas.
    /// @param _context, the context of the execution, returned by the "validateAndPayForPaymasterTransaction" method.
    /// @param  _transaction, the users' transaction.
    /// @param _txResult, the result of the transaction execution (success or failure).
    /// @param _maxRefundedGas, the upper bound on the amout of gas that could be refunded to the paymaster.
    /// @dev The exact amount refunded depends on the gas spent by the "postOp" itself and so the developers should
    /// take that into account.
    function postTransaction(
        bytes calldata _context,
        Transaction calldata _transaction,
        bytes32 _txHash,
        bytes32 _suggestedSignedHash,
        ExecutionResult _txResult,
        uint256 _maxRefundedGas
    ) external payable onlyBootloader {}

    /// @dev Withdraws the balance of the paymaster.
    /// @param _to The address to withdraw the balance to.
    function withdraw(address payable _to) external onlyOwner {
        uint256 balance = address(this).balance;
        (bool success, ) = _to.call{value: balance}("");
        require(success, "Failed to withdraw funds from paymaster.");
    }

    receive() external payable {}
}

Deploy Nft Smart Contract

Similar to the previous step we must now eploy the nft smart contract to handle the minting of NFTs.

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;

import {ERC721A} from "erc721a/contracts/ERC721A.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

interface IErrors {
    // @dev Insufficient funds sent with mint request transaction
    error InsufficientFundsSent();

    // @dev Quantity to mint must be greater than 0
    error InvalidMintQuantity();

    // @dev Invalid timestamps provided for mint phases (must be linear)
    error InvalidTimestamps();

    // @dev Maximum amount of mints per wallet on public phase exceeded
    error MaxMintsPerWalletExceeded();

    // @dev Max supply cap has exceeded
    error MaxSupplyExceeded();

    // @dev All mint phases have ended
    error MintHasEnded();

    // @dev Minting has not started for the requested phase
    error MintHasNotStarted();

    // @dev Withdrawal of ethereum balance failed
    error WithdrawalFailed();
}

contract NftMint is ERC721A, Ownable, IErrors {
    // ================================================================
    // │                          STORAGE                             │
    // ================================================================
    // @dev Maximum supply cap for the collection
    // @notice This variable is preset to a placeholder and its value is user editable
    uint256 private s_maxSupply = 10000;

    // @dev Mint price per NFT
    // @notice This variable is preset to a placeholder and its value is user editable
    uint256 private s_mintPrice = 0.01 ether;

    // @dev Maximum allowed mints per user address
    // @notice This variable is preset to a placeholder and its value is user editable
    uint256 private s_maxMintsPerWallet = 5;

    // @dev Mint start time using UNIX timestamp
    uint256 private s_mintStartTimestamp;

    // @dev Mint end time using UNIX timestamp
    uint256 private s_mintEndTimestamp;

    // @dev Base URI for the collection metadata
    string private s_baseURI;

    // @dev Extension added to s_baseURI to form the final tokenURI
    string private baseExtension = ".json";

    // ================================================================
    // │                          EVENTS                              │
    // ================================================================
    // @dev Event emitted when the supply cap is edited
    event SupplyCapEdited(uint256 newSupplyCap, uint256 timestamp);

    // @dev Event emitted when a new mint is performed
    event Mint(address indexed minter, uint256 quantity, uint256 timestamp);

    // @dev Event emitted when the baseURI is edited
    event BaseURISet(string newBaseURI, uint256 timestamp);

    // @dev Event emitted when the mint price is edited
    event MintPriceEdited(uint256 newPrice, uint256 timestamp);

    // @dev Event emitted when the timestamps are edited
    event TimestampsEdited(
        uint256 mintStart,
        uint256 mintEnd,
        uint256 timestamp
    );

    // @dev Event emitted when the max mints per user address is edited
    event MaxMintsPerWalletEdited(
        uint256 newMaxMintsPerWallet,
        uint256 timestamp
    );

    // @dev Event emitted when the contract owner withdraws the balance
    event Withdrawal(
        address indexed caller,
        uint256 totalAmount,
        uint256 timestamp
    );

    // ================================================================
    // │                        CONSTRUCTOR                           │
    // ================================================================
    /**
     * @dev {constructor}
     *
     * @param _initBaseURI Initial baseUri to be set.
     * @param _initMintStart Initial mint start timestamp.
     * @param _initMintEnd Initial mint end timestamp.
     *
     * @notice This NFT collection is named "Gizmolab UI" with the symbol "GLUI" these are
     * placeholders that must be changed for your desired collection name and symbol    .
     */
    constructor(
        string memory _initBaseURI,
        uint256 _initMintStart,
        uint256 _initMintEnd
    ) ERC721A("Gizmolab UI", "GLUI") {
        if (
            _initMintStart >= _initMintEnd ||
            _initMintStart == 0 ||
            _initMintEnd == 0
        ) revert InvalidTimestamps();

        s_baseURI = _initBaseURI;
        s_mintStartTimestamp = _initMintStart;
        s_mintEndTimestamp = _initMintEnd;
    }

    // ================================================================
    // │                        MINT FUNCTION                         │
    // ================================================================
    /**
     * @dev function {mint}
     *
     * Function to mints NFTs.
     *
     * Emits a {Mint} event indicating the details of a successful mint.
     *
     * NOTE: Various mint criterias must be met in order for the transaction to succeed.
     * Criterias include the mint phase being active, the total supply cap not being exceeded,
     * the maximum mints per user address not being exceeded, and the correct amount of funds
     * being sent with the transaction.
     *
     * @param quantity Number of NFTs to mint.
     */
    function mint(uint256 quantity) external payable {
        if (block.timestamp < s_mintStartTimestamp) revert MintHasNotStarted();

        if (block.timestamp > s_mintEndTimestamp) revert MintHasEnded();

        if (quantity == 0) revert InvalidMintQuantity();

        uint256 previouslyMinted = _getAux(_msgSender());

        if (previouslyMinted + quantity > s_maxMintsPerWallet)
            revert MaxMintsPerWalletExceeded();

        if (totalSupply() + quantity > s_maxSupply) revert MaxSupplyExceeded();

        if (msg.value < s_mintPrice * quantity) revert InsufficientFundsSent();

        _setAux(_msgSender(), uint64(previouslyMinted + quantity));
        _safeMint(_msgSender(), quantity);

        emit Mint(msg.sender, quantity, block.timestamp);
    }

    // ================================================================
    // │                       VIEW FUNCTIONS                         │
    // ================================================================
    /**
     * @dev function {getMintPrice}
     *
     * Returns the mint price for a single token.
     *
     * @return s_mintPrice
     */
    function getMintPrice() external view returns (uint256) {
        return s_mintPrice;
    }

    /**
     * @dev function {getMaxSupply}
     *
     * Returns the maximum supply cap for the collection.
     *
     * @return s_maxSupply
     */
    function getMaxSupply() external view returns (uint256) {
        return s_maxSupply;
    }

    /**
     * @dev function {getMaxMintsPerWallet}
     *
     * Returns the maximum mints allowed for a single user address.
     *
     * @return s_maxMintsPerWallet
     */
    function getMaxMintsPerWallet() external view returns (uint256) {
        return s_maxMintsPerWallet;
    }

    /**
     * @dev function {getStartTimestamp}
     *
     * Returns the UNIX mint start timestamp.
     *
     * @return s_mintStartTimestamp
     */
    function getStartTimestamp() external view returns (uint256) {
        return s_mintStartTimestamp;
    }

    /**
     * @dev function {getEndTimestamp}
     *
     * Returns the UNIX end start timestamp.
     *
     * @return s_mintEndTimestamp
     */
    function getEndTimestamp() external view returns (uint256) {
        return s_mintEndTimestamp;
    }

    /**
     * @dev function {getUserMintedNftCount}
     *
     * Returns the amount of NFTs the function caller has minted.
     *
     * @param userAddress The user address to query the number of minted NFTs for.
     *
     * @return _getAux(userAddress)
     */
    function getUserMintedNftCount(
        address userAddress
    ) external view returns (uint256) {
        return _getAux(userAddress);
    }

    /**
     * @dev function {getBaseURI}
     *
     * Returns the current active collection base URI.
     *
     * @return s_baseURI
     */
    function getBaseURI() external view returns (string memory) {
        return s_baseURI;
    }

    // ================================================================
    // │                      COLLECTION METADATA                     │
    // ================================================================
    /**
     * @dev function {_baseURI}
     *
     * Override to return the storage variable of {_baseURI()} representing the base URI for the collection metadata.
     *
     * @return s_baseURI
     */
    function _baseURI() internal view virtual override returns (string memory) {
        return s_baseURI;
    }

    /**
     * @dev function {_startTokenId}
     *
     * Override to return 1 as a tokenId instead of 0 for the first token minted.
     *
     * @return number with a override value of 1
     */
    function _startTokenId() internal view virtual override returns (uint256) {
        return 1;
    }

    /**
     * @dev function {tokenURI}
     *
     * Override to return the appropriate metadata uri for a given tokenId.
     *
     * @param tokenId The tokenId to query the metadata URI for.
     *
     * @return string representing the metadata URI for the given tokenId.
     */
    function tokenURI(
        uint256 tokenId
    ) public view virtual override returns (string memory) {
        require(
            _exists(tokenId),
            "ERC721Metadata: URI query for nonexistent token"
        );

        string memory currentBaseURI = _baseURI();
        return
            bytes(currentBaseURI).length > 0
                ? string(
                    abi.encodePacked(
                        currentBaseURI,
                        _toString(tokenId),
                        baseExtension
                    )
                )
                : "";
    }

    // ================================================================
    // │                      OWNER FUNCTIONS                         │
    // ================================================================
    /**
     * @dev function {setBaseURI}
     *
     * Sets new URI to the storage variable {s_baseURI}.
     *
     * Emits a {BaseURISet} event indicating a new URI has been set as the base and timestamp of the edit.
     *
     * @param baseURI New base URI to set for the collection metadata.
     *
     * NOTE: Only the contract owner can call this function.
     */
    function setBaseURI(string memory baseURI) public onlyOwner {
        s_baseURI = baseURI;

        emit BaseURISet(baseURI, block.timestamp);
    }

    /**
     * @dev function {editMintPrice}
     *
     * Sets new price for minting a single token.
     *
     * Emits a {MintPriceEdited} event indicating the new mint price and timestamp of the edit.
     *
     * @param newPrice New mint price in WEI.
     *
     * NOTE: Only the contract owner can call this function.
     */
    function editMintPrice(uint256 newPrice) external onlyOwner {
        s_mintPrice = newPrice;

        emit MintPriceEdited(newPrice, block.timestamp);
    }

    /**
     * @dev function {editMaxSupply}
     *
     * Sets new limit for maximum supply cap for the collection.
     *
     * Emits a {SupplyCapEdited} event indicating the new supply cap and timestamp of the edit.
     *
     * @param newSupply New maximum supply cap for the collection.
     *
     * NOTE: Only the contract owner can call this function.
     */
    function editMaxSupply(uint256 newSupply) external onlyOwner {
        s_maxSupply = newSupply;

        emit SupplyCapEdited(newSupply, block.timestamp);
    }

    /**
     * @dev function {editMaxMintsPerWallet}
     *
     * Sets new maximum cap of number of NFTs mintable per user address.
     *
     * Emits a {MaxMintsPerWalletEdited} event indicating the maximum mintable NFTs cap and timestamp of the edit.
     *
     * @param newMaxMintsPerWallet New maximum cap of mintable NFTs per user address.
     *
     * NOTE:
     * Only the contract owner can call this function.
     */
    function editMaxMintsPerWallet(
        uint256 newMaxMintsPerWallet
    ) external onlyOwner {
        s_maxMintsPerWallet = newMaxMintsPerWallet;

        emit MaxMintsPerWalletEdited(newMaxMintsPerWallet, block.timestamp);
    }

    /**
     * @dev function {editTimestamps}
     *
     * Sets new timestamps for the start and end of the minting phase.
     *
     * Emits a {TimestampsEdited} event indicating the new timestamps and timestamp of the edit.
     *
     * @param startTime New timestamp for the start of the minting phase.
     * @param endTime New timestamp for the end of the minting phase.
     *
     * NOTE:
     * Minting phase timestamps must be linear ({mintStart} < {mintEnd}).
     * Only the contract owner can call this function.
     */
    function editTimestamps(
        uint256 startTime,
        uint256 endTime
    ) external onlyOwner {
        if (startTime >= endTime || startTime == 0 || endTime == 0)
            revert InvalidTimestamps();

        s_mintStartTimestamp = startTime;
        s_mintEndTimestamp = endTime;

        emit TimestampsEdited(startTime, endTime, block.timestamp);
    }

    /**
     * @dev function {withdraw}
     *
     * Withdraws the contract balance to the owner addresses.
     *
     * Emits a {Withdrawal} event indicating the caller, the amount of ether withdrawn and timestamp of the withdrawal.
     *
     * NOTE: Only the contract owner can call this function.
     */
    function withdraw() external onlyOwner {
        uint256 totalAmount = address(this).balance;
        (bool success, ) = payable(owner()).call{value: totalAmount}("");

        if (!success) revert WithdrawalFailed();

        emit Withdrawal(msg.sender, totalAmount, block.timestamp);
    }
}

Install Web3 dependencies

npm install @abstract-foundation/agw-react wagmi viem@2.x @tanstack/react-query

Add Shadcn components

npx shadcn@latest add card toast button skeleton

Import NFT smart contract ABI

Create a file named contracts/GaslessNftMint.json and add the smart contract ABI:
{
  "abi": [
    {
      "inputs": [
        {
          "internalType": "string",
          "name": "_initBaseURI",
          "type": "string"
        },
        {
          "internalType": "uint256",
          "name": "_initMintStart",
          "type": "uint256"
        },
        {
          "internalType": "uint256",
          "name": "_initMintEnd",
          "type": "uint256"
        }
      ],
      "stateMutability": "nonpayable",
      "type": "constructor"
    },
    {
      "inputs": [],
      "name": "ApprovalCallerNotOwnerNorApproved",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "ApprovalQueryForNonexistentToken",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "BalanceQueryForZeroAddress",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "InsufficientFundsSent",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "InvalidMintQuantity",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "InvalidTimestamps",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "MaxMintsPerWalletExceeded",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "MaxSupplyExceeded",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "MintERC2309QuantityExceedsLimit",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "MintHasEnded",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "MintHasNotStarted",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "MintToZeroAddress",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "MintZeroQuantity",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "NotCompatibleWithSpotMints",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "OwnerQueryForNonexistentToken",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "OwnershipNotInitializedForExtraData",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "SequentialMintExceedsLimit",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "SequentialUpToTooSmall",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "SpotMintTokenIdTooSmall",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "TokenAlreadyExists",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "TransferCallerNotOwnerNorApproved",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "TransferFromIncorrectOwner",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "TransferToNonERC721ReceiverImplementer",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "TransferToZeroAddress",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "URIQueryForNonexistentToken",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "WithdrawalFailed",
      "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": "string",
          "name": "newBaseURI",
          "type": "string"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "timestamp",
          "type": "uint256"
        }
      ],
      "name": "BaseURISet",
      "type": "event"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": true,
          "internalType": "uint256",
          "name": "fromTokenId",
          "type": "uint256"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "toTokenId",
          "type": "uint256"
        },
        {
          "indexed": true,
          "internalType": "address",
          "name": "from",
          "type": "address"
        },
        {
          "indexed": true,
          "internalType": "address",
          "name": "to",
          "type": "address"
        }
      ],
      "name": "ConsecutiveTransfer",
      "type": "event"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "newMaxMintsPerWallet",
          "type": "uint256"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "timestamp",
          "type": "uint256"
        }
      ],
      "name": "MaxMintsPerWalletEdited",
      "type": "event"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": true,
          "internalType": "address",
          "name": "minter",
          "type": "address"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "quantity",
          "type": "uint256"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "timestamp",
          "type": "uint256"
        }
      ],
      "name": "Mint",
      "type": "event"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "newPrice",
          "type": "uint256"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "timestamp",
          "type": "uint256"
        }
      ],
      "name": "MintPriceEdited",
      "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"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "newSupplyCap",
          "type": "uint256"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "timestamp",
          "type": "uint256"
        }
      ],
      "name": "SupplyCapEdited",
      "type": "event"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "mintStart",
          "type": "uint256"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "mintEnd",
          "type": "uint256"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "timestamp",
          "type": "uint256"
        }
      ],
      "name": "TimestampsEdited",
      "type": "event"
    },
    {
      "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"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": true,
          "internalType": "address",
          "name": "caller",
          "type": "address"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "totalAmount",
          "type": "uint256"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "timestamp",
          "type": "uint256"
        }
      ],
      "name": "Withdrawal",
      "type": "event"
    },
    {
      "inputs": [
        {
          "internalType": "address",
          "name": "to",
          "type": "address"
        },
        {
          "internalType": "uint256",
          "name": "tokenId",
          "type": "uint256"
        }
      ],
      "name": "approve",
      "outputs": [],
      "stateMutability": "payable",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "address",
          "name": "owner",
          "type": "address"
        }
      ],
      "name": "balanceOf",
      "outputs": [
        {
          "internalType": "uint256",
          "name": "",
          "type": "uint256"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "uint256",
          "name": "newMaxMintsPerWallet",
          "type": "uint256"
        }
      ],
      "name": "editMaxMintsPerWallet",
      "outputs": [],
      "stateMutability": "nonpayable",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "uint256",
          "name": "newSupply",
          "type": "uint256"
        }
      ],
      "name": "editMaxSupply",
      "outputs": [],
      "stateMutability": "nonpayable",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "uint256",
          "name": "newPrice",
          "type": "uint256"
        }
      ],
      "name": "editMintPrice",
      "outputs": [],
      "stateMutability": "nonpayable",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "uint256",
          "name": "startTime",
          "type": "uint256"
        },
        {
          "internalType": "uint256",
          "name": "endTime",
          "type": "uint256"
        }
      ],
      "name": "editTimestamps",
      "outputs": [],
      "stateMutability": "nonpayable",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "uint256",
          "name": "tokenId",
          "type": "uint256"
        }
      ],
      "name": "getApproved",
      "outputs": [
        {
          "internalType": "address",
          "name": "",
          "type": "address"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "getBaseURI",
      "outputs": [
        {
          "internalType": "string",
          "name": "",
          "type": "string"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "getEndTimestamp",
      "outputs": [
        {
          "internalType": "uint256",
          "name": "",
          "type": "uint256"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "getMaxMintsPerWallet",
      "outputs": [
        {
          "internalType": "uint256",
          "name": "",
          "type": "uint256"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "getMaxSupply",
      "outputs": [
        {
          "internalType": "uint256",
          "name": "",
          "type": "uint256"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "getMintPrice",
      "outputs": [
        {
          "internalType": "uint256",
          "name": "",
          "type": "uint256"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "getStartTimestamp",
      "outputs": [
        {
          "internalType": "uint256",
          "name": "",
          "type": "uint256"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "address",
          "name": "userAddress",
          "type": "address"
        }
      ],
      "name": "getUserMintedNftCount",
      "outputs": [
        {
          "internalType": "uint256",
          "name": "",
          "type": "uint256"
        }
      ],
      "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": [
        {
          "internalType": "uint256",
          "name": "quantity",
          "type": "uint256"
        }
      ],
      "name": "mint",
      "outputs": [],
      "stateMutability": "payable",
      "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": "renounceOwnership",
      "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": "payable",
      "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": "payable",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "address",
          "name": "operator",
          "type": "address"
        },
        {
          "internalType": "bool",
          "name": "approved",
          "type": "bool"
        }
      ],
      "name": "setApprovalForAll",
      "outputs": [],
      "stateMutability": "nonpayable",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "string",
          "name": "baseURI",
          "type": "string"
        }
      ],
      "name": "setBaseURI",
      "outputs": [],
      "stateMutability": "nonpayable",
      "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"
    },
    {
      "inputs": [],
      "name": "totalSupply",
      "outputs": [
        {
          "internalType": "uint256",
          "name": "result",
          "type": "uint256"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "address",
          "name": "from",
          "type": "address"
        },
        {
          "internalType": "address",
          "name": "to",
          "type": "address"
        },
        {
          "internalType": "uint256",
          "name": "tokenId",
          "type": "uint256"
        }
      ],
      "name": "transferFrom",
      "outputs": [],
      "stateMutability": "payable",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "address",
          "name": "newOwner",
          "type": "address"
        }
      ],
      "name": "transferOwnership",
      "outputs": [],
      "stateMutability": "nonpayable",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "withdraw",
      "outputs": [],
      "stateMutability": "nonpayable",
      "type": "function"
    }
  ]
}

Copy and paste source code

"use client";

import React, { useState, useEffect } from "react";
import {
  Card,
  CardContent,
  CardHeader,
  CardTitle,
  CardDescription,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { useToast } from "@/hooks/use-toast";
import {
  useLoginWithAbstract,
  useWriteContractSponsored,
} from "@abstract-foundation/agw-react";
import GaslessNft from "@/contracts/GaslessNftMint.json";
import { getGeneralPaymasterInput } from "viem/zksync";
import {
  useAccount,
  useReadContracts,
  useWaitForTransactionReceipt,
} from "wagmi";
import { formatEther } from "viem";

const AbstractGaslessNftMint = ({
  nftContractAddress,
  paymasterContractAddress,
}: {
  nftContractAddress: `0x${string}`;
  paymasterContractAddress: `0x${string}`;
}) => {
  const [mounted, setMounted] = useState<boolean>(false);
  const [mintQuantity, setMintQuantity] = useState<number>(1);
  const [txHash, setTxHash] = useState<`0x${string}` | undefined>(undefined);

  const { address, isConnected, isConnecting, isReconnecting } = useAccount();
  const { login } = useLoginWithAbstract();
  const { writeContractSponsoredAsync } = useWriteContractSponsored();

  const { toast } = useToast();

  const currentUnixTimestamp = Math.floor(Date.now()) / 1000;

  // ================================================== CONTRACT READS ===================================================
  const { data, isLoading, refetch } = useReadContracts({
    contracts: [
      {
        address: nftContractAddress,
        abi: GaslessNft.abi,
        functionName: "totalSupply",
      },
      {
        address: nftContractAddress,
        abi: GaslessNft.abi,
        functionName: "getMaxSupply",
      },
      {
        address: nftContractAddress,
        abi: GaslessNft.abi,
        functionName: "getUserMintedNftCount",
        args: [address],
      },
      {
        address: nftContractAddress,
        abi: GaslessNft.abi,
        functionName: "getMaxMintsPerWallet",
      },
      {
        address: nftContractAddress,
        abi: GaslessNft.abi,
        functionName: "getMintPrice",
      },
      {
        address: nftContractAddress,
        abi: GaslessNft.abi,
        functionName: "getStartTimestamp",
      },
      {
        address: nftContractAddress,
        abi: GaslessNft.abi,
        functionName: "getEndTimestamp",
      },
    ],
  });

  // ================================================== CONTRACT WRITES ==================================================
  const handleMint = async () => {
    try {
      const hash = await writeContractSponsoredAsync({
        abi: GaslessNft.abi,
        address: nftContractAddress,
        functionName: "mint",
        args: [BigInt(mintQuantity)],
        value: (data?.[4]?.result as bigint) * BigInt(mintQuantity),
        paymaster: paymasterContractAddress,
        paymasterInput: getGeneralPaymasterInput({
          innerInput: "0x",
        }),
      });

      setTxHash(hash);
    } catch (error) {
      console.log(error);
    }
  };

  const {
    isSuccess: isTxSuccessful,
    isLoading: isTxProcessing,
    isError: isTxError,
  } = useWaitForTransactionReceipt({
    hash: txHash,
  });

  useEffect(() => {
    if (isTxSuccessful) {
      toast({
        title: "NFT Minted",
        description: "Your NFT(s) has been minted successfully",
      });
      refetch();
      setMintQuantity(1);
    }

    if (isTxError) {
      toast({
        title: "Execution reverted",
        description: "Your transaction was reverted when processing.",
        variant: "destructive",
      });
    }
  }, [isTxSuccessful, isTxError]);

  const handleIncrement = (remainingAllocation: number) => {
    if (mintQuantity < remainingAllocation) {
      setMintQuantity(mintQuantity + 1);
    }
  };

  const handleDecrement = () => {
    if (mintQuantity > 1) {
      setMintQuantity(mintQuantity - 1);
    }
  };

  useEffect(() => {
    setMounted(true);
  }, []);

  if (!mounted)
    return (
      <div className="w-full h-56 flex items-center">
        <div className="flex justify-center items-center gap-2 w-full h-44 text-[#A1A1AA]">
          <div className="w-3.5 h-3.5 border-2 border-gray-400 border-b-transparent rounded-full inline-block box-border animate-spin" />
          Loading
        </div>
      </div>
    );

  return (
    <Card className="max-w-[650px] md:w-[650px] md:h-[347px] shadow border-[0.5px] border-white/15 font-mono relative">
      {/* Gradient bottom border */}
      <div className="absolute inset-x-0 bottom-0 h-[1px] w-full mx-auto bg-gradient-to-r from-transparent via-[#02CA6A] to-transparent" />
      <div className="absolute inset-x-0 bottom-0 h-[4px] w-1/2 mx-auto bg-gradient-to-r from-transparent via-[#02CA6A] to-transparent blur-sm" />

      <CardHeader>
        <CardTitle>Mint your NFT on Abstract</CardTitle>
        <CardDescription>
          Mint your very own unique NFT in a couple clicks. GAS FREE!
        </CardDescription>
      </CardHeader>
      <CardContent className="flex flex-col md:flex-row items-center gap-10 w-full">
        <Logo className="w-[200px] h-[200px] mx-4" />

        {!isConnected || !address || isReconnecting || isConnecting ? (
          <div className="flex flex-col w-full">
            <p className="text-white text-sm py-20 text-center">
              Please connect wallet to mint your NFT
            </p>
            <Button
              onClick={login}
              className="bg-[#02CA6A]/70 w-full mt-2 text-white rounded-[4px] hover:text-black"
            >
              Connect wallet
            </Button>
          </div>
        ) : isLoading ? (
          <div className="w-full font-medium">
            <div className="flex justify-center">
              <Skeleton className="h-5 w-32 mb-6" />
            </div>
            <div className="flex justify-between gap-4 mb-2">
              <Skeleton className="h-5 w-14" />
              <Skeleton className="h-5 w-20" />
            </div>
            <div className="flex justify-between gap-4 mb-2">
              <Skeleton className="h-5 w-14" />
              <Skeleton className="h-5 w-20" />
            </div>
            <div className="flex justify-between items-center gap-4 mb-4">
              <Skeleton className="h-5 w-14" />
              <Skeleton className="h-5 w-20" />
            </div>

            <div className="flex justify-between items-center">
              <Button className="bg-[#02CA6A]/70 text-white">-</Button>
              <p className="font-bold text-3xl">1</p>
              <Button className="bg-[#02CA6A]/70 text-white">+</Button>
            </div>

            <Button
              disabled
              className="bg-[#02CA6A]/70 mt-6 w-full flex items-center gap-2 text-white"
            >
              <div className="w-3.5 h-3.5 border-2 border-gray-400 border-b-transparent rounded-full inline-block box-border animate-spin" />{" "}
              loading
            </Button>
          </div>
        ) : (
          data &&
          Array.isArray(data) &&
          data[0]?.result != null &&
          data[1]?.result != null &&
          data[2]?.result != null &&
          data[3]?.result != null &&
          data[4]?.result != null &&
          data[5]?.result != null &&
          data[6]?.result != null && (
            <div className="w-full font-medium">
              <p className="font-bold text-center mb-4">
                {data[0].result.toString()} / {data[1].result.toString()} Minted
              </p>

              <div className="flex justify-between gap-4 mb-2">
                <p>Remaining allocation</p>
                <p>{Number(data[3].result) - Number(data[2].result)}</p>
              </div>
              <div className="flex justify-between gap-4 mb-2">
                <p>Price</p>
                <p>
                  {Number(formatEther(data[4].result as bigint)).toFixed(2)} Eth
                </p>
              </div>
              <div className="flex justify-between items-center gap-4 mb-4">
                <p>Mint status</p>

                {currentUnixTimestamp < Number(data[5].result) ? (
                  <p className="text-red-600">Not started</p>
                ) : currentUnixTimestamp > Number(data[6].result) ? (
                  <p className="text-red-600">Ended</p>
                ) : (
                  <p className="text-green-600">Live</p>
                )}
              </div>

              <div className="flex justify-between items-center">
                <Button
                  onClick={handleDecrement}
                  className="bg-[#02CA6A]/70 text-white hover:text-black rounded-[4px]"
                >
                  -
                </Button>
                <p className="font-bold text-3xl">{mintQuantity}</p>
                <Button
                  onClick={() =>
                    handleIncrement(
                      Number(data[3].result) - Number(data[2].result)
                    )
                  }
                  className="bg-[#02CA6A]/70 text-white hover:text-black rounded-[4px]"
                >
                  +
                </Button>
              </div>

              <Button
                onClick={handleMint}
                disabled={
                  isTxProcessing ||
                  Number(data[3].result) === Number(data[2].result)
                }
                className="bg-[#02CA6A]/70 mt-4 w-full text-white hover:text-black rounded-[4px]"
              >
                {Number(data[3].result) === Number(data[2].result) ? (
                  "Max allocation minted"
                ) : isTxProcessing ? (
                  <div className="flex items-center gap-2">
                    <div className="w-3.5 h-3.5 border-2 border-gray-400 border-b-transparent rounded-full inline-block box-border animate-spin" />{" "}
                    Minting {mintQuantity} {mintQuantity === 1 ? "NFT" : "NFTs"}
                  </div>
                ) : (
                  `Mint ${mintQuantity} for
                  ${(
                    mintQuantity * Number(formatEther(data[4].result as bigint))
                  ).toFixed(2)} Eth`
                )}
              </Button>
            </div>
          )
        )}
      </CardContent>
    </Card>
  );
};

export default AbstractGaslessNftMint;

const Logo: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
  return (
    <svg
      width="28"
      height="25"
      viewBox="0 0 28 25"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
      {...props}
    >
      <path
        d="M23.5006 22.2831L18.0126 16.9277H9.34632L3.85828 22.2831L6.43334 24.7959L11.9214 19.4405C12.3919 18.9814 13.0122 18.7309 13.6795 18.7309C14.3468 18.7309 14.967 18.9814 15.4375 19.4405L20.9256 24.7959L23.5006 22.2831Z"
        fill="#02CA6A"
      />
      <path
        d="M18.928 15.3833L26.4221 17.341L27.3632 13.9057L19.869 11.948C19.2274 11.7811 18.6927 11.3804 18.359 10.8169C18.0254 10.2575 17.9399 9.6022 18.111 8.9761L20.1171 1.66308L16.5967 0.744781L14.5906 8.05779L18.9237 15.3792L18.928 15.3833Z"
        fill="#02CA6A"
      />
      <path
        d="M0.941051 17.341L8.43524 15.3833L8.43952 15.3792L12.7726 8.05779L10.7665 0.744781L7.24609 1.66308L9.25224 8.9761C9.42334 9.6022 9.33779 10.2575 9.00415 10.8169C8.6705 11.3804 8.13582 11.7811 7.49419 11.948L0 13.9057L0.941051 17.341Z"
        fill="#02CA6A"
      />
    </svg>
  );
};

Props

PropTypeDescriptionDefault value
nftContractAddress`0x${string}`Deployed NFT smart contract address of your collectionundefined
paymasterContractAddress`0x${string}`Deployed paymaster smart contract address for handling gas sponsoringundefined

See something you like?

Take your project further with our advanced custom development.

"One of the only full-stack Web3 component libraries i've seen in the space so far. Ten out of ten recommended. Saved me a ton of time. Can't wait to see what templates they release next."

Samy

Side projects builder