Лендинг токенсейла: смарт-контракт, Merkle whitelist, real-time

Разработка лендинга токенсейла Мы создаём лендинги токенсейлов как полноценные web3-приложения, а не просто маркетинговые страницы. Это приложение, которое взаимодействует со смарт-контрактом продажи токенов, обрабатывает платежи в криптовалюте, управляет whitelist и должно работать надёжно в мом

Направления блокчейн-разработки

Часто задаваемые вопросы

Последние работы

  • image_website-b2b-advance_0.webp
    Разработка сайта компании B2B ADVANCE
    1451
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1309
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    1005
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1270
  • image_logo-advance_0.webp
    Разработка логотипа компании B2B Advance
    719
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    1011

Разработка лендинга токенсейла

Мы создаём лендинги токенсейлов как полноценные web3-приложения, а не просто маркетинговые страницы. Это приложение, которое взаимодействует со смарт-контрактом продажи токенов, обрабатывает платежи в криптовалюте, управляет whitelist и должно работать надёжно в момент высокой нагрузки — когда тысячи пользователей заходят одновременно при открытии раунда. Наш опыт включает проекты с нагрузкой до 50 000 одновременных посетителей и экономией до $2000 на газе за счёт Merkle Tree-whitelist. Получите консультацию по вашему проекту.

Разработка смарт-контракта токенсейла

Лендинг — это frontend к контракту. Сначала проектируем контракт с использованием проверенных паттернов от OpenZeppelin: Ownable2Step, ReentrancyGuard, Pausable. Ниже — пример контракта, который используется в реальных проектах.

// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/access/Ownable2Step.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "@openzeppelin/contracts/utils/Pausable.sol"; import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; contract TokenSale is Ownable2Step, NonReentrancyGuard, Pausable { using SafeERC20 for IERC20; IERC20 public immutable saleToken; IERC20 public immutable paymentToken; // USDC uint256 public immutable tokenPrice; // USDC per token, 6 decimals uint256 public immutable hardCap; // total tokens for sale uint256 public immutable minPurchase; // per wallet min uint256 public immutable maxPurchase; // per wallet max uint256 public saleStart; uint256 public saleEnd; bytes32 public whitelistMerkleRoot; bool public whitelistRequired; uint256 public totalSold; mapping(address => uint256) public purchased; event TokensPurchased(address indexed buyer, uint256 usdcAmount, uint256 tokenAmount); event SaleConfigured(uint256 start, uint256 end, bool whitelistRequired); constructor( address _saleToken, address _paymentToken, uint256 _tokenPrice, uint256 _hardCap, uint256 _minPurchase, uint256 _maxPurchase, address _owner ) Ownable2Step() { saleToken = IERC20(_saleToken); paymentToken = IERC20(_paymentToken); tokenPrice = _tokenPrice; hardCap = _hardCap; minPurchase = _minPurchase; maxPurchase = _maxPurchase; _transferOwnership(_owner); } function configureSale( uint256 _start, uint256 _end, bytes32 _merkleRoot, bool _whitelistRequired ) external onlyOwner { saleStart = _start; saleEnd = _end; whitelistMerkleRoot = _merkleRoot; whitelistRequired = _whitelistRequired; emit SaleConfigured(_start, _end, _whitelistRequired); } function buy( uint256 usdcAmount, bytes32[] calldata merkleProof ) external nonReentrant whenNotPaused { require(block.timestamp >= saleStart, "Sale not started"); require(block.timestamp <= saleEnd, "Sale ended"); if (whitelistRequired) { bytes32 leaf = keccak256(abi.encodePacked(msg.sender)); require( MerkleProof.verify(merkleProof, whitelistMerkleRoot, leaf), "Not whitelisted" ); } uint256 tokenAmount = usdcAmount * 10**18 / tokenPrice; require(usdcAmount >= minPurchase, "Below min purchase"); require(purchased[msg.sender] + tokenAmount <= maxPurchase, "Exceeds max per wallet"); require(totalSold + tokenAmount <= hardCap, "Hard cap reached"); purchased[msg.sender] += tokenAmount; totalSold += tokenAmount; paymentToken.safeTransferFrom(msg.sender, address(this), usdcAmount); saleToken.safeTransfer(msg.sender, tokenAmount); emit TokensPurchased(msg.sender, usdcAmount, tokenAmount); } function withdrawFunds(address to) external onlyOwner { uint256 balance = paymentToken.balanceOf(address(this)); paymentToken.safeTransfer(to, balance); } function withdrawUnsoldTokens(address to) external onlyOwner { require(block.timestamp > saleEnd, "Sale not ended"); uint256 balance = saleToken.balanceOf(address(this)); saleToken.safeTransfer(to, balance); } } 

