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

Проектируем и разрабатываем блокчейн-решения полного цикла: от архитектуры смарт-контрактов до запуска 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
    885

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

Наивная реализация проверки криптоплатежей выглядит так: получили транзакцию → проверили сумму → засчитали. Эта схема ломается при первом же reorg, двойной трате, или когда пользователь присылает платёж спустя час после истечения сессии. Надёжная система подтверждений — это конечный автомат с явными переходами между состояниями и защитой от всех граничных случаев.

Модель состояний платежа

Каждый платёж проходит через строго определённые состояния:

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-вызов в обработчике блока — это путь к потере событий при временной недоступности получателя.