Розробка системи підтвердження платежів

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

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

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

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1307
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1219
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    920
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1148
  • image_logo-advance_0.webp
    Розробка логотипу компанії B2B Advance
    611
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    886

Розробка системи підтвердження платежів

Наївна реалізація перевірки крипто-платежів виглядає так: отримали транзакцію → перевірили суму → засчитали. Ця схема ломується при першому ж reorg, подвійній трaті або коли користувач надсилає платіж спустя годину після закінчення сеансу. Надійна система підтвердження — це скінченний автомат з явними переходами між станами та захистом від усіх граничних випадків.

Модель станів платежу

Кожен платіж проходить через строго визначені стани:

PENDING → DETECTED → CONFIRMING → CONFIRMED → SETTLED
              ↓                     ↓
          EXPIRED              UNDERPAID / OVERPAID
                                    ↓
                                REFUNDED
Стан Опис
PENDING Адреса видана, чекаємо транзакцію
DETECTED Транзакція в mempool (0 підтвердження)
CONFIRMING 1+ підтверджень, ще не фіналізована
CONFIRMED Досягнутий поріг підтвердження, сума коректна
SETTLED Бізнес-логіка виконана (замовлення створено, підписка активована)
EXPIRED Таймер вигас, транзакція не прийшла
UNDERPAID Транзакція є, але сума менша ніж очікувалась

Схема БД для платежу:

CREATE TABLE payments (
  id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  order_id      UUID NOT NULL REFERENCES orders(id),
  address       VARCHAR(64) NOT NULL UNIQUE,
  network       VARCHAR(32) NOT NULL,  -- 'ethereum', 'bsc', 'tron'
  token         VARCHAR(32) NOT NULL,  -- 'USDT', 'ETH', 'TRX'
  expected_amount NUMERIC(36, 18) NOT NULL,
  received_amount NUMERIC(36, 18) DEFAULT 0,
  status        VARCHAR(20) NOT NULL DEFAULT 'PENDING',
  tx_hash       VARCHAR(128),
  confirmations INTEGER DEFAULT 0,
  required_confirmations INTEGER NOT NULL,
  expires_at    TIMESTAMPTZ NOT NULL,
  confirmed_at  TIMESTAMPTZ,
  created_at    TIMESTAMPTZ NOT NULL DEFAULT now(),

  CONSTRAINT valid_status CHECK (
    status IN ('PENDING','DETECTED','CONFIRMING','CONFIRMED','SETTLED','EXPIRED','UNDERPAID','OVERPAID','REFUNDED')
  )
);

CREATE INDEX idx_payments_address ON payments(address) WHERE status IN ('PENDING', 'DETECTED', 'CONFIRMING');
CREATE INDEX idx_payments_expires_at ON payments(expires_at) WHERE status = 'PENDING';

Моніторинг блокчейна: архітектура

Монолітний моніторинг усіх мереж в одному процесі — погана ідея. Краще: окремий worker на кожну мережу з незалежним retry-механізмом:

// Абстрактний інтерфейс монітора
interface ChainMonitor {
  network: string;
  start(): Promise<void>;
  stop(): void;
  onTransaction(handler: (tx: IncomingTransaction) => Promise<void>): void;
}

// Реалізація для EVM-мереж
class EvmChainMonitor implements ChainMonitor {
  private provider: ethers.JsonRpcProvider;
  private watchedAddresses = new Set<string>();

  async start() {
    // Завантажуємо активні адреси з БД
    const activePayments = await db.query(
      "SELECT address FROM payments WHERE status IN ('PENDING', 'DETECTING', 'CONFIRMING')"
    );
    activePayments.rows.forEach(p => this.watchedAddresses.add(p.address));

    // Підписка на нові блоки
    this.provider.on('block', async (blockNumber) => {
      await this.processBlock(blockNumber);
    });
  }

  private async processBlock(blockNumber: number) {
    const block = await this.provider.getBlock(blockNumber, true);

    for (const tx of block.transactions) {
      // Нативний ETH
      if (tx.to && this.watchedAddresses.has(tx.to.toLowerCase())) {
        await this.handleNativeTransfer(tx, blockNumber);
      }
    }

    // ERC-20 Transfer events
    await this.scanErc20Transfers(blockNumber);
  }
}

Обробка reorg'ів

Reorg — реорганізація цепи, коли прийнятий блок замінюється іншим. На Ethereum ймовірність низька (PoS, 1–2 блоки), на Polygon — вища. Правильний захист:

async function processConfirmations(paymentId: string) {
  const payment = await db.findPayment(paymentId);
  const currentBlock = await provider.getBlockNumber();

  // Отримуємо свіжий receipt
  const receipt = await provider.getTransactionReceipt(payment.txHash);

  if (!receipt) {
    // Транзакція пропала з цепи — reorg або dropped
    await db.updatePayment(paymentId, {
      status: 'DETECTED',  // откат до попереднього стану
      confirmations: 0,
      reorgDetected: true,
    });
    return;
  }

  const confirmations = currentBlock - receipt.blockNumber + 1;
  const isConfirmed = confirmations >= payment.requiredConfirmations;

  await db.updatePayment(paymentId, {
    confirmations,
    status: isConfirmed ? 'CONFIRMED' : 'CONFIRMING',
    confirmedAt: isConfirmed ? new Date() : null,
  });
}

Перевіряйте receipt заново кожні N блоків для CONFIRMING-платежів — не довіряйте старим даним.

Tolerance-вікно та курсова погрішність

Користувач платить "47.50 USDT", але після конвертації в wei та назад може вийти 47.499999... через floating point. Плюс: обмінники часто беруть комісію з суми переводу. Розумне tolerance-вікно:

function isAmountSufficient(
  received: bigint,    // у мінімальних одиницях (wei, sun)
  expected: bigint,
  toleranceBps: number = 50  // 0,5% за умовчанням
): 'exact' | 'underpaid' | 'overpaid' {
  const tolerance = expected * BigInt(toleranceBps) / 10000n;
  const min = expected - tolerance;
  const max = expected + expected / 10n;  // overpaid — до 10%

  if (received >= min && received <= max) return 'exact';
  if (received < min) return 'underpaid';
  return 'overpaid';
}

Ідемпотентність та захист від дублів

Один txHash повинен засчитуватися ровно один раз:

INSERT INTO payment_transactions (payment_id, tx_hash, amount, block_number)
VALUES ($1, $2, $3, $4)
ON CONFLICT (tx_hash) DO NOTHING
RETURNING id;

Якщо RETURNING повернув порожній результат — транзакція вже оброблена, пропускаємо.

Сповіщення та webhook'и

Після переходу в CONFIRMED — негайне сповіщення у систему:

async function dispatchPaymentConfirmed(payment: Payment) {
  // Внутрішній event bus
  await eventBus.emit('payment.confirmed', {
    paymentId: payment.id,
    orderId: payment.orderId,
    amount: payment.receivedAmount,
    txHash: payment.txHash,
  });

  // Зовнішній webhook якщо налаштований
  if (payment.webhookUrl) {
    await webhookQueue.add('payment-webhook', {
      url: payment.webhookUrl,
      payload: { event: 'payment.confirmed', data: payment },
    }, {
      attempts: 5,
      backoff: { type: 'exponential', delay: 2000 },
    });
  }
}

Webhook'и — через чергу (Bull/BullMQ) з retry. Прямий HTTP-вклик у обробнику блока — це шлях до втрати подій при тимчасовій недоступності одержувача.