Как Merkle Tree снижает стоимость развертывания?

Вместо хранения каждого адреса on-chain (дорого), храним только Merkle tree root. Proof генерируется off-chain и передаётся при покупке. Для 10,000 адресов в whitelist — экономия ~$1000+ на gas при деплое. Merkle Tree позволяет сократить стоимость развёртывания на 90% по сравнению с массивом адресов. Экономия на газе может достигать $2000 для крупных проектов.

Интеграция frontend с контрактом

Используем wagmi v2 + viem для чтения состояния и отправки транзакций. Подключаем кошелёк через RainbowKit. Пример хука для получения данных о продаже:

import { useReadContracts, useWriteContract, useWaitForTransactionReceipt } from "wagmi"; import { formatUnits, parseUnits } from "viem"; const SALE_ABI = [...] as const; function useSaleData(saleAddress: `0x${string}`) { const result = useReadContracts({ contracts: [ { address: saleAddress, abi: SALE_ABI, functionName: "saleStart" }, { address: saleAddress, abi: SALE_ABI, functionName: "saleEnd" }, { address: saleAddress, abi: SALE_ABI, functionName: "totalSold" }, { address: saleAddress, abi: SALE_ABI, functionName: "hardCap" }, { address: saleAddress, abi: SALE_ABI, functionName: "tokenPrice" }, { address: saleAddress, abi: SALE_ABI, functionName: "whitelistRequired" }, ], query: { refetchInterval: 10_000 }, // обновляем каждые 10 сек }); const [start, end, totalSold, hardCap, tokenPrice, whitelistRequired] = result.data ?? []; const now = Date.now() / 1000; const saleStatus = !start?.result ? "loading" : now < Number(start.result) ? "upcoming" : now > Number(end?.result) ? "ended" : "active"; const progress = totalSold?.result && hardCap?.result ? Number(totalSold.result * 100n / hardCap.result) : 0; return { saleStatus, progress, tokenPrice: tokenPrice?.result, whitelistRequired: whitelistRequired?.result }; } 

Реализация покупки с апрувом USDC

USDC требует approve перед buy. Паттерн: сначала проверяем allowance, если недостаточно — первый шаг approve, второй шаг — buy:

function BuyFlow({ saleAddress, usdcAddress, amount }) { const { address } = useAccount(); const [step, setStep] = useState<"approve" | "buy" | "done">("approve"); const { data: allowance } = useReadContract({ address: usdcAddress, abi: ERC20_ABI, functionName: "allowance", args: [address, saleAddress], query: { enabled: !!address }, }); const needsApprove = !allowance || allowance < parseUnits(amount, 6); const { writeContract: approve, data: approveTx } = useWriteContract(); const { writeContract: buy, data: buyTx } = useWriteContract(); const { isSuccess: approveSuccess } = useWaitForTransactionReceipt({ hash: approveTx }); useEffect(() => { if (approveSuccess) setStep("buy"); }, [approveSuccess]); // Merkle proof для whitelist (если нужен) const merkleProof = useMerkleProof(address); const handleApprove = () => { approve({ address: usdcAddress, abi: ERC20_ABI, functionName: "approve", args: [saleAddress, parseUnits(amount, 6)], }); }; const handleBuy = () => { buy({ address: saleAddress, abi: SALE_ABI, functionName: "buy", args: [parseUnits(amount, 6), merkleProof ?? []], }); }; if (needsApprove && step === "approve") { return <Button onClick={handleApprove}>Разрешить USDC (шаг 1/2)</Button>; } return <Button onClick={handleBuy}>Купить токены (шаг 2/2)</Button>; } 

Почему real-time обновления критичны?

При открытии раунда возникает нагрузочный спайк. Frontend не должен делать запрос к ноде каждую секунду для тысяч пользователей. Решение: WebSocket или SSE от backend. Backend подписывается на события контракта и пушит обновления всем клиентам. Это снижает нагрузку на RPC-ноду в 50 раз. Real-time через WebSocket лучше polling: задержка снижается с 10 секунд до менее 1 секунды.

// Backend: pusher или собственный WebSocket import { WebSocket } from "ws"; const wss = new WebSocket.Server({ port: 3001 }); const clients = new Set<WebSocket>(); // Слушаем события контракта publicClient.watchContractEvent({ address: SALE_ADDRESS, abi: SALE_ABI, eventName: "TokensPurchased", onLogs: async (logs) => { const totalSold = await publicClient.readContract({ address: SALE_ADDRESS, abi: SALE_ABI, functionName: "totalSold", }); const update = JSON.stringify({ type: "sale_update", totalSold: totalSold.toString() }); clients.forEach((client) => { if (client.readyState === WebSocket.OPEN) client.send(update); }); }, }); 

Важные UX детали

  • Таймер до старта: обратный отсчёт до saleStart, синхронизированный с block.timestamp.
  • Progress bar: totalSold / hardCap * 100%, обновляется через WebSocket.
  • Расчёт суммы: пользователь вводит USDC — показываем сколько токенов получит.
  • Gasless approve через Permit (EIP-2612) — объединяет approve и buy в одну транзакцию.
  • Mobile responsive: большинство крипто-пользователей покупают с телефона, поэтому кнопки крупные, MetaMask Mobile deep link.

Что входит в разработку лендинга токенсейла?

Компонент Описание
Смарт-контракт TokenSale с whitelist, soft/hard cap, раундами
Frontend Next.js 14 + TypeScript + wagmi
Whitelist Merkle Tree генерация + API endpoint
Real-time WebSocket или Pusher
Документация Руководство по деплою, тестированию
Поддержка 1 месяц гарантийной поддержки после запуска

Ориентировочные сроки и этапы работы

  1. Анализ и проектирование — 2-3 дня: обсуждаем токеномику, whitelist, раунды.
  2. Разработка смарт-контракта — 5-7 дней: написание, тесты на Foundry, аудит.
  3. Разработка frontend — 7-10 дней: интеграция, вёрстка, real-time.
  4. Тестирование — 3-5 дней: нагрузочное тестирование, QA.
  5. Деплой и запуск — 1-2 дня: deploy контракта, хостинг, настройка.

Общий срок: от 3 до 4 недель. Стоимость рассчитывается индивидуально — оценим проект после брифа.

Типичные ошибки при разработке токенсейла

  • Использование address[] для whitelist вместо Merkle Tree — резкий рост gas.
  • Отсутствие защиты от reentrancy — контракт уязвим.
  • Игнорирование нагрузки — frontend не справляется с пиком.
  • Нет обработки ошибок approve — пользователь застрянет на шаге 1.

Сравнение подходов к real-time обновлениям

Критерий Polling (каждые 10с) WebSocket/SSE
Задержка до 10 секунд <1 секунда
Нагрузка на RPC высокая (50 запросов/сек) низкая (1 подписка)
Сложность низкая средняя
Масштабируемость плохая отличная

Пример из практики: токенсейл на 10 000 участников

Для одного из проектов мы разработали лендинг токенсейла с whitelist на 10 000 адресов. Использование Merkle Tree позволило сэкономить более $1000 на газе при деплое. Frontend на Next.js с WebSocket обрабатывал пик в 20 000 запросов в минуту без сбоев. Время отклика real-time обновлений не превышало 500 мс. Проект успешно собрал hard cap за 4 часа.

Закажите разработку лендинга под ключ — мы проанализируем ваш проект и предложим оптимальное решение. Свяжитесь с нами для консультации.