Разработка дашборда токенсейла

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

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

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

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

  • image_website-b2b-advance_0.webp
    Разработка сайта компании B2B ADVANCE
    1285
  • 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

Разработка дашборда токенсейла

Токенсейл дашборд — это не лендинг с кнопкой "купить". Это интерфейс, который должен работать безотказно именно тогда, когда нагрузка максимальна: первые часы после открытия продаж. В этот момент десятки тысяч пользователей одновременно подключают кошельки, проверяют whitelist, одобряют токены, отправляют транзакции — и ждут. Любая ошибка в UI/UX в этот момент стоит потерянными продажами и репутационным ущербом.

Смарт-контракт: что должен делать и что проверять

Дашборд — это frontend к sale контракту. Понимать контракт нужно полностью, не частично. Типичный sale контракт включает:

Sale параметры:

struct SaleConfig {
    uint256 startTime;
    uint256 endTime;
    uint256 tokenPrice;         // цена в USD/ETH за 1 токен
    uint256 minPurchase;        // минимальная покупка
    uint256 maxPurchase;        // максимум на адрес
    uint256 hardCap;            // общий hardcap
    uint256 softCap;            // если не достигнут — refund
    address paymentToken;       // USDC/USDT/ETH (address(0) для ETH)
    bool whitelistEnabled;
    bytes32 merkleRoot;         // root для whitelist верификации
}

Ключевые события для frontend:

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

Дашборд должен слушать эти события реально-временно и обновлять UI без перезагрузки страницы.

Архитектура дашборда

Состояние продажи: state machine

Sale контракт проходит через состояния. UI должен корректно отображать каждое:

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

Для WhitelistOnly sale добавляется:

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,
  whitelistEndTime: bigint,
  totalRaised: bigint,
  softCap: bigint,
  hardCap: bigint,
  now: bigint
): SalePhase {
  if (now < startTime) return 'not_started';
  if (now >= startTime && now < whitelistEndTime) return 'whitelist_only';
  if (now >= whitelistEndTime && now < endTime && totalRaised < hardCap) return 'public';
  if (now >= endTime && totalRaised >= softCap) return 'ended_success';
  if (now >= endTime && totalRaised < softCap) return 'ended_failed';
  if (totalRaised >= hardCap) return 'distribution'; // или ended_success
  return 'ended_failed';
}

Whitelist верификация через Merkle Proof

Merkle tree позволяет верифицировать on-chain что адрес в whitelist, не храня весь список on-chain:

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

// Генерация дерева (делается off-chain, root публикуется в контракт)
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 });
}

// Генерация proof для конкретного адреса
function getMerkleProof(tree: MerkleTree, address: string): string[] {
  const leaf = keccak256(Buffer.from(address.toLowerCase().slice(2), 'hex'));
  return tree.getHexProof(leaf);
}

// В UI: проверяем whitelist статус и готовим proof для транзакции
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 данные: polling vs events

Event subscription через WebSocket — оптимально для real-time progress bar:

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

  // Если это наш пользователь — обновляем его allocation
  if (buyer.toLowerCase() === userAddress.toLowerCase()) {
    setUserAllocation(prev => prev + tokenAmount);
  }
});

Polling как fallback — WebSocket соединения нестабильны. Резервный polling каждые 15 секунд для критических данных (totalRaised, hardCap progress).

Оптимизация RPC вызовов через Multicall:

// Один RPC вызов вместо пяти
const multicall = new Contract(MULTICALL_ADDRESS, MULTICALL_ABI, provider);
const [totalRaised, hardCap, userPurchased, isWhitelisted, 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('isWhitelisted', [userAddress]) },
    { target: SALE_ADDRESS, callData: iface.encodeFunctionData('saleEnded') },
  ]);

Покупка: UX flow

Мультивалютные платежи

Большинство sale принимают несколько токенов. Для каждого нужна approval проверка:

async function handlePurchase(
  paymentToken: string,  // USDC/USDT/address(0) для ETH
  paymentAmount: bigint,
  proof: string[]
) {
  if (paymentToken !== ethers.ZeroAddress) {
    // Проверяем allowance
    const allowance = await erc20.allowance(userAddress, SALE_ADDRESS);
    if (allowance < paymentAmount) {
      // Approval транзакция
      setStep('approving');
      const approveTx = await erc20.approve(SALE_ADDRESS, paymentAmount);
      await approveTx.wait();
    }
  }

  // Симуляция перед отправкой — показываем ожидаемый результат
  setStep('simulating');
  try {
    await saleContract.buy.staticCall(paymentAmount, proof, { value: ethValue });
  } catch (err) {
    setError(parseContractError(err));
    return;
  }

  // Отправка транзакции
  setStep('confirming');
  const tx = await saleContract.buy(paymentAmount, proof, { value: ethValue });
  setStep('waiting');
  const receipt = await tx.wait();
  setStep('success');
}

Transaction simulation перед отправкой (staticCall) — обязательна. Позволяет показать пользователю ожидаемый результат и поймать ошибки (cap exceeded, not whitelisted) без трат на gas.

Gas estimation и EIP-1559

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

// EIP-1559: рекомендуемые gas параметры
async function getFeeData() {
  const feeData = await provider.getFeeData();
  return {
    maxFeePerGas: (feeData.maxFeePerGas! * 120n) / 100n,
    maxPriorityFeePerGas: feeData.maxPriorityFeePerGas!
  };
}

UI компоненты

Progress Bar

Hardcap progress — центральный элемент интерфейса. Должен обновляться в реальном времени без мерцания:

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)}%` }}
        />
        {/* Маркер soft cap */}
        <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

Синхронизация с блокчейн временем, не с системными часами:

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]); // обновляем с каждым новым блоком

  return timeLeft;
}

Производительность при пиковой нагрузке

Первые минуты продаж — максимальная нагрузка на RPC и frontend. Подготовка:

  • RPC rate limits: публичные ноды упадут. Нужен Alchemy/QuickNode с enterprise лимитами или собственная нода
  • CDN для статики: JS bundle, изображения — через Cloudflare
  • Optimistic UI: показываем предполагаемый статус до подтверждения транзакции
  • Queue visualization: если продажи очень горячие — показываем очередь транзакций, pending count
  • Кэширование: static данные (tokenomics, allocation table) — кэшируем, не запрашиваем каждый раз