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

Bitcoin-платежи: возврат с отдельным согласованием

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

В этой главе построим только модель согласованного возврата. Используем условный избыток 5000 сатоши по счёту курса 17. Реальных адресов, подписей и транзакций нет; кошельки и команды отправления не запускаются. Процедура показывает архитектурные границы будущего учебного сервиса.

Почему нельзя вернуть на адрес входа

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

Поэтому получатель возврата задаётся отдельным обращением владельца заказа. Сайт проверяет его полномочия и сеть адреса, а операция требует независимого согласования. Данные браузера не попадают прямо в команду кошелька. Замена адреса после согласования отменяет прежнее разрешение и создаёт новую версию запроса.

Само владение аккаунтом тоже не доказывает владение конкретным Bitcoin-адресом. Процедура подтверждения назначения должна быть определена отдельно и соразмерно риску. В лаборатории фиксируем решение оператора и причину, не выдумывая универсальную подпись, которая гарантировала бы все виды адресов и кошельков.

Сохраняем запрос возврата

Запрос ссылается на исходный счёт и денежную позицию. Сумма остаётся целой величиной сатоши. Поле назначения хранит уже проверенный адрес выбранной сети, но не секрет пользователя.

CREATE TABLE refund_requests (
    id TEXT PRIMARY KEY,
    invoice_id TEXT NOT NULL,
    amount_sats INTEGER NOT NULL CHECK (amount_sats > 0),
    network TEXT NOT NULL CHECK (network = 'regtest'),
    destination TEXT NOT NULL,
    request_version INTEGER NOT NULL,
    state TEXT NOT NULL,
    created_by TEXT NOT NULL,
    approved_by TEXT,
    approved_at INTEGER,
    operation_key TEXT UNIQUE,
    FOREIGN KEY (invoice_id) REFERENCES invoices(id)
);

Состояния проходят путь от requested к approved, затем к prepared, broadcast_observed и итоговому учёту. Если результат отправления неизвестен, операция получает отдельный разбор. Не перескакиваем от запроса пользователя сразу к отметке «Возвращено».

Уникальный операционный ключ нужен для повторов собственной процедуры. Он не означает, что Bitcoin Core автоматически обеспечит идемпотентность любого RPC отправления. У кошельковой операции есть собственный подписанный результат, и его судьбу нужно устанавливать отдельно.

Сверяем доступную для возврата позицию

Пусть учтено 35 000 сатоши при цене 30 000. Избыток составляет 5000. Перед согласованием проверяем, нет ли уже другого активного возврата на этот избыток и не изменилось ли состояние исходных выходов после реорганизации.

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

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

Отделяем сумму получателя и комиссию

Возврат 5000 сатоши может означать либо ровно такой выход получателю, либо общий бюджет с комиссией — это разные условия. В нашей модели запрос фиксирует именно сумму выходу получателя, а комиссия является отдельной затратой библиотеки. Не меняем это значение скрыто при подготовке транзакции.

При небольшом избытке комиссия может оказаться значимой относительно суммы. Это не повод автоматически уменьшить согласованный выход или объявить случайную универсальную минимальную сумму. Требуется заранее определённая процедура, которую оператор видит до разрешения действия.

Точная будущая комиссия зависит от выбранного кошелька и текущего состояния сети. Здесь не приводим измеренный fee rate и не заявляем, что условные 700 из первоначальной арифметики подходят для возврата. То число служило отдельным разбором входа и выходов, а не действующей оценкой.

Подготовка и подпись — отдельный этап

Можно построить процедуру через PSBT, разделив подготовку, проверку и подпись. В этом уроке не реализуем подписант и не предлагаем копировать неизвестный raw transaction. Паспорт подготовленной операции должен сохранять назначение, сумму, сеть и связь с утверждённой версией запроса.

Перед подписью независимый участник сверяет фактические выходы с паспортом. UI-карточка не является достаточной проверкой, если подписываемые байты содержат другое назначение. После изменения подготовленного результата прежнее согласование должно быть пересмотрено по установленной процедуре.

Приватные ключи не попадают в веб-сервер счёта. Читательский адаптер не получает методы отправления. Эти границы позволяют исследовать возврат, не превращая страницу статуса в кошелёк с широкими полномочиями.

Меняем условие: ответ отправления потерян

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

Если он уже находится в цепочке, повторная отправка другой операции не нужна. Если результат не был опубликован, дальнейшее действие определяется процедурой подписи и наблюдения. Статус unknown_result не переводится автоматически обратно в approved, разрешая любое новое отправление.

При поздней реорганизации исходного платежа после возврата возникает отдельный учётный инцидент. Его нельзя исправить удалением обоих событий: два денежных действия имеют свою историю. Журнал сохраняет связь с исходным счётом и основания принятого решения.

Закрываем служебное дело

После наблюдения возврата сохраняем связь его выхода с запросом и исходным счётом. Пользователь видит сумму назначенного возврата и этап подтверждения, а оператор — основание согласования и ограничения результата. Удалять исходный платёж после завершения возврата нельзя: он объясняет происхождение отдельной исходящей операции и остаётся частью денежной истории.

Разбор результата

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

В следующем уроке сопоставим внутренний журнал с кошельком, включая неназначенные поступления и незавершённые операции. Полное оглавление курса связывает все части модели.

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