Реалізація мінтингу NFT через веб-інтерфейс

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація мінтингу NFT через веб-інтерфейс
Складна
~5 робочих днів
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Реалізація мінтингу NFT через веб-інтерфейс

Мінтинг NFT — це транзакція в блокчейні, яка створює новий токен у контракті. Веб-інтерфейс для мінтингу — це UI поверх смарт-контракту: підключення гаманця, перевірка умов (whitelist, публічна продажа, ліміти), відправка транзакції, відстеження статусу, фінальний екран з токеном.

Складність реалізації залежить від механіки контракту: вільний мінт, whitelist через merkle tree, ERC-2981 royalties, крива ціни, максимум на гаманець.

Аналіз контракту перед розробкою

Перший крок — вивчити ABI контракту. Типовий мінт-контракт (ERC-721A — більш популярний за чистий ERC-721 для дешевого пакетного мінтингу):

// Типові функції мінт-контракту
function mint(uint256 quantity) external payable;
function whitelistMint(uint256 quantity, bytes32[] calldata proof) external payable;
function totalSupply() external view returns (uint256);
function maxSupply() external view returns (uint256);
function mintPrice() external view returns (uint256);
function maxPerWallet() external view returns (uint256);
function saleState() external view returns (uint8); // 0=paused, 1=whitelist, 2=public
function numberMinted(address owner) external view returns (uint256);

Читання стану контракту

// lib/mintContract.ts
import { createPublicClient, http, parseAbi } from 'viem';
import { mainnet } from 'viem/chains';

const MINT_ABI = parseAbi([
  'function totalSupply() view returns (uint256)',
  'function maxSupply() view returns (uint256)',
  'function mintPrice() view returns (uint256)',
  'function maxPerWallet() view returns (uint256)',
  'function saleState() view returns (uint8)',
  'function numberMinted(address) view returns (uint256)',
  'function mint(uint256 quantity) payable',
  'function whitelistMint(uint256 quantity, bytes32[] proof) payable',
]);

export async function getMintState(
  contractAddress: `0x${string}`,
  walletAddress: `0x${string}` | null,
) {
  const client = createPublicClient({ chain: mainnet, transport: http() });

  const calls = [
    { address: contractAddress, abi: MINT_ABI, functionName: 'totalSupply' },
    { address: contractAddress, abi: MINT_ABI, functionName: 'maxSupply' },
    { address: contractAddress, abi: MINT_ABI, functionName: 'mintPrice' },
    { address: contractAddress, abi: MINT_ABI, functionName: 'maxPerWallet' },
    { address: contractAddress, abi: MINT_ABI, functionName: 'saleState' },
    ...(walletAddress ? [{
      address: contractAddress,
      abi: MINT_ABI,
      functionName: 'numberMinted',
      args: [walletAddress],
    }] : []),
  ] as const;

  const results = await client.multicall({ contracts: calls });

  return {
    totalSupply: results[0].result as bigint,
    maxSupply: results[1].result as bigint,
    mintPrice: results[2].result as bigint,
    maxPerWallet: results[3].result as bigint,
    saleState: results[4].result as number,
    numberMinted: walletAddress ? (results[5].result as bigint) : 0n,
  };
}

Whitelist через Merkle Tree

Більшість сучасних мінтів використовує merkle proof замість зберігання всіх whitelist-адрес у контракті. Proof генерується на фронтенді за адресою користувача:

// lib/merkle.ts
import { MerkleTree } from 'merkletreejs';
import { keccak256, encodePacked } from 'viem';

// allowlist.json — масив адрес з CMS або API
import allowlist from '@/data/allowlist.json';

function hashLeaf(address: string): `0x${string}` {
  return keccak256(encodePacked(['address'], [address as `0x${string}`]));
}

const leaves = allowlist.map(hashLeaf);
const tree = new MerkleTree(leaves, keccak256, { sortPairs: true });

export function getMerkleProof(address: string): `0x${string}`[] {
  const leaf = hashLeaf(address);
  return tree.getHexProof(leaf) as `0x${string}`[];
}

export function isWhitelisted(address: string): boolean {
  const leaf = hashLeaf(address);
  return tree.verify(tree.getHexProof(leaf), leaf, tree.getRoot());
}

Транзакція мінтингу

// hooks/useMint.ts
import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi';
import { parseEther } from 'viem';

export function useMint(contractAddress: `0x${string}`) {
  const { writeContract, data: txHash, isPending, error } = useWriteContract();
  const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({
    hash: txHash,
  });

  const mint = async (quantity: number, price: bigint) => {
    writeContract({
      address: contractAddress,
      abi: MINT_ABI,
      functionName: 'mint',
      args: [BigInt(quantity)],
      value: price * BigInt(quantity),
    });
  };

  const whitelistMint = async (
    quantity: number,
    price: bigint,
    proof: `0x${string}`[],
  ) => {
    writeContract({
      address: contractAddress,
      abi: MINT_ABI,
      functionName: 'whitelistMint',
      args: [BigInt(quantity), proof],
      value: price * BigInt(quantity),
    });
  };

  return { mint, whitelistMint, txHash, isPending, isConfirming, isSuccess, error };
}

