Token Sale Dashboard Development

We design and develop full-cycle blockchain solutions: from smart contract architecture to launching DeFi protocols, NFT marketplaces and crypto exchanges. Security audits, tokenomics, integration with existing infrastructure.
Showing 1 of 1All 1306 services
Token Sale Dashboard Development
Medium
~3-5 days
Frequently Asked Questions

Blockchain Development Services

Blockchain Development Stages

Latest works

  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1198
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1122
  • image_logo-advance_0.webp
    B2B Advance company logo design
    589
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    859
  • image_logo-aider_0.webp
    AIDER company logo development
    788
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    906

Token Sale Dashboard Development

Token sale dashboard is not a landing page with "buy" button. It's interface that must work flawlessly exactly when load peaks: first hours after sales open. Tens of thousands users simultaneously connect wallets, check whitelist, approve tokens, send transactions—and wait. Any UI/UX error at this moment costs lost sales and reputation damage.

Smart Contract: What to Understand

Dashboard is frontend to sale contract. Understand contract fully, not partially. Typical sale contract includes:

Sale parameters:

struct SaleConfig {
    uint256 startTime;
    uint256 endTime;
    uint256 tokenPrice;         // price in USD/ETH per 1 token
    uint256 minPurchase;        // min buy per wallet
    uint256 maxPurchase;        // max per wallet
    uint256 hardCap;            // total hardcap
    uint256 softCap;            // if not reached—refund
    address paymentToken;       // USDC/USDT/ETH (address(0) for ETH)
    bool whitelistEnabled;
    bytes32 merkleRoot;         // for whitelist verification
}

Key events:

event TokensPurchased(address indexed buyer, uint256 paymentAmount, uint256 tokenAmount);
event SaleStarted(uint256 startTime, uint256 endTime);
event HardCapReached(uint256 totalRaised);
event RefundClaimed(address indexed buyer, uint256 amount);

Dashboard must listen to events real-time and update UI without page reload.

Dashboard Architecture

Sale State: State Machine

Sale contract goes through states. UI displays each correctly:

Not Started → Active → Ended (Success) → Distribution
                     → Ended (Failed) → Refund Available

For WhitelistOnly:
Whitelist Round → Public Round → Ended
type SalePhase =
  | 'not_started'
  | 'whitelist_only'
  | 'public'
  | 'ended_success'
  | 'ended_failed'
  | 'distribution'
  | 'refund_available';

function getSalePhase(
  startTime: bigint,
  endTime: bigint,
  totalRaised: bigint,
  softCap: bigint,
  hardCap: bigint,
  now: bigint
): SalePhase {
  if (now < startTime) return 'not_started';
  if (now >= startTime && now < endTime && totalRaised < hardCap) return 'public';
  if (now >= endTime && totalRaised >= softCap) return 'ended_success';
  if (now >= endTime && totalRaised < softCap) return 'ended_failed';
  return 'ended_failed';
}

Whitelist Verification via Merkle Proof

Merkle tree verifies address is in whitelist without storing full list on-chain:

import { MerkleTree } from 'merkletreejs';
import { keccak256 } from 'ethers';

function buildMerkleTree(whitelist: string[]): MerkleTree {
  const leaves = whitelist.map(addr =>
    keccak256(Buffer.from(addr.toLowerCase().slice(2), 'hex'))
  );
  return new MerkleTree(leaves, keccak256, { sortPairs: true });
}

function getMerkleProof(tree: MerkleTree, address: string): string[] {
  const leaf = keccak256(Buffer.from(address.toLowerCase().slice(2), 'hex'));
  return tree.getHexProof(leaf);
}

async function checkWhitelistStatus(address: string): Promise<{
  isWhitelisted: boolean;
  proof: string[];
}> {
  const proof = getMerkleProof(merkleTree, address);
  const isWhitelisted = merkleTree.verify(
    proof,
    keccak256(Buffer.from(address.toLowerCase().slice(2), 'hex')),
    merkleTree.getRoot()
  );
  return { isWhitelisted, proof };
}

Real-Time Data: Polling vs Events

Event subscription via WebSocket — optimal for real-time progress:

const provider = new ethers.WebSocketProvider(WS_RPC_URL);
const saleContract = new ethers.Contract(SALE_ADDRESS, SALE_ABI, provider);

