Розробка лендингу токенсейлу
Ми створюємо лендинги токенсейлів як повноцінні 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 місяць гарантійної підтримки після запуску |
Орієнтовні терміни та етапи роботи
- Аналіз та проектування — 2-3 дні: обговорюємо токеноміку, whitelist, раунди.
- Розробка смарт-контракту — 5-7 днів: написання, тести на Foundry, аудит.
- Розробка frontend — 7-10 днів: інтеграція, верстка, real-time.
- Тестування — 3-5 днів: навантажувальне тестування, QA.
- Деплой та запуск — 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 години.
Замовте розробку лендингу під ключ — ми проаналізуємо ваш проект і запропонуємо оптимальне рішення. Зв'яжіться з нами для консультації.







