Bitcoin-платежи: частичная и избыточная оплата
Политика подтверждений умеет выбрать допустимые выходы, но цена курса может быть достигнута несколькими переводами. Читатель отправил 12 000 сатоши, затем 18 000; другой отправил сразу 35 000. Для обоих случаев нужен определённый денежный статус, который не теряет средства и не выдаёт несколько прав доступа на один заказ.
В этой главе добавим частичную и избыточную оплату к счёту курса 17 с ценой 30 000 сатоши. Все числа являются условными входами учебной модели. Транзакции не формируются и не отправляются, SQL и программа сейчас не исполняются.
Сохраняем три разные суммы
Обнаруженная сумма показывает все назначенные выходы, которые источник сейчас видит как потенциальное поступление. Допустимая сумма включает только выходы, отвечающие onchain-v1: активная цепочка и не менее двух подтверждений. Требуемая сумма берётся из неизменяемого счёта.
Разница между обнаруженной и допустимой суммой помогает интерфейсу объяснить ожидание. Если оба перевода видны, но второй ещё неподтверждён, не показываем «Недоплачено» без уточнения. Денежное поступление обнаружено полностью, однако условия глубины выполнены только для первой части.
Для неизвестного или устаревшего источника обе суммы получают отметку давности. Не выдаём старую оценку за актуальную, но и не обнуляем её из-за timeout. История должна сохранять, какие выходы были известны до задержки и когда в последний раз удалось их уточнить.
Вычисляем состояние без дробного допуска
Используем точные целые сатоши. Никакое округление не должно превращать 29 999 в 30 000. Если продавец когда-нибудь введёт допуск, он будет отдельным правилом с версией и причиной, а не скрытым эффектом сравнения дробей.
from dataclasses import dataclass
@dataclass(frozen=True)
class AmountPosition:
required_sats: int
eligible_sats: int
remaining_sats: int
excess_sats: int
state: str
def amount_position(required, eligible):
if type(required) is not int or required <= 0:
raise ValueError("invalid invoice amount")
if type(eligible) is not int or eligible < 0:
raise ValueError("invalid eligible amount")
remaining = max(required - eligible, 0)
excess = max(eligible - required, 0)
state = "partial" if remaining else "sufficient"
if eligible == 0:
state = "awaiting"
return AmountPosition(required, eligible, remaining, excess, state)
Для входа required=30000, eligible=12000 ожидаем остаток 18000 и состояние partial. Для eligible=35000 остаток равен нулю, избыток — 5000, состояние достаточности суммы сохраняется. Эти результаты разобраны по выражениям функции, а не получены её запуском.
Важно, что функция не выдаёт доступ и не оформляет возврат. Она только описывает денежную позицию. Срок счёта, свежесть наблюдения, политика подтверждений и состояние заказа проверяются в других частях принятия решения.
Складываем несколько выходов
Уже существует уникальность по network:txid:vout. Поэтому два ответа об одном выходе не увеличивают оплату. Для частичной оплаты складываем разные актуальные выходы, назначенные одному счёту. Отдельный счёт того же пользователя не входит в эту сумму автоматически.
Предположим, выход A составляет 12 000 и имеет три подтверждения. Выход B составляет 18 000 и имеет одно. Обнаруженная сумма равна 30 000, допустимая — 12 000. После второго подтверждения B допустимая сумма становится полной, и обработчик может перейти к квалификации платежа.
Если B заменён конфликтной транзакцией, не считаем его отрицательной денежной величиной и не вычитаем произвольную комиссию. Пересчитываем проекцию допустимых выходов по их текущему состоянию. Связанный новый выход оценивается отдельно; изменение txid не означает, что появился независимый второй покупатель.
Не вычитаем комиссию отправителя из получения
В условном равенстве 80 000 = 30 000 + 49 300 + 700 библиотека получает выход 30 000. Комиссия 700 уже является разницей входа и всех выходов; её не нужно повторно вычитать из полученной суммы. Ошибка дала бы фиктивную недоплату 700, хотя назначенный выход отвечает цене.
Если читатель сам отправил библиотеке только 29 300, потому что решил включить комиссию в общую цену, выход действительно меньше счёта. Сервер не угадывает намерение по интерфейсу чужого кошелька. Он показывает полученное назначение и остаток по точной арифметике.
Именно различие между суммой отправления пользователя и выходом продавца делает модель наблюдений полезной. Поле кошелька отправителя и поле входящего выхода библиотеки относятся к разным точкам зрения. В нашем учёте используется вторая.
Работаем с избыточной оплатой
Избыток 5000 не создаёт право на второй курс автоматически. Заказ всё ещё относится к курсу 17. Дополнительные средства сохраняются как денежная позиция счёта и получают отдельное решение по заранее объявленной политике продавца.
Не отправляем излишек на первый адрес входа транзакции. Он может принадлежать инфраструктуре отправителя, общей транзакции или совсем не быть пригодным получателем возврата. Корректный возврат требует нового назначения, подтверждённых полномочий и независимого согласования; это тема шестнадцатого урока.
Также не скрываем избыток в интерфейсе. Владелец видит принятую цену, учтённую сумму и сообщение о дополнительном поступлении. Служба поддержки должна иметь объяснимую связь с исходными outpoint, а не только заметку «Покупатель ошибся» без данных.
Меняем условие: две вкладки одного счёта
Читатель открыл счёт на телефоне и компьютере. На первом отправил 12 000, на втором ещё видит первоначальные 30 000. Если второй интерфейс не обновился, он может отправить лишнее. Поэтому карточка показывает время актуальности и перед новой инструкцией запрашивает текущую денежную позицию.
Но обновление интерфейса не гарантирует, что другая отправка не произошла одновременно. У уже выданных реквизитов остаётся независимая жизнь в кошельке. Модель обязана обработать итоговую избыточную оплату корректно, а не обещать невозможность её возникновения.
Цена следующего действия
При частичной оплате интерфейс показывает остаток отдельной величиной, а не перезаписывает первоначальную цену счёта. Если первоначальная цена исчезнет из карточки, человек не поймёт, почему новая инструкция меньше первой. История также должна сохранить обе части: назначенный заказ и последующие поступления. Это позволяет объяснить итог без обращения к чужому кошельку.
Разбор результата
Частичная оплата стала суммой разных допустимых выходов, а избыток сохранился как отдельная позиция. Комиссия отправителя не вычитается повторно, совпадение пользователя не объединяет разные счета, и денежная достаточность не подменяет доступ или возврат.
В следующем уроке добавим срок счёта и позднее поступление. Это позволит понять, что происходит, когда нужная сумма пришла после окончания инструкции. Полный маршрут находится в оглавлении курса.