Перейти к содержанию

BOLT11 в Lightning: платёжный счёт

Оглавление курса «Bitcoin и Lightning: платежи на сайте» · Урок 22 из 30.

Условный платёж должен быть связан с конкретным заказом сайта. Для нашей библиотеки нужен счёт на курс 17 стоимостью 30 000 сатоши, то есть 30 000 000 миллисатоши. В этом уроке рассмотрим Lightning invoice формата BOLT 11 и составим серверную проверку полученного инструмента. Результатом станет запись, которой можно доверять в пределах выбранного адаптера, не пытаясь самостоятельно написать кодек длинной платёжной строки.

Счета, кошельки и SDK здесь не запускались. В листингах нет действительного BOLT 11 для оплаты. Строки с явными заполнителями служат описанием формы, а не рабочим QR-кодом. Для реализации потребуется выбрать версию узла и библиотеку декодирования, затем проверить их договор по первичной документации.

Какие данные важны сайту

BOLT 11 представляет платёжный запрос, включая сеть, сумму, payment hash, временные поля и подпись. Сумма может отсутствовать у некоторых типов счетов; для нашего фиксированного заказа такой вариант не допускается. Детали формата описаны в официальной спецификации BOLT 11.

Наша задача не состоит в ручном извлечении числа из начала строки. У формата есть единицы, структура и требования проверки. Случайный регулярный шаблон, который находит lnbc и несколько цифр, не удостоверяет ни подпись, ни сеть, ни нужный payment hash. Декодер принадлежит выбранной реализации, а сервер проверяет его нормализованный результат.

Платёжная строка сама по себе не указывает получателя учебного доступа. Даже если описание содержит «курс 17», сервер должен иметь собственную связь orderId → paymentHash. Текст описания полезен человеку, но не является уникальным ключом базы. Несколько заказов на один курс могут иметь одинаковое описание и сумму.

Нормализованный счёт

Определим объект нашего адаптера, в котором сумма уже выражена в msat. Временные значения передаются как секунды Unix. Поле network использует собственное нормализованное имя regtest, а не пытается угадать цепочку по адресам другого способа оплаты.

from dataclasses import dataclass

@dataclass(frozen=True)
class DecodedTeachingInvoice:
    network: str
    amount_msat: int | None
    payment_hash_hex: str
    created_at: int
    expiry_seconds: int
    signature_valid: bool
    payment_request: str


def validate_course_invoice(invoice, order, now):
    if not invoice.signature_valid:
        raise ValueError('Подпись счёта не проверена адаптером')
    if invoice.network != 'regtest':
        raise ValueError('Другая сеть')
    if invoice.amount_msat != order['amount_msat']:
        raise ValueError('Другая или неопределённая сумма')
    if len(invoice.payment_hash_hex) != 64:
        raise ValueError('Неверный размер payment hash')
    bytes.fromhex(invoice.payment_hash_hex)
    if invoice.expiry_seconds <= 0:
        raise ValueError('Неверный срок')
    if now >= invoice.created_at + invoice.expiry_seconds:
        raise ValueError('Счёт уже истёк')
    return invoice

Флаг signature_valid является результатом доверенного декодера, а не полем, которое клиент присылает в HTTP-запросе. Сам dataclass не делает данные достоверными. Адаптер также проверяет обязательные элементы и поддерживаемые возможности выбранного формата. Функция отвечает за предметные условия курса, не пытаясь заменить протокольный парсер.

В этой модели order содержит серверную цену. Посетитель не может заменить amount_msat на единицу и получить новый счёт со скидкой. Выбор courseId проходит собственный контроль каталога и доступности услуги. Если цена изменилась, уже созданный заказ сохраняет свою зафиксированную политику либо получает явную процедуру отмены, а не молча подстраивается под новое значение.

Создание и сохранение

Сервер просит выбранный адаптер создать invoice и получает payment request вместе с идентификатором. Затем проверяет возвращённые поля и атомарно сохраняет связь. В документации LND существует AddInvoice, но мы не выдаём собственные аргументы за готовый вызов установленного клиента. Официальный справочник AddInvoice.

У записи сайта есть orderId, instrumentId, method равный lightning, payment hash, amount_msat, expiresAt и идентификатор профиля источника. payment hash нормализуем в одном представлении, например 64 символа lowercase hex. Если API отдаёт bytes в base64, преобразование выполняет адаптер, а не браузерная страница.

Потеря ответа во время создания счёта требует отдельного восстановления. Если узел успел создать инструмент, повторный вызов без договорённой идемпотентности может создать другой счёт. Поэтому запрос подготовки связывается с серверной задачей, а неопределённая попытка не получает автоматически новый payment hash. Способ поиска результата фиксируется для выбранной реализации.

Срок счёта и состояние заказа

Срок invoice ограничивает возможность оплаты этого инструмента. Срок заказа и срок выдаваемого доступа определяются приложением. Они не обязаны совпадать. Например, платёж может быть подтверждён до окончания счёта, а право чтения продолжаться дольше. Или интерфейс заказа уже завершил ожидание, но сервер ещё должен сверить позднее наблюдение.

Браузер показывает обратный отсчёт для удобства, однако его часы не решают состояние платежа. Сервер использует свой контекст и ответ получателя. Когда отображаемый срок истёк, кнопка оплаты прекращает предлагать старый инструмент, но сервер не удаляет историю и не объявляет любое последующее сообщение ложным.

Не объединяем BOLT 11 с обычным Bitcoin URI автоматически. Современный on-chain формат и поддержка кошелька рассматриваются в предыдущих уроках отдельно. Если продукт позднее предложит единый QR с альтернативами, ему понадобится согласованная модель двух инструментов и обработка возможного двойного поступления. В этой главе у заказа один выбранный Lightning invoice.

Ожидаемые примеры

Счёт нужной сети, на 30 000 000 msat, с проверенной подписью и действующим сроком допускается к сохранению. Счёт без суммы отклоняется для нашего фиксированного курса. Счёт другой сети отклоняется, даже если описание правильно. Истёкший запрос не показывается как действующий способ оплаты.

Правильный invoice ещё не означает settlement. После его создания статус заказа остаётся ожидающим оплату. Читатель получает понятную сумму, метод и срок; сервер — уникальную связь с payment hash. В следующем уроке включим этот инструмент в общий адаптер, сохранив различие Lightning и on-chain подтверждений.

Описание и личные данные

В назначении счёта достаточно нейтрального «учебная библиотека, курс 17». Email читателя, пароль кабинета или его полный профиль не нужны платёжной строке. Серверная база сохраняет связь с пользователем под собственным контролем доступа. Даже если декодер показывает описание, интерфейс выводит его безопасно как текст, а не вставляет в HTML.

Если поставщик использует хеш описания вместо самого описания, адаптер фиксирует соответствующий договор и проверяет нужную связь. Это не даёт серверу права заменять цену по произвольному внешнему тексту. Цена остаётся полем заказа.

В поддержке удобнее просить безопасный идентификатор заказа, чем заставлять читателя публиковать всю строку invoice на форуме. Ограниченное раскрытие помогает найти инструмент без ненужной передачи платёжной истории. Такая политика относится к продукту независимо от конкретного кодека.

Оглавление серии