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

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

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

Часті запитання

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

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1450
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1309
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    1004
  • 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 години.

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