saleContract.on('TokensPurchased', (buyer, paymentAmount, tokenAmount, event) => {
  setTotalRaised(prev => prev + paymentAmount);
  setParticipantCount(prev => prev + 1);

  if (buyer.toLowerCase() === userAddress.toLowerCase()) {
    setUserAllocation(prev => prev + tokenAmount);
  }
});

Polling as fallback — WebSocket unstable. Reserve polling every 15 seconds for critical data.

Optimize RPC calls via Multicall:

const multicall = new Contract(MULTICALL_ADDRESS, MULTICALL_ABI, provider);
const [totalRaised, hardCap, userPurchased, saleEnded] =
  await multicall.aggregate([
    { target: SALE_ADDRESS, callData: iface.encodeFunctionData('totalRaised') },
    { target: SALE_ADDRESS, callData: iface.encodeFunctionData('hardCap') },
    { target: SALE_ADDRESS, callData: iface.encodeFunctionData('purchased', [userAddress]) },
    { target: SALE_ADDRESS, callData: iface.encodeFunctionData('saleEnded') },
  ]);

Purchase: UX Flow

Multi-Currency Payments

Most sales accept multiple tokens. Check approval for each:

async function handlePurchase(
  paymentToken: string,  // USDC/USDT/address(0) for ETH
  paymentAmount: bigint,
  proof: string[]
) {
  if (paymentToken !== ethers.ZeroAddress) {
    const allowance = await erc20.allowance(userAddress, SALE_ADDRESS);
    if (allowance < paymentAmount) {
      setStep('approving');
      const approveTx = await erc20.approve(SALE_ADDRESS, paymentAmount);
      await approveTx.wait();
    }
  }

  // Simulate before sending
  setStep('simulating');
  try {
    await saleContract.buy.staticCall(paymentAmount, proof, { value: ethValue });
  } catch (err) {
    setError(parseContractError(err));
    return;
  }

  // Send transaction
  setStep('confirming');
  const tx = await saleContract.buy(paymentAmount, proof, { value: ethValue });
  setStep('waiting');
  const receipt = await tx.wait();
  setStep('success');
}

Transaction simulation before send (staticCall) is mandatory. Shows expected result, catches errors without gas waste.

Gas Estimation and EIP-1559

async function estimateGasWithBuffer(tx: ContractTransaction) {
  const estimated = await provider.estimateGas(tx);
  return (estimated * 120n) / 100n;  // +20% buffer
}

async function getFeeData() {
  const feeData = await provider.getFeeData();
  return {
    maxFeePerGas: (feeData.maxFeePerGas! * 120n) / 100n,
    maxPriorityFeePerGas: feeData.maxPriorityFeePerGas!
  };
}

UI Components

Progress Bar

Hardcap progress—central dashboard element. Must update real-time without flicker:

function SaleProgress({ totalRaised, hardCap, softCap }: SaleProgressProps) {
  const progress = Number((totalRaised * 100n) / hardCap);
  const softCapProgress = Number((softCap * 100n) / hardCap);

  return (
    <div className="sale-progress">
      <div className="progress-bar-container">
        <div
          className="progress-fill"
          style={{ width: `${Math.min(progress, 100)}%` }}
        />
        <div
          className="softcap-marker"
          style={{ left: `${softCapProgress}%` }}
          title={`Soft Cap: ${formatUSD(softCap)}`}
        />
      </div>
      <div className="progress-labels">
        <span>{formatUSD(totalRaised)} raised</span>
        <span>{progress.toFixed(1)}%</span>
        <span>Hard Cap: {formatUSD(hardCap)}</span>
      </div>
    </div>
  );
}

Countdown Timer

Sync with blockchain time, not system clock:

function useBlockchainCountdown(targetTimestamp: bigint) {
  const [timeLeft, setTimeLeft] = useState<number>(0);
  const { data: blockNumber } = useBlockNumber({ watch: true });

  useEffect(() => {
    const now = BigInt(Math.floor(Date.now() / 1000));
    const diff = Number(targetTimestamp - now);
    setTimeLeft(Math.max(0, diff));
  }, [blockNumber, targetTimestamp]); // update on new block

  return timeLeft;
}

Performance at Peak Load

First sales minutes—maximum RPC and frontend load. Preparation:

  • RPC rate limits: public nodes fail. Need Alchemy/QuickNode enterprise or own node
  • CDN for static: JS bundle, images via Cloudflare
  • Optimistic UI: show assumed status before confirmation
  • Queue visualization: if hot—show transaction queue, pending count
  • Caching: static data (tokenomics, allocation table)—cache, don't request every time