Реалізація 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 учасників, критична точність у розрахунках токенів.

Фази launchpad

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 раунд — потрібен 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 днів. Повний launchpad з whitelist-раундом через merkle tree, двофазною продажею, клеймингом та refund-механізмом — 10–14 днів.