Реализация минтинга 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, price curve, максимум на кошелёк.

Анализ контракта перед разработкой

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

// Типичные функции минт-контракта
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 дней.