Реализация Token Launchpad (IDO/ICO) на сайте

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация Token Launchpad (IDO/ICO) на сайте
Сложная
~2-4 недели
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Реализация Token Launchpad (IDO/ICO) на сайте

Token Launchpad — это интерфейс для публичной продажи токенов. Пользователи приходят, вносят ETH/USDC, получают распределение токенов. Под капотом: смарт-контракт прессейла, UI с таймером, прогрессом сбора, персональными лимитами, whitelist-механизмом и клеймингом после TGE.

Это сложная фронтенд-задача: много состояний (до старта, активная продажа, между раундами, клейминг), разные сценарии для whitelist и public участников, критичная точность в расчётах токенов.

Фазы лаunchpad

Upcoming → Whitelist Round → Public Round → Ended → Claiming

Каждая фаза — своё UI-состояние, свои доступные действия, своя логика смарт-контракта.

Чтение состояния контракта прессейла

// lib/presale.ts
import { createPublicClient, http, parseAbi } from 'viem';
import { mainnet } from 'viem/chains';

const PRESALE_ABI = parseAbi([
  'function salePhase() view returns (uint8)',        // 0=upcoming, 1=whitelist, 2=public, 3=ended
  'function startTime() view returns (uint256)',
  'function endTime() view returns (uint256)',
  'function claimStartTime() view returns (uint256)',
  'function hardCap() view returns (uint256)',
  'function softCap() view returns (uint256)',
  'function totalRaised() view returns (uint256)',
  'function tokenPrice() view returns (uint256)',     // wei per token
  'function minContribution() view returns (uint256)',
  'function maxContribution() view returns (uint256)',
  'function contributions(address) view returns (uint256)',
  'function tokenAllocation(address) view returns (uint256)',
  'function claimed(address) view returns (bool)',
  'function contribute(bytes32[] proof) payable',
  'function contributePublic() payable',
  'function claim() nonpayable',
  'function refund() nonpayable',
]);

export async function getPresaleState(
  contractAddress: `0x${string}`,
  userAddress?: `0x${string}`,
) {
  const client = createPublicClient({ chain: mainnet, transport: http() });

  const base = await client.multicall({
    contracts: [
      { address: contractAddress, abi: PRESALE_ABI, functionName: 'salePhase' },
      { address: contractAddress, abi: PRESALE_ABI, functionName: 'startTime' },
      { address: contractAddress, abi: PRESALE_ABI, functionName: 'endTime' },
      { address: contractAddress, abi: PRESALE_ABI, functionName: 'claimStartTime' },
      { address: contractAddress, abi: PRESALE_ABI, functionName: 'hardCap' },
      { address: contractAddress, abi: PRESALE_ABI, functionName: 'softCap' },
      { address: contractAddress, abi: PRESALE_ABI, functionName: 'totalRaised' },
      { address: contractAddress, abi: PRESALE_ABI, functionName: 'tokenPrice' },
      { address: contractAddress, abi: PRESALE_ABI, functionName: 'minContribution' },
      { address: contractAddress, abi: PRESALE_ABI, functionName: 'maxContribution' },
    ],
  });

  const userCalls = userAddress ? await client.multicall({
    contracts: [
      { address: contractAddress, abi: PRESALE_ABI, functionName: 'contributions', args: [userAddress] },
      { address: contractAddress, abi: PRESALE_ABI, functionName: 'tokenAllocation', args: [userAddress] },
      { address: contractAddress, abi: PRESALE_ABI, functionName: 'claimed', args: [userAddress] },
    ],
  }) : null;

  return {
    phase: base[0].result as number,
    startTime: Number(base[1].result as bigint),
    endTime: Number(base[2].result as bigint),
    claimStartTime: Number(base[3].result as bigint),
    hardCap: base[4].result as bigint,
    softCap: base[5].result as bigint,
    totalRaised: base[6].result as bigint,
    tokenPrice: base[7].result as bigint,
    minContribution: base[8].result as bigint,
    maxContribution: base[9].result as bigint,
    userContribution: userCalls?.[0].result as bigint ?? 0n,
    userAllocation: userCalls?.[1].result as bigint ?? 0n,
    userClaimed: userCalls?.[2].result as boolean ?? false,
  };
}

Таймер обратного отсчёта

// components/CountdownTimer.tsx
import { useEffect, useState } from 'react';

interface TimeLeft {
  days: number;
  hours: number;
  minutes: number;
  seconds: number;
}

function calcTimeLeft(targetTs: number): TimeLeft {
  const diff = Math.max(0, targetTs * 1000 - Date.now());
  return {
    days: Math.floor(diff / 86_400_000),
    hours: Math.floor((diff % 86_400_000) / 3_600_000),
    minutes: Math.floor((diff % 3_600_000) / 60_000),
    seconds: Math.floor((diff % 60_000) / 1_000),
  };
}

