Token Launchpad (IDO/ICO) on Website

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1215
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1043
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    815

Implementing Token Launchpad (IDO/ICO) on a Website

Token Launchpad is an interface for public token sales. Users come, deposit ETH/USDC, and receive token allocation. Under the hood: presale smart contract, UI with timer, collection progress, personal limits, whitelist mechanism, and claiming after TGE.

This is a complex frontend task: many states (before start, active sale, between rounds, claiming), different scenarios for whitelist and public participants, critical precision in token calculations.

Launchpad Phases

Upcoming → Whitelist Round → Public Round → Ended → Claiming

Each phase has its own UI state, available actions, and smart contract logic.

Reading Presale Contract State

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

Countdown Timer

// 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: 'days' },
          { value: timeLeft.hours, label: 'hours' },
          { value: timeLeft.minutes, label: 'minutes' },
          { value: timeLeft.seconds, label: 'seconds' },
        ].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>
  );
}

Token Allocation Calculator

// 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">Contribution Amount (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>Min: {formatEther(minContrib)} ETH</span>
          <span>Remaining: {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">You will receive:</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">Already contributed:</span>
          <span>{formatEther(userContrib)} ETH</span>
        </div>
      </div>

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

Contribution Transaction with 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 — need proof
      if (!isWhitelisted(address)) {
        throw new Error('Address not in 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 };
}

Funding Progress and Progress Bar

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 raised</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 marker */}
        <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>
  );
}

Timeframe: UI with one round (public sale), timer and progress — 4–5 days. Full launchpad with whitelist round via merkle tree, two-phase sale, claiming and refund mechanism — 10–14 days.