Разработка системы выставления счетов в криптовалюте

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

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

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

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

  • image_website-b2b-advance_0.webp
    Разработка сайта компании B2B ADVANCE
    1309
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1222
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    922
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1151
  • image_logo-advance_0.webp
    Разработка логотипа компании B2B Advance
    614
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    887

Разработка системы выставления счетов в криптовалюте

Разница между «принять крипто-платёж» и «выставить счёт в криптовалюте» — принципиальная. Счёт — это юридический документ с зафиксированной суммой, сроком оплаты, идентификатором контрагента и возможностью сверки. Большинство готовых решений останавливаются на первом — дают адрес для оплаты. Полноценный биллинг требует учёта, напоминаний, частичных оплат, мультивалютности и интеграции с бухгалтерией.

Жизненный цикл счёта

DRAFT → SENT → PENDING_PAYMENT → PARTIALLY_PAID → PAID | OVERDUE | CANCELLED

Каждый переход — событие с timestamp и данными транзакции. Для аудита статусы не перезаписываются, а добавляются новые записи.

interface Invoice {
    id: string              // UUID
    number: string          // читаемый: INV-2024-0042
    issuerId: string        // организация/кошелёк
    clientId: string
    clientWallet?: string   // если известен
    issuedAt: Date
    dueDate: Date
    lineItems: LineItem[]
    baseCurrency: string    // USD/EUR — в чём выставлен счёт
    subtotalFiat: Decimal
    taxAmountFiat: Decimal
    totalFiat: Decimal
    acceptedTokens: AcceptedToken[]  // в чём можно оплатить
    paymentAddress: string  // уникальный deposit address
    status: InvoiceStatus
    payments: InvoicePayment[]  // принятые частичные/полные оплаты
}

interface AcceptedToken {
    token: string           // contract address
    chain: string
    amountEquiv: Decimal    // сумма в токенах по текущему курсу
    rateLockedAt?: Date     // если курс зафиксирован
    rateLockExpiry?: Date   // до когда действует зафиксированный курс
}

Генерация уникальных payment addresses

Каждый счёт получает уникальный адрес для приёма платежей — это ключ к автоматическому matching входящих транзакций со счетами без ручного мемо/тега.

Деривация через HD wallet (BIP-32):

import { HDNodeWallet } from 'ethers'

class InvoiceAddressGenerator {
    private xpub: string  // master public key, never private key

    generateAddress(invoiceIndex: number): string {
        const node = HDNodeWallet.fromExtendedKey(this.xpub)
        // Путь деривации: m/0/{invoiceIndex}
        return node.deriveChild(0).deriveChild(invoiceIndex).address
    }

    async createInvoiceAddress(invoiceId: string): Promise<string> {
        // Атомарно получить следующий индекс
        const index = await this.db.transaction(async (trx) => {
            const result = await trx('address_counter')
                .increment('counter', 1)
                .returning('counter')
            return result[0].counter
        })

        const address = this.generateAddress(index)

        await this.db('invoice_addresses').insert({
            invoice_id: invoiceId,
            address,
            derivation_index: index,
        })

        return address
    }
}

Один address на один счёт позволяет автоматически сматчить входящие транзакции через мониторинг адресов (Alchemy Notify, Moralis Streams или собственный event listener).

Мониторинг входящих платежей

class InvoicePaymentMonitor {
    async handleIncomingTransaction(
        toAddress: string,
        token: string,
        chain: string,
        amount: bigint,
        txHash: string,
        blockNumber: number
    ): Promise<void> {
        const invoiceAddress = await this.db('invoice_addresses')
            .where({ address: toAddress.toLowerCase() })
            .first()

        if (!invoiceAddress) return  // не наш адрес

        const invoice = await this.getInvoice(invoiceAddress.invoice_id)

        if (!['sent', 'pending_payment', 'partially_paid'].includes(invoice.status)) {
            // Счёт уже оплачен или отменён — алерт для ручной обработки
            await this.alertUnexpectedPayment(invoice, txHash, amount)
            return
        }

        // Ждём confirmations перед кредитованием
        await this.pendingPayments.add({
            invoiceId: invoice.id,
            txHash,
            blockNumber,
            token,
            chain,
            amount,
        })
    }

