Розробка лендингу токенсейлу
Ми створюємо лендинги токенсейлів як повноцінні 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 години.
Замовте розробку лендингу під ключ — ми проаналізуємо ваш проект і запропонуємо оптимальне рішення. Зв'яжіться з нами для консультації.