UI компонент мінтингу

// components/MintWidget.tsx
import { useState } from 'react';
import { formatEther } from 'viem';
import { useAccount } from 'wagmi';
import { useMint } from '@/hooks/useMint';
import { getMintState } from '@/lib/mintContract';
import { getMerkleProof, isWhitelisted } from '@/lib/merkle';
import { useQuery } from '@tanstack/react-query';

const CONTRACT = process.env.NEXT_PUBLIC_CONTRACT_ADDRESS as `0x${string}`;

export function MintWidget() {
  const { address, isConnected } = useAccount();
  const [quantity, setQuantity] = useState(1);
  const { mint, whitelistMint, isPending, isConfirming, isSuccess, txHash } = useMint(CONTRACT);

  const { data: state } = useQuery({
    queryKey: ['mintState', address],
    queryFn: () => getMintState(CONTRACT, address ?? null),
    refetchInterval: 10_000,
  });

  if (!state) return <MintSkeleton />;

  const sold = Number(state.totalSupply);
  const total = Number(state.maxSupply);
  const priceEth = formatEther(state.mintPrice);
  const remaining = total - sold;
  const canMint = Number(state.maxPerWallet) - Number(state.numberMinted);

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

    if (state.saleState === 1) {
      // Whitelist sale
      if (!isWhitelisted(address)) return;
      const proof = getMerkleProof(address);
      await whitelistMint(quantity, state.mintPrice, proof);
    } else {
      await mint(quantity, state.mintPrice);
    }
  };

  return (
    <div className="space-y-6 rounded-2xl border border-white/10 bg-neutral-900 p-6">
      {/* Прогрес */}
      <div>
        <div className="mb-2 flex justify-between text-sm">
          <span>{sold} / {total} заминтено</span>
          <span>{remaining} залишилось</span>
        </div>
        <div className="h-2 overflow-hidden rounded-full bg-neutral-800">
          <div
            className="h-full rounded-full bg-blue-500 transition-all"
            style={{ width: `${(sold / total) * 100}%` }}
          />
        </div>
      </div>

      {/* Кількість */}
      <QuantitySelector
        value={quantity}
        onChange={setQuantity}
        max={Math.min(canMint, remaining, 10)}
      />

      {/* Загальна вартість */}
      <div className="flex justify-between text-sm">
        <span className="text-neutral-400">Вартість</span>
        <span>{(parseFloat(priceEth) * quantity).toFixed(4)} ETH</span>
      </div>

      {/* Кнопка */}
      <MintButton
        state={state.saleState}
        address={address}
        isConnected={isConnected}
        isPending={isPending || isConfirming}
        isSuccess={isSuccess}
        canMint={canMint > 0}
        onClick={handleMint}
      />

      {/* Статус транзакції */}
      {txHash && (
        <a
          href={`https://etherscan.io/tx/${txHash}`}
          target="_blank"
          rel="noopener noreferrer"
          className="block text-center text-xs text-blue-400 hover:underline"
        >
          Переглянути транзакцію →
        </a>
      )}
    </div>
  );
}

Обробка помилок

Мінтинг не вдається з багатьох причин: недостатньо ETH, перевищено ліміт гаманця, продажа не активна, неверний proof. Помилки з viem містять ABI-декодовано повідомлення контракту:

import { ContractFunctionRevertedError, UserRejectedRequestError } from 'viem';

function parseMintError(error: Error): string {
  if (error instanceof UserRejectedRequestError) {
    return 'Транзакція відхилена в гаманці';
  }
  if (error instanceof ContractFunctionRevertedError) {
    const reason = error.data?.errorName ?? error.message;
    const messages: Record<string, string> = {
      'ExceedsMaxPerWallet': 'Перевищено ліміт токенів на гаманець',
      'SaleNotActive': 'Продажа ще не розпочалась',
      'InvalidMerkleProof': 'Ваша адреса не у вайтлисті',
      'InsufficientFunds': 'Недостатньо ETH',
      'MaxSupplyReached': 'Усі токени заминтені',
    };
    return messages[reason] ?? `Помилка контракту: ${reason}`;
  }
  return 'Невідома помилка';
}

Часова шкала: інтерфейс мінтингу з публічним мінтом, прогрес-баром та базовою обробкою помилок — 2–3 дні. Повна реалізація з whitelist через merkle tree, багатофазною продажею та фінальним екраном з токеном — 4–6 днів.