    async processConfirmedPayment(pendingPayment: PendingPayment): Promise<void> {
        const invoice = await this.getInvoice(pendingPayment.invoiceId)
        const tokenPrice = await this.priceService.getHistoricalPrice(
            pendingPayment.token,
            pendingPayment.chain,
            pendingPayment.confirmedAt
        )

        const fiatEquivalent = new Decimal(pendingPayment.amount.toString())
            .div(10 ** TOKEN_DECIMALS)
            .mul(tokenPrice)

        await this.db.transaction(async (trx) => {
            await trx('invoice_payments').insert({
                invoice_id: invoice.id,
                tx_hash: pendingPayment.txHash,
                token: pendingPayment.token,
                chain: pendingPayment.chain,
                crypto_amount: pendingPayment.amount.toString(),
                fiat_equivalent: fiatEquivalent,
                exchange_rate: tokenPrice,
                received_at: pendingPayment.confirmedAt,
            })

            const totalPaid = await this.getTotalPaidFiat(invoice.id, trx)
            const newStatus = totalPaid.gte(invoice.total_fiat)
                ? 'paid'
                : 'partially_paid'

            await trx('invoices')
                .where({ id: invoice.id })
                .update({ status: newStatus, updated_at: new Date() })
        })

        await this.notifyPaymentReceived(invoice, fiatEquivalent)
    }
}

Rate locking и защита от волатильности

Для B2B-выставления счетов клиент может попросить зафиксировать курс на 1-24 часа. Это снижает неопределённость — клиент знает точно сколько USDC нужно перевести. Для продавца это риск если токен упадёт за время ожидания (актуально для volatile токенов, не для стейблкоинов).

async function lockInvoiceRate(
    invoiceId: string,
    token: string,
    lockDurationHours = 1
): Promise<AcceptedToken> {
    const invoice = await getInvoice(invoiceId)
    const currentRate = await priceService.getRate('USD', token)

    const tokenAmount = invoice.totalFiat.div(currentRate)
    const expiry = new Date(Date.now() + lockDurationHours * 3600 * 1000)

    await db('invoice_accepted_tokens')
        .where({ invoice_id: invoiceId, token })
        .update({
            amount_equiv: tokenAmount,
            rate_locked_at: new Date(),
            rate_lock_expiry: expiry,
            locked_rate: currentRate,
        })

    return { token, amountEquiv: tokenAmount, rateLockExpiry: expiry }
}

После истечения rateLockExpiry сумма пересчитывается по текущему курсу — клиент получает уведомление.

PDF-генерация и юридическая форма

Счёт должен выглядеть как счёт, а не как выписка из блокчейна. Генерация PDF с QR-кодом на payment address и суммой:

import PDFDocument from 'pdfkit'
import QRCode from 'qrcode'

async function generateInvoicePdf(invoice: Invoice): Promise<Buffer> {
    const doc = new PDFDocument({ margin: 50 })
    const buffers: Buffer[] = []

    doc.on('data', chunk => buffers.push(chunk))

    // Заголовок и реквизиты
    doc.fontSize(20).text(`Invoice ${invoice.number}`, 50, 50)
    doc.fontSize(10)
        .text(`Issued: ${format(invoice.issuedAt, 'dd MMM yyyy')}`)
        .text(`Due: ${format(invoice.dueDate, 'dd MMM yyyy')}`)

    // Таблица позиций, итоги...

    // QR-код с EIP-681 URI для удобной оплаты
    const paymentUri = `ethereum:${invoice.paymentAddress}?value=${invoice.totalFiat}`
    const qrBuffer = await QRCode.toBuffer(paymentUri, { width: 150 })
    doc.image(qrBuffer, 400, 650, { width: 100 })
    doc.fontSize(8).text('Scan to pay', 410, 755)

    doc.end()
    return new Promise(resolve =>
        doc.on('end', () => resolve(Buffer.concat(buffers)))
    )
}

Напоминания и автоматизация

Фоновый джоб для overdue счетов:

// Запускается каждый час
async function processOverdueInvoices(): Promise<void> {
    const overdue = await db('invoices')
        .where('status', 'in', ['sent', 'pending_payment', 'partially_paid'])
        .where('due_date', '<', new Date())

    for (const invoice of overdue) {
        const daysPastDue = differenceInDays(new Date(), invoice.due_date)

        if (daysPastDue === 1 || daysPastDue === 3 || daysPastDue === 7) {
            await emailService.sendOverdueReminder(invoice, daysPastDue)
        }

        if (invoice.status !== 'overdue') {
            await db('invoices').where({ id: invoice.id }).update({ status: 'overdue' })
        }
    }
}

Стек: Node.js/TypeScript backend, PostgreSQL, Bull/BullMQ для фоновых задач (мониторинг платежей, напоминания), React frontend для клиентского и внутреннего интерфейса. Интеграции: Alchemy Notify для webhook на входящие транзакции, SendGrid/Postmark для email. Срок разработки базовой системы с мультивалютным биллингом и автоматическим matching — 2–3 недели.