Адаптер Lightning для сайта
Оглавление курса «Bitcoin и Lightning: платежи на сайте» · Урок 23 из 30.
Теперь у заказа библиотеки есть проверенный Lightning invoice, но прежняя линия курса работает с обычными Bitcoin-транзакциями. Нам нужен единый интерфейс для сайта, который при этом не стирает различия способов оплаты. В уроке определим договор платёжного адаптера и безопасный публичный ответ. Результат позволит заменить поставщика или добавить другой метод, сохраняя предметные состояния заказа.
Примеры используют Python 3.12 и собственные типы. Они не импортируют установленный Lightning SDK и не выполняют сетевых запросов. Реальные методы LND или стороннего сервиса реализуются внутри отдельного адаптера по закреплённому профилю. Показанные функции описывают границу, а не выдаются за готовый платёжный сервер.
Общими остаются цель и сумма
Оба метода связаны с курсом 17 и ценой 30 000 сатоши. Сайт создаёт заказ, выбирает инструмент и наблюдает результат. Но on-chain линия отслеживает подходящие выходы транзакций и применяет собственную политику onchain-v1 с двумя подтверждениями. Lightning линия ожидает подтверждённый расчёт конкретного invoice от выбранного источника.
Не вводим искусственное поле confirmations равное двум для Lightning. Такое число создаёт ложную аналогию: оно не описывает его settlement. Вместо этого общий слой получает решение адаптера с названием метода, источником и объяснимыми свидетельствами. Читатель видит привычную карточку заказа, а внутренняя модель сохраняет различия.
Также нельзя использовать только status равный paid без происхождения. Администратор не сможет понять, кто и по какому договору сообщил результат. Если один поставщик недоступен, другая система может оставаться работоспособной. Явные поля помогают локализовать проблему, не обнуляя всю платёжную библиотеку.
Договор собственных типов
Запишем узкий интерфейс: создание инструмента и чтение его состояния. Отдельный процесс уведомлений позже использует тот же нормализованный формат, но не становится единственным источником восстановления.
from dataclasses import dataclass
from typing import Protocol
@dataclass(frozen=True)
class PaymentInstrument:
id: str
order_id: str
method: str
amount_msat: int
source_profile: str
reference: str
expires_at: int
@dataclass(frozen=True)
class PaymentObservation:
instrument_id: str
source_profile: str
state: str
received_msat: int | None
observed_at: int
evidence: dict
class PaymentAdapter(Protocol):
def create(self, order: dict) -> PaymentInstrument: ...
def lookup(self, instrument: PaymentInstrument) -> PaymentObservation: ...
Поле reference имеет разный смысл: payment hash для Lightning, собственная связь счёта с адресом или наблюдаемыми выходами для on-chain линии. Не пытаемся сравнить его с transaction id другой системы. Слой заказа знает тип инструмента и передаёт его правильному адаптеру, а формат evidence остаётся версионным договором.
В общем типе сумма хранится в msat. Для on-chain входа преобразование сатоши в миллисатоши умножением на тысячу точное. Обратное преобразование допустимо лишь для суммы, кратной тысяче. Нельзя округлять остаток Lightning и потом объявлять его точным Bitcoin-выходом. Цена курса фиксирована в серверном заказе, а observed received_msat описывает отдельный факт поступления.
Версия адаптера — часть источника
source_profile включает выбранную сеть, реализацию, версию договора и идентичность узла или поставщика. Один и тот же payment hash, прочитанный из чужого источника, не становится автоматически наблюдением нашего инструмента. Адаптер должен проверять соответствие своей конфигурации и исходной связи.
В справочнике LND есть отдельные операции добавления и поиска invoice по payment hash. Это полезный пример границ создания и чтения. Официальный первичный интерфейс LND. Мы не связываем статьи с произвольным REST-путём и не предполагаем одинаковое представление bytes во всех клиентах.
При обновлении адаптера сохраним способность читать старые инструменты или создадим явную миграцию. Нельзя просто заменить source_profile в базе и считать, что новый поставщик знает все ранее созданные счета. Если переноса истории нет, старый источник остаётся ответственным за наблюдение своих инструментов до выяснения их судьбы.
Публичный ответ не повторяет внутренний объект
В браузер возвращаем только необходимые данные: безопасный идентификатор заказа, выбранный способ, ожидаемую сумму, платёжный запрос при наличии права его смотреть, отображаемый срок и статус. Не отправляем токен RPC, macaroon, полный raw-ответ узла или внутренние заметки оператора.
def public_status(order, observation):
return {
'order_id': order['public_id'],
'course_id': 17,
'method': order['method'],
'expected_msat': str(order['amount_msat']),
'state': observation.state,
'source_updated_at': observation.observed_at,
}
Эта функция не проверяет право просмотра; такой контроль должен произойти до вызова. Public_id не является автоматическим доказательством владения заказом. Можно использовать сессию или специально ограниченный capability-токен, но выбранный механизм должен иметь область и срок. Скрытое угадывание последовательных orderId не подходит для персонального статуса.
Технический evidence остаётся в серверном слое и имеет свою политику доступа. Для on-chain результата может понадобиться связь выходов, срез цепочки и политика подтверждений. Для Lightning — payment hash, итоговое состояние и полученная сумма. Один общий словарь позволяет сохранять разные свидетельства, не превращая их в одинаковую фиктивную транзакцию.
Ошибки и неизвестное состояние
Адаптер различает неподдерживаемую конфигурацию, отказ подготовки и временную недоступность чтения. Транспортная ошибка lookup приводит к неизвестному наблюдению, а не к выводу «платёж не поступил». Последний достоверный результат сохраняется вместе со временем и причиной, поэтому карточка может показать устаревшее состояние честно.
Создание инструмента имеет другой риск: неизвестный ответ может означать уже существующий invoice. Автоматический retry всего метода create безопасен только при конкретном договоре идемпотентности. Обычную обёртку повторов чтения не переносим на создание нового счёта. Это различие должно быть видно в интерфейсе адаптера и эксплуатационном описании.
Ожидаемый маршрут
Сайт создаёт один заказ и передаёт его выбранному адаптеру. Возвращённый инструмент сохраняется со source_profile. Чтение получает наблюдение, проверяет связь и передаёт решение предметному слою. Предметный слой применяет собственную политику доступа атомарно с фиксацией результата.
On-chain адаптер не выдаёт оплачено раньше принятой политики двух подтверждений. Lightning адаптер не выдаёт оплачено по состоянию ожидания HTLC. Недоступный источник не превращает успех другого способа в ошибку. Общий экран показывает ясный результат, а разработчик сохраняет объяснение его происхождения. В следующем уроке подробно определим границу Lightning settlement.
Граница общего типа
Если новый метод не умеет достоверно сообщать окончательный результат, нельзя включать его в общий интерфейс с фиктивным paid. Он может поддерживать лишь подготовку или предварительное наблюдение. Тогда доступная область объявляется в профиле, а завершение заказа остаётся неподдерживаемым до реализации необходимого адаптера. Единое имя метода lookup не создаёт одинаковых гарантий у разных источников.