export function CountdownTimer({ targetTs, label }: { targetTs: number; label: string }) {
  const [timeLeft, setTimeLeft] = useState<TimeLeft>(calcTimeLeft(targetTs));

  useEffect(() => {
    const interval = setInterval(() => setTimeLeft(calcTimeLeft(targetTs)), 1000);
    return () => clearInterval(interval);
  }, [targetTs]);

  return (
    <div className="text-center">
      <p className="mb-3 text-sm text-neutral-400">{label}</p>
      <div className="flex items-center gap-3">
        {[
          { value: timeLeft.days, label: 'дней' },
          { value: timeLeft.hours, label: 'часов' },
          { value: timeLeft.minutes, label: 'минут' },
          { value: timeLeft.seconds, label: 'секунд' },
        ].map(({ value, label }) => (
          <div key={label} className="min-w-[60px] rounded-xl bg-neutral-800 p-3 text-center">
            <span className="block text-2xl font-bold tabular-nums">
              {String(value).padStart(2, '0')}
            </span>
            <span className="text-xs text-neutral-500">{label}</span>
          </div>
        ))}
      </div>
    </div>
  );
}

Калькулятор распределения токенов

// components/ContributionCalculator.tsx
import { formatEther, formatUnits, parseEther } from 'viem';

interface Props {
  tokenPrice: bigint;   // wei per token
  minContrib: bigint;
  maxContrib: bigint;
  userContrib: bigint;
  tokenDecimals?: number;
}

export function ContributionCalculator({
  tokenPrice, minContrib, maxContrib, userContrib, tokenDecimals = 18,
}: Props) {
  const [ethAmount, setEthAmount] = useState('');

  const ethWei = ethAmount ? parseEther(ethAmount) : 0n;
  const tokensReceived = tokenPrice > 0n ? (ethWei * BigInt(10 ** tokenDecimals)) / tokenPrice : 0n;
  const remaining = maxContrib - userContrib;
  const canContribute = ethWei >= minContrib && ethWei <= remaining;

  return (
    <div className="space-y-4">
      <div>
        <label className="mb-1 block text-sm text-neutral-400">Сумма взноса (ETH)</label>
        <input
          type="number"
          step="0.01"
          min={formatEther(minContrib)}
          max={formatEther(remaining)}
          value={ethAmount}
          onChange={e => setEthAmount(e.target.value)}
          className="w-full rounded-lg border border-white/10 bg-neutral-800 px-4 py-2.5"
        />
        <div className="mt-1 flex justify-between text-xs text-neutral-500">
          <span>Мин: {formatEther(minContrib)} ETH</span>
          <span>Осталось: {formatEther(remaining)} ETH</span>
        </div>
      </div>

      <div className="rounded-lg bg-neutral-800/50 p-4">
        <div className="flex justify-between text-sm">
          <span className="text-neutral-400">Получите токенов:</span>
          <span className="font-semibold">
            {formatUnits(tokensReceived, tokenDecimals)} TOKEN
          </span>
        </div>
        <div className="mt-2 flex justify-between text-sm">
          <span className="text-neutral-400">Уже внесено:</span>
          <span>{formatEther(userContrib)} ETH</span>
        </div>
      </div>

      <ContributeButton disabled={!canContribute} ethAmount={ethWei} />
    </div>
  );
}

Транзакция взноса с Merkle proof

// hooks/useContribute.ts
import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi';
import { getMerkleProof, isWhitelisted } from '@/lib/merkle';

export function useContribute(contractAddress: `0x${string}`, phase: number) {
  const { address } = useAccount();
  const { writeContract, data: txHash, isPending } = useWriteContract();
  const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash: txHash });

  const contribute = async (ethValue: bigint) => {
    if (!address) return;

    if (phase === 1) {
      // Whitelist round — нужен proof
      if (!isWhitelisted(address)) {
        throw new Error('Адрес не в whitelist');
      }
      const proof = getMerkleProof(address);
      writeContract({
        address: contractAddress,
        abi: PRESALE_ABI,
        functionName: 'contribute',
        args: [proof],
        value: ethValue,
      });
    } else {
      writeContract({
        address: contractAddress,
        abi: PRESALE_ABI,
        functionName: 'contributePublic',
        value: ethValue,
      });
    }
  };

  return { contribute, txHash, isPending, isConfirming, isSuccess };
}

Процент заполнения и прогресс-бар

function FundingProgress({ raised, hardCap, softCap }: { raised: bigint; hardCap: bigint; softCap: bigint }) {
  const raisedEth = parseFloat(formatEther(raised));
  const hardCapEth = parseFloat(formatEther(hardCap));
  const softCapEth = parseFloat(formatEther(softCap));
  const progress = (raisedEth / hardCapEth) * 100;
  const softCapPercent = (softCapEth / hardCapEth) * 100;

  return (
    <div>
      <div className="mb-2 flex justify-between text-sm">
        <span>{raisedEth.toFixed(2)} ETH собрано</span>
        <span>{progress.toFixed(1)}%</span>
      </div>
      <div className="relative h-3 overflow-hidden rounded-full bg-neutral-800">
        <div
          className="h-full rounded-full bg-gradient-to-r from-blue-600 to-violet-600 transition-all duration-500"
          style={{ width: `${Math.min(progress, 100)}%` }}
        />
        {/* Маркер soft cap */}
        <div
          className="absolute top-0 h-full w-0.5 bg-yellow-400"
          style={{ left: `${softCapPercent}%` }}
        />
      </div>
      <div className="mt-1 flex justify-between text-xs text-neutral-500">
        <span>Soft cap: {softCapEth} ETH</span>
        <span>Hard cap: {hardCapEth} ETH</span>
      </div>
    </div>
  );
}

Сроки: UI с одним раундом (публичная продажа), таймером и прогрессом — 4–5 дней. Полный лаunchpad с whitelist-раундом через merkle tree, двухфазной продажей, клеймингом и refund-механизмом — 10–14 дней.