Bitcoin-платежи: наблюдение выходов по кошельку
Серверный адаптер может читать кошелёк библиотеки, но сырой ответ RPC ещё не является платежом курса. В одной транзакции могут быть несколько выходов, а общее денежное поле описывает отношение кошелька к транзакции. Нам нужно выделить конкретный выход, который направлен на адрес сохранённого счёта.
В этой главе определим нормализованное наблюдение. Оно сохраняет идентификатор выхода, сеть, сумму и сведения о текущей цепочке. Примеры условные; Bitcoin Core не запускается, реальные txid и адреса не создаются. Решение о выдаче доступа пока не принимаем.
Выбираем устойчивую единицу наблюдения
Для on-chain оплаты используем outpoint: пару txid и vout. txid определяет транзакцию, vout — номер выхода в ней. Добавляем профиль сети, потому что наш журнал должен явно отличать regtest от любых будущих окружений. Одна запись без номера выхода могла бы потерять второй платёж внутри той же транзакции.
Предположим, транзакция содержит выход на адрес курса и другой выход сдачи отправителю. В библиотечный платёж попадает только первый. Сдача не прибавляется к сумме заказа и не становится основанием возврата. Именно поэтому недостаточно смотреть на общий объём выходов транзакции или баланс кошелька.
Есть и обратный случай: два разных адреса библиотеки встречаются в одной транзакции. Они могут относиться к разным счетам. Каждую пару txid:vout назначаем отдельно по сохранённому адресу, даже если транзакция общая. Уникальность адреса в таблице счетов делает это назначение однозначным внутри нашей модели.
Читаем кошельковую историю
Метод listsinceblock возвращает историю относительно контрольного блока. У него есть данные об удалённых из цепочки записях при реорганизации; доступность такого восстановления зависит и от режима узла. Полученная история остаётся кошельковой, а не всемирным списком платежей.
Для уточнения известной транзакции используем gettransaction. В его details есть категория и отдельные выходы. Поле общей суммы не подменяет детализацию, а сведения о подтверждениях относятся к состоянию, которое видел кошелёк при подготовке ответа.
receive является нужной категорией обычного входящего перевода. Награды блоков, отправления и сдача не превращаются автоматически в оплату товара. Если лаборатория получила coinbase на адрес библиотеки, это учебное пополнение кошелька, а не доказанный заказ читателя.
Определяем свою схему
Нормализованный объект должен быть небольшим и пригодным для повторного применения. Его ключ не зависит от текущего числа подтверждений: иначе каждый новый блок создавал бы новый «платёж».
from dataclasses import dataclass
from typing import Optional
@dataclass(frozen=True)
class OutputObservation:
network: str
txid: str
vout: int
address: str
amount_sats: int
confirmations: int
block_hash: Optional[str]
observed_tip_hash: str
observed_at: int
amount_sats получается точным преобразованием из третьего урока. confirmations может быть отрицательным для конфликтной транзакции, поэтому это не безусловно положительное число. block_hash отсутствует у неподтверждённого состояния. Время наблюдения задаёт наша служба, а время блока хранится отдельно, если требуется.
observed_tip_hash связывает ответ с известным состоянием источника. Если два ответа относятся к разным вершинам, обработчик не должен считать их единым атомарным снимком. Core и кошелёк имеют свои правила консистентности, поэтому приложение обязано сохранять контекст и уметь перечитать данные.
Сопоставляем адрес со счётом
Наблюдатель сначала убеждается, что категория и поля подходят для входящего выхода. Затем ищет адрес в сохранённых назначениях. Если адрес неизвестен, записывает неназначенное поступление для сверки, а не выбирает «последний открытый счёт с такой же суммой».
Сумма тоже не служит ключом назначения. Два курса могут стоить одинаково, один читатель может оплачивать частями, другой — отправить лишнее. Единственная связь здесь создана заранее сервером: адрес принадлежит определённому счёту. Все последующие решения опираются на эту запись.
wallet detail
→ category + outpoint + exact amount
→ saved address assignment
→ observation journal
→ current payment projection
Стрелки показывают порядок преобразований, а не уже работающий процесс. На каждом этапе неподходящие сведения получают объяснимую категорию. Повреждённый txid или отрицательная сумма входящего выхода не исправляются догадкой; обработчик сохраняет диагностический отказ без выдачи доступа.
Что нельзя вывести из наблюдения
Адрес получения не устанавливает личность отправителя. Входы могут принадлежать нескольким участникам или системе совместного формирования транзакции. Поэтому не извлекаем «адрес покупателя» из первого входа и не связываем его с пользовательской сессией библиотеки.
Также наличие trusted в кошельковом ответе не равно коммерческому решению «доступ можно выдать». Кошелёк оценивает свою сторону расходования, а библиотека применяет отдельную политику подтверждений и срока счёта. Перенос одного поля между этими смыслами скрыл бы важное решение продавца.
Положительное число подтверждений тоже не является обещанием необратимости. История может измениться при реорганизации. Запись должна сохранять происхождение наблюдения и позволять обновить проекцию, не теряя факт ранее принятого решения.
Меняем пример: два выхода на один адрес
Пусть условная транзакция имеет выходы vout=0 на 12 000 и vout=2 на 18 000 сатоши, оба на адрес счёта курса 17. Это два разных наблюдения. Их допустимо сложить по правилам частичной оплаты, но нельзя объединить в одну запись только по txid.
Если тот же ответ прочитан повторно, оба outpoint сохраняют прежние ключи. Журнал обновит их состояния, а не добавит ещё 30 000. Если один выход окажется конфликтным, текущая сумма будет пересчитана только из допустимых наблюдений. В дальнейшем этот пример покажет разницу между историей и текущей проекцией.
Разбор результата
Теперь у приложения есть конкретная единица платежного наблюдения: выход в определённой сети с точной суммой и происхождением. Мы не называем сырой кошельковый ответ заказом и не используем совпадение суммы как назначение. Это делает дальнейшую обработку повторов и конфликтов определённой.
В следующем уроке сохраним наблюдения в идемпотентном журнале. Он отделит повтор чтения от нового поступления и подготовит восстановление после сбоя. Все главы находятся в оглавлении.