Bitcoin-платежи: платёжная ссылка и QR
У счёта курса уже есть сохранённые цена, адрес и срок. Теперь передадим эти сведения кошельку читателя. Для этого используется платёжная URI и её графическое представление — QR. Сама ссылка остаётся инструкцией: открытие кошелька, сканирование картинки и возвращение на сайт не подтверждают поступление средств.
В старых руководствах обычно встречается BIP 21. Сейчас он помечен как заменённый BIP 321. Разберём совместимый базовый вариант с адресом и суммой, затем отделим новые возможности от того, что реализовано в нашей учебной модели. Кошелёк и генератор QR сейчас не запускаются.
Собираем ссылку из сохранённого счёта
URI начинается с bitcoin:. Это схема запуска платёжного обработчика, а не обычный HTTPS-адрес. После неё помещается адрес назначения, затем параметры. Не добавляем // по привычке к веб-ссылкам. Сумму выражаем в BTC с точкой и точностью до сатоши, используя функцию предыдущего урока.
Следующая функция принимает уже назначенный сервером адрес. Она не проверяет контрольные суммы Bitcoin сама: такую проверку должен выполнять выбранный проверенный адресный адаптер в соответствующей сети. Нам достаточно показать отдельный этап сериализации разрешённых полей.
from urllib.parse import urlencode, quote
def invoice_uri(invoice, checked_address):
if invoice["network"] != "regtest":
raise ValueError("unexpected laboratory network")
if checked_address != invoice["address"]:
raise ValueError("address binding changed")
query = urlencode({
"amount": sats_to_btc_text(invoice["amount_sats"]),
"label": "Учебная библиотека",
"message": f"Курс {invoice['course_id']}"
}, quote_via=quote)
return f"bitcoin:{checked_address}?{query}"
amount получается из сохранённой цены, а не из поля формы. label и message помогают человеку понять назначение. Они не являются секретом или надёжным доказательством заказа. Не записываем туда email, токен сессии или приватный идентификатор пользователя: содержимое ссылки будет доступно приложению кошелька и человеку, который увидел QR.
Почему важно кодирование текста
Русский текст должен корректно передаваться как UTF-8 с процентным кодированием. Символ & в названии без кодирования превратился бы в разделитель параметров, а # мог бы восприниматься как часть другого синтаксиса. Поэтому не собираем query простым соединением строк, полученных из редакторского ввода.
У BIP 321 есть свои правила ключей и повторов. Наш генератор выпускает один amount, один label и один message; он не нуждается в неоднозначных дублях. Новая спецификация рассматривает ключи параметров без учёта регистра, но это не разрешение менять регистр самого адреса или произвольного значения. Адреса проверяются по собственному формату.
Ссылка курса на 30 000 сатоши должна содержать сумму 0.00030000. Не используем запись 30000: внутри URI это означало бы BTC, а не сатоши. Локализованная запятая также недопустима. Цена на видимой странице может быть отформатирована по-русски, однако машинная инструкция остаётся строго определённой.
Оставляем понятную альтернативу QR
На странице рядом с картинкой показываем сеть, адрес текстом и точную сумму. Читатель должен иметь возможность скопировать реквизиты, если камера не работает или QR не поддерживается. Перед копированием сайт повторно читает условия своего счёта; изменение размера картинки не должно менять её содержимое.
QR — способ кодировать готовую строку. Библиотеку рендеринга выбирают отдельно, проверяя лицензирование и поддержку длинных payload. В этом уроке не генерируем фиктивную картинку и не обещаем её читаемость камерой. У интерфейса должен быть текстовый источник истины, с которым в будущем сравнят фактически отрисованный QR.
Для изображения задаём осмысленное описание и достаточный контраст. Надпись «Оплата курса в regtest» помогает заметить учебную сеть. Слишком декоративный код с логотипом посередине может ухудшить считывание; оценивать результат нужно на реальных устройствах после будущего этапа исполнения, а не по привлекательному макету.
Что сделает кошелёк
Кошелёк разбирает адрес, сумму и дополнительные поля, после чего пользователь подтверждает отправление в своём интерфейсе. Он может отказаться от ссылки, не поддержать сеть или изменить сумму перед переводом. Серверный счёт от этого не меняется: библиотека будет сравнивать фактически полученные выходы со своими условиями.
Поэтому событие клика не переводит счёт в paid. Кнопка «Я отправил» может лишь ускорить запрос актуального статуса, причём сервер всё равно пользуется доверенным источником. Переданный браузером txid допустимо принять как подсказку для поиска, но нельзя выдать доступ только по его наличию.
Если ссылка открылась в чужом обработчике или у пользователя вообще нет Bitcoin-кошелька, страница должна сохранить копируемые реквизиты и объяснение regtest. Автоматическое скачивание приложения не входит в обязанности платёжного маршрута и не доказывает возможность безопасно оплатить заказ.
Совмещённые инструкции и границы модели
BIP 321 допускает дополнительные способы оплаты, включая Lightning. Позже мы добавим вторую платёжную ветку к одному намерению покупки. На этом этапе не помещаем неизвестную строку lightning в ссылку: сначала потребуются реально созданный invoice выбранного адаптера и правила взаимоисключения повторной оплаты.
Параметры запроса доказательства оплаты тоже нельзя использовать как произвольный веб-возврат. В частности, спецификация ограничивает схемы callback; обычная HTTPS-ссылка в pop не является допустимым способом открыть наш сайт. В учебном проекте вообще не добавляем этот механизм. Статус читается через отдельный авторизованный HTTP-интерфейс библиотеки.
Это полезное архитектурное разделение: платёжная инструкция обслуживает кошелёк, а пользовательская сессия обслуживает сайт. Попытка объединить их случайным callback привела бы к доверию внешнему сообщению там, где требуется собственное наблюдение.
Меняем условие: ссылка сохранена после срока
Читатель сделал скриншот QR, а счёт уже истёк. Картинка не умеет сама исчезать из телефона и по-прежнему указывает на прежний адрес. Перед формированием нового QR интерфейс проверяет срок, но наблюдатель продолжает учитывать поздний перевод на старый адрес.
Позднее поступление попадёт в отдельную ветку политики, которую изучим в двенадцатом уроке. Не отправляем деньги обратно автоматически и не удаляем историю счёта. Сама URI не даёт такой команды и не содержит достоверного адреса возврата.
Мы получили воспроизводимую сериализацию инструкции и отделили её от решения об оплате. В следующем уроке создадим ограниченный серверный RPC-адаптер. Оглавление связывает все этапы лаборатории.