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







