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

Проєктуємо та розробляємо блокчейн-рішення повного циклу: від архітектури смарт-контрактів до запуску DeFi-протоколів, NFT-маркетплейсів та криптобірж. Аудит безпеки, токеноміка, інтеграція з наявною інфраструктурою.
Показано 1 з 1Усі 1306 послуг
Розробка лендингу токенсейлу
Середній
~3-5 днів
Часті запитання

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

Етапи блокчейн-розробки

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

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1286
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1198
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    902
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1122
  • image_logo-advance_0.webp
    Розробка логотипу компанії B2B Advance
    589
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    859

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

Лендинг токен-сейлу — це не просто маркетингова сторінка. Це web3-додаток, який взаємодіє зі смарт-контрактом продажу токенів, обробляє платежі в крипто, керує whitelist, та повинен працювати надійно при високій нагрузці (коли тисячі користувачів заходять одночасно при відкритті раунду).

Смарт-контракт

Лендинг — це frontend до контракту. Спочатку проектуємо контракт.

// 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, ReentrancyGuard, Pausable {
    using SafeERC20 for IERC20;
    
    IERC20 public immutable saleToken;
    IERC20 public immutable paymentToken;  // USDC
    
    uint256 public immutable tokenPrice;    // USDC на токен, 6 decimals
    uint256 public immutable hardCap;       // усього токенів на продаж
    uint256 public immutable minPurchase;   // мінімум на гаманець
    uint256 public immutable maxPurchase;   // максимум на гаманець
    
    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 whitelist: замість зберігання кожної адреси on-chain (дорого), зберігаємо тільки merkle root. Proof генерується off-chain та передається при покупці. Для 10,000 адрес в whitelist — економія ~$1000+ на gas при деплоюванні.

Frontend: Web3 інтеграція

Підключення гаманця та стан сейлу

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>;
}

Merkle Tree whitelist: генерація та управління

import { MerkleTree } from "merkletreejs";
import { keccak256, encodePacked } from "viem";

function generateMerkleTree(addresses: string[]) {
  const leaves = addresses.map((addr) =>
    keccak256(encodePacked(["address"], [addr as `0x${string}`]))
  );
  
  const tree = new MerkleTree(leaves, keccak256, { sortPairs: true });
  const root = tree.getHexRoot();
  
  return { tree, root };
}

function getMerkleProof(tree: MerkleTree, address: string): `0x${string}`[] {
  const leaf = keccak256(encodePacked(["address"], [address as `0x${string}`]));
  return tree.getHexProof(leaf) as `0x${string}`[];
}

// API endpoint: GET /api/whitelist/proof?address=0x...
async function getProofForAddress(req, res) {
  const { address } = req.query;
  const whitelist = await loadWhitelistFromDB(); // ваша логіка
  const { tree } = generateMerkleTree(whitelist);
  const proof = getMerkleProof(tree, address);
  
  if (proof.length === 0) {
    return res.status(403).json({ error: "Not whitelisted" });
  }
  
  res.json({ proof, address });
}

Real-time оновлення та черга

При відкритті раунду — спайк нагрузки. Frontend не повинен запитувати ноду кожну секунду для тисяч користувачів.

Рішення: WebSocket або SSE від backend → клієнти. Backend підписується на события контракту, при TokensPurchased пушить оновлені дані всім підключеним клієнтам.

// 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 в одну транзакцію через permit + buy паттерн — покращує UX.

Mobile responsive: більшість крипто-користувачів купують з телефону. Кнопки великі, MetaMask Mobile deep link.

Стек

Компонент Технологія
Frontend Next.js 14 + TypeScript
Web3 wagmi v2 + viem + RainbowKit
Whitelist Merkle Tree + API endpoint
Real-time WebSocket або Pusher
Analytics custom события + Dune Dashboard
Hosting Vercel + Cloudflare

Строк розроблення: 2–3 тижні для full-stack лендинга зі смарт-контрактом, whitelist, real-time оновленнями. Дизайн та маркетингові контенти — окремо.