Skip to content

rippled 3.3.0 schema, three conformance guards, autofill fees and reconnect hardening (10.11.0.0) - #76

Merged
Platonenkov merged 14 commits into
releasefrom
dev
Aug 12, 2026
Merged

rippled 3.3.0 schema, three conformance guards, autofill fees and reconnect hardening (10.11.0.0)#76
Platonenkov merged 14 commits into
releasefrom
dev

Conversation

@Platonenkov

@Platonenkov Platonenkov commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Релиз 10.11.0.0: Xrpl и Xrpl.BinaryCodec идут на 10.11.0.0, Xrpl.AddressCodec и Xrpl.Keypairs остаются на 10.9.0.0 — они не менялись и продолжают подтягиваться потребителями в опубликованной версии.

Полное описание — в секции 10.11.0.0 CHANGES.md. Ниже — что именно едет и почему.

⚠️ Ломающие изменения

DynamicMPT: MutableFlagsImmutableFlags, смысл инвертирован. Поле то же — UInt32, nth 53, те же значения битов — но установленный бит теперь означает «заморожено навсегда», а не «разрешено менять позже». Выпуск, созданный без поля, стал полностью изменяемым: противоположность прежнему поведению. Так как код поля не изменился, старые модели собирали блоб, который нода принимала и читала наоборот. Затронуты MPTokenIssuanceCreate, MPTokenIssuanceSet, LOMPTokenIssuance; два enum'а *MutableFlags заменены общим MPTokenIssuanceImmutableFlags.

DynamicMPT: включение возможности переехало в флаги транзакции. Вместо MPTokenIssuanceSet.MutableFlags = tmfMPTSet*Flags = tfMPTSet* (0x04–0x100) в MPTokenIssuanceSetFlags.

Sponsor: SponsorshipSet принимает дельты. FeeAmountDelta (Amount 34) и RemainingOwnerCountDelta (Int32 2, знаковый) вместо абсолютных FeeAmount/RemainingOwnerCount — те остались полями только объекта Sponsorship. Отправка старых полей отвергается нодой на разборе: Field 'FeeAmount' found in disallowed location.

Удалены свойства ledger-объектов, которые не являются полями протоколаLOVault.DomainID, LOLoan.PrincipalRequested, LOCredential.OwnerNode, LONFTokenPage.NFTokenPage, LOAmm.LedgerCurrentIndex, LOAmm.Validated. Ни одно из них никогда не могло содержать значение: rippled строит объект по фиксированному SOTemplate. Проверено на живой ноде и по четырём версиям rippled.

Миграция для всех пунктов — с примерами до/после — в CHANGES.md.

Протокол и модели

  • Стенд переведён на rippled 3.3.0, что включает Sponsor, DynamicMPT, BatchV1_1, PermissionDelegationV1_1, ConfidentialTransfer на генезисе: 44 теста, прежде скипавшихся через AmendmentGuard, теперь выполняются на каждом прогоне
  • sfLEVersion — версия схемы Vault (rippled #7817): definitions.json, генерируемый Field.Uint8, LOVault.LEVersion и enum VaultVersion. Отсутствие значения у старых vault'ов осмысленно, а не «нет данных»
  • Флаги ledger-объектов, которых не было в моделяхMPTCanHoldConfidentialBalance, lsfMPTAMM и другие: безымянный бит всё равно приходил числом, поэтому пропажа ничем себя не проявляла
  • Поля, объявленные протоколом, но отсутствовавшие в моделяхPreviousTxnID/PreviousTxnLgrSeq на LOAmm, LOAmendments, LODirectoryNode, LOFeeSettings, LONegativeUNL
  • LOAmmAMMAccount никогда не десериализовался (поле объекта называется Account, атрибута не было), а конструктор объявлял объект как AccountRoot

Три поверхности конформности

Модели теперь проверяются против вендорённых копий rippled по всем трём осям, а не выборочно:

Гвард Источник истины Что ловит
TestUTxFormatConformance transactions.macro поля транзакций
TestULedgerEntryFieldsConformance ledger_entries.macro поля ledger-объектов
TestULedgerFlagsConformance LedgerFormats.h флаги ledger-объектов

Фикстуры пиньтся по sha и байт-идентичны upstream (перепроверяются curl … | diff из .ref). transactions.macro и LedgerFormats.h — на теге 3.3.0, ledger_entries.macro — на develop, потому что sfLEVersion есть только там. Парсер флагов дополнительно читает lsif*-константы, которые в 3.3.0 вынесены из LEDGER_OBJECT.

Autofill

Покрыты три транзактора со special base fee, которые недоплачивали — а это отказ telINSUF_FEE_P, а не переплата:

  • LoanSet — одна базовая комиссия на каждого подписанта CounterpartySignature.Signers, а не фиксированный ×2
  • LoanPay — инкремент на каждые 5 обрабатываемых платежей, до 20 инкрементов
  • пять confidential-MPT транзакций — множитель 9, то есть десять базовых комиссий

Клиент: переподключение

  • исключение из OnConnected больше не убивало клиент навсегда
  • повторные сбои OnConnected уходят в бэкофф вместо бесконечного цикла connect → падение → разрыв на фиксированной задержке
  • жизненный цикл сессии переподключения синхронизирован; _reconnectCts объявлен volatile
  • быстрое переподключение не трогает чужой источник отмены

Инфраструктура

  • nightly-pin-watch.yml — новый еженедельный сторож пина nightly-стенда. Пин определяет горизонт definitions-watch: пока он устаревший, еженедельная сверка definitions.json идёт против сборки старее той, что гоняет CI, и рапортует «в порядке» про прошлое. Именно так оба переименования 3.3.0 месяц оставались незамеченными. Бампает .ci-config/bump-nightly-pin.sh, конфиг перегенерируется по develop-коммиту, зашитому в саму строку версии, поэтому конфиг и бинарник не могут разойтись
  • Xrpl.BinaryCodec синхронизирован по версии с Xrpl (10.11.0.0): кодек везёт новые поля, поэтому двигаются вместе

Проверка

  • юнит-тесты: 902
  • интеграция против стенда 3.3.0: 265, включая 44 теста, разбуженных новыми амендментами
  • merge queue прогнала полный набор перед каждым мержем в dev

Summary by CodeRabbit

  • New Features

    • Added MPT path-step encoding and validation, including MPToken issuance identifiers.
    • Added support for immutable MPT issuance flags, sponsorship deltas, confidential balances, and Vault versions.
    • Expanded autofill fee calculations for loans and confidential MPT transactions.
    • Added configurable health checks and inactivity timeouts.
  • Bug Fixes

    • Improved reconnection, server switching, and WebSocket error handling.
    • Corrected ledger-entry mappings and unknown-type deserialization behavior.
  • Documentation

    • Updated lending, sponsorship, and confidential MPT guides.

xrpl-release-watch Bot and others added 5 commits August 3, 2026 12:14
Co-authored-by: github-merge-queue <github-merge-queue@users.noreply.github.com>
…гда (10.10.1.0) (#72)

* fix(client): исключение из OnConnected больше не убивает клиент навсегда (10.10.1.0)

Connection.OnceOpen ловил любое исключение потребительского обработчика
OnConnected и звал Disconnect() - пользовательский путь отключения:
_permanentlyDisconnected = true плюс ClearReconnectState(). После этого
клиент мёртв навсегда: реконнект-цикл не перезапускается, новых сокетов
не открывается, OnConnected больше не поднимается, а любой запрос падает
с NotConnectedException. Наружу при этом не выходило ничего - ни события,
ни лога.

Триггер - самый штатный сценарий: перезапуск ноды. OnConnected - типовое
место восстановления подписок (SDK их после реконнекта не восстанавливает),
а нода принимает TCP раньше, чем начинает отвечать на запросы, поэтому
первый subscribe уходит в RequestTimeout и падает. В проде так встал флот
ботов - по четыре часа тишины после обновления ноды.

Теперь сбой обработчика трактуется как отказ СОЕДИНЕНИЯ, а не как воля
пользователя: сокет сносится, дальше работает обычный реконнект с
экспоненциальным бэкоффом, флаг постоянного отключения не ставится.

Защита от вечного цикла: OnceOpen чистит реконнект-состояние до вызова
обработчика, поэтому счётчик попыток самого цикла обнуляется на каждом
успешном TCP-коннекте и сойтись не может. Подряд идущие сбои обработчика
считаются отдельно (_connectHandlerFailures, сбрасывается при успешном
прогоне обработчика, в Connect() и в ChangeServer()); при достижении
MaxReconnectAttempts с StopAfterMaxAttempts клиент осознанно сдаётся -
сразу понятный NotConnectedException вместо пятиминутного молчания.

Причина теперь наблюдаема: исключение поднимается через OnError с
errorMessage = "connectHandlerError" и через OnConnectionStatus.

Попутно: WebSocketClient.SendMessageAsync (async void, вызывается без
await) больше не глотает ошибку отправки молча - она трассируется.
Поведение не меняется, запрос по-прежнему ограничен RequestTimeout, но
причина перестала быть невидимой при диагностике.

TestUOnConnectedHandlerFailure закрывает все четыре свойства против
mock rippled: восстановление после разового сбоя, работоспособность
клиента после восстановления, отчёт через OnError и остановка вечно
падающего обработчика.

* fix(review): замечания CodeRabbit к PR #72

1. Детальная причина отказа терялась. SetConnectionState после Disconnect()
   - no-op: Disconnect() уже перевёл состояние в Disconnected, а
   SetConnectionState уведомляет только при смене состояния. Подписчик видел
   "Disconnected by user request." вместо причины. Уведомление перенесено
   перед Disconnect().

2. WaitForConnectionAsync проверял _permanentlyDisconnected один раз на входе
   и никогда - внутри цикла ожидания. Ждущий в Connect() вызывающий досиживал
   весь ConnectionAcquisitionTimeout (по умолчанию 5 минут) и получал общий
   TimeoutException вместо реальной причины. Проверка перенесена в цикл.

3. Личность сокета при разборе. WebSocketClient.Connect вызывает OnConnect без
   await, поэтому connect-лок отпускается, пока обработчик ещё выполняется, и
   к моменту разбора ws может указывать уже на новый сокет. Закрывается ровно
   тот сокет, для которого падал обработчик; ws обнуляется, только если всё
   ещё ссылается на него; если сокет уже не текущий - реконнект-состояние
   принадлежит новому соединению и не трогается.

   При этом тест на вечно падающий обработчик вскрыл гонку в самом фиксе:
   проверка "цикл реконнекта уже работает" гонится с выходом этого цикла - он
   прерывается сразу, как сокет отрапортовал Open, то есть ДО того, как
   обработчик успел упасть. Проигрыш гонки оставлял клиента без реконнекта -
   ровно тот клин, который правится. Вместо проверки путь теперь безусловно
   забирает владение: гасит текущий цикл и запускает новый, а пришедший позже
   OnceClose видит живой цикл и корректно отступает. Побочно: восстановление
   стало занимать сотни миллисекунд вместо десятков секунд.

4. Ошибка отправки: Debug.WriteLine вырезается без DEBUG, которого нет в
   релизной сборке. Мёртвый колбэк ошибки WebSocketClient (его никто не звал и
   не подписывал) теперь доносит исключение до Connection.OnError с
   errorMessage = "socketSendError". Только уведомление: неудачная отправка
   сама по себе не означает потерю соединения, реконнект не запускается,
   запрос по-прежнему ограничен RequestTimeout.

Отклонено: подъём версий Xrpl.AddressCodec / Xrpl.BinaryCodec / Xrpl.Keypairs
до 10.10.1.0. Эти пакеты в этом PR не менялись, версионируются независимо и
уже расходятся с основным на NuGet осознанно (Xrpl 10.10.0 против base 10.9.0;
так же было в 10.9.1.0 и 10.10.0.0). Публикация идёт с --skip-duplicate, так
что неизменные пакеты просто пропускаются, а подъём выложил бы побайтово те же
артефакты под новым номером.

Тест TestPermanentlyFailingOnConnectedHandlerStops усилен: теперь проверяет,
что ожидающий вызывающий разблокируется именно NotConnectedException.

* fix(review): вторая порция замечаний CodeRabbit к PR #72

1. Реконнект-цикл писал в чужую сессию. StopReconnectLoop() отменяет токен, но
   не дожидается цикла, поэтому снятый цикл мог добраться до тела или хвоста
   уже после того, как установлена замена, и обнулить _reconnectMode живого
   цикла, сбросить его _reconnectAttempts или выбросить его _reconnectCts.
   Дефект существовал и раньше (RetireCurrentSessionAndReconnectAsync снимает
   циклы ровно так же), но путь сбоя OnConnected делает его гораздо более
   достижимым. ReconnectLoopAsync теперь принимает CancellationTokenSource,
   которым владеет, и трогает общее состояние, только пока этот источник
   остаётся активным.

2. Mock-сервер в тестах не останавливался: CreateMockRippled.Start() создавал
   Server (его конструктор поднимает слушателя) и терял ссылку. Добавлен
   Stop(), TestUOnConnectedHandlerFailure зовёт его в TestCleanup.

* fix(client): ChangeServer на неподнятый сервер больше не убивает клиент

Тот же класс клина, что и основной фикс PR, но другой путь. Найдено при
прогоне Blazor-демо: переключение селектора сети на выключенную ноду.

ChangeServer ставил ГЛОБАЛЬНЫЙ _isIntentionalDisconnect = true, чтобы
отфильтровать поздние колбэки уходящего сокета, а сбрасывался этот флаг
только в OnceOpen. Если новый сервер не поднимался, OnceOpen не выполнялся
никогда: OnConnectionFailed читал отказ НОВОГО соединения как отключение по
воле пользователя, писал "Connection closed permanently.", не запускал
реконнект-цикл, и дальше всё - включая сам ChangeServer - падало с
вводящим в заблуждение "No connection attempt in progress. Call Connect()
first.". Поднятие сервера потом ничего не меняло: клиент был мёртв.

Поздние колбэки теперь фильтруются исключительно по-сокетному отслеживанием,
которое здесь и так уже было (_userInitiatedSockets плюс собственный флаг
сокета, выставляемый в RetireOldSessionAsync) - ровно так же, как всегда
делал путь ping-timeout / network-drop; в его коде даже висит комментарий,
предостерегающий от глобального флага именно по этой причине. Дополнительно
флаг явно гасится на входе, чтобы ChangeServer после пользовательского
Disconnect() не подавлялся оставшимся от него значением.

Дефект существовал до этого PR - проверено дважды: ни одна из строк этого
пути им не менялась, и одинаковый диагностический тест на чистом dev
(f17fc11) даёт байт-в-байт тот же вывод.

TestUChangeServerFailure закрывает оба случая: клиент доходит до нового
сервера, когда тот появляется, с предшествующим Disconnect() и без него.
Оба теста падают на dev и проходят с фиксом.

* fix(review): гонка старта и остановки mock-сервера в тестах

Start() выполняется на фоновом потоке, поэтому Stop() из TestCleanup мог
увидеть _server == null до присваивания и оставить живого слушателя.
Останов теперь фиксируется под локом: старт, завершившийся после него,
гасит собственный Server вместо того, чтобы его сохранить.
…тие DynamicMPT (10.11.0.0) (#71)

* feat(models): именованные lsf-флаги MPT и интеграционное покрытие DynamicMPT

Модели MPT отставали от rippled по флагам ledger-объектов, а DynamicMPT
(XLS-94) не имел ни гварда, ни интеграционных тестов: поля MutableFlags в
моделях были, но против ноды никогда не проверялись.

* MPTokenIssuanceFlags + MPTCanHoldConfidentialBalance (0x80) — вводит
  ConfidentialTransfer; остальная поддержка амендмента уже полная
  (транзакции 85-89, IssuerEncryptionKey/AuditorEncryptionKey,
  ConfidentialOutstandingAmount), не хватало только имени у флага
* MPTokenFlags + lsfMPTAMM (0x4) — пробел старше: флаг есть уже в 3.2.1.
  AMMCreate ставит его вместе с lsfMPTAuthorized, неявно авторизуя MPT-актив
  для псевдо-аккаунта AMM (src/libxrpl/tx/transactors/dex/AMMCreate.cpp)
* AmendmentGuard + DynamicMPT; id совпал с тем, что generate-amendments.sh
  уже кладёт в [amendments] nightly-стенда
* TestIDynamicMPT — 4 теста, каждый читает результат из ledger-объекта:
  MutableFlags с Create доезжают в LOMPTokenIssuance; MPTokenIssuanceSet
  меняет TransferFee и MPTokenMetadata, не переписывая MutableFlags;
  tmfMPTSetCanLock поднимает lsfMPTCanLock на issuance, созданном без него;
  мутация без разрешения отбивается tecNO_PERMISSION, а метаданные в ledger
  остаются прежними

Сценарии выверены по транзактору rippled 3.3.0-rc1
(src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp), а не по документации:
отсюда tfMPTCanTransfer на создании — preclaim требует lsfMPTCanTransfer уже
установленным, включение его той же транзакцией правило не удовлетворяет.

Проверено: nightly-стенд (3.3.0-b1, DynamicMPT enabled) — 4/4 passed;
CI-стенд (xrpld 3.2.0) — 4 skipped через гвард, exit 0; MPT-регрессия 28/28,
юнит-тесты 867/867. Значение 0x80 подтверждено на живом объекте:
MPTokenIssuanceSet с MutableFlags=0x40 даёт Flags=128.

* feat(models): флаги ledger-объектов по LedgerFormats.h и гвард против дрейфа

LedgerFormats.h — единственное место, где протокол объявляет, какие lsf-флаги
принадлежат какому ledger-объекту (definitions.json несёт коды полей и типы
объектов, но не значения флагов). Против него не стоял ни один тест, поэтому
пропущенный флаг не давал симптома: безымянный бит всё равно приходит в модель
числом, теряется только возможность проверить его по имени. Так lsfMPTAMM
прожил несколько релизов. Пофайловый диф всех enum'ов против тега 3.3.0-rc1
нашёл четыре пробела.

* MPTokenIssuanceFlags + MPTCanHoldConfidentialBalance (0x80) — от
  ConfidentialTransfer; значение подтверждено на живом объекте
* MPTokenFlags + lsfMPTAMM (0x4) — пробел старше, флаг есть уже в 3.2.1
* LOLoan.Flags — у объекта Loan вообще не было свойства Flags (нет его и в
  BaseLedgerEntry), то есть lsfLoanDefault/lsfLoanImpaired/lsfLoanOverpayment
  не читались типизированной моделью никак: статус дефолта и impairment займа
  был ненаблюдаем. Добавлено как LoanFlags?
* новые SignerListFlags и DirectoryNodeFlags — сами свойства Flags остаются
  raw uint (смена типа была бы breaking), enum'ы дают именованные константы
  вместо магических чисел; ложный комментарий у LODirectoryNode.Flags про
  «протокол не определяет флагов» исправлен

TestULedgerFlagsConformance — ledger-аналог TestUTxFormatConformance:
вендоренный Fixtures/LedgerFormats.h с пином по sha, парсер, падающий громко
на незнакомом LSF_FLAG* и на тощем разборе, диф в обе стороны и требование
регистрировать каждый флагованный объект — новый объект роняет сборку, а не
пропускается молча. Проверен мутациями: неверное значение, удалённый флаг и
незарегистрированный объект дают внятное падение.

Интеграционное покрытие DynamicMPT (XLS-94): AmendmentGuard + TestIDynamicMPT,
4 теста, каждый читает результат из ledger-объекта. Сценарии выверены по
транзактору rippled, а не по документации.

Версия Xrpl поднята до 10.11.0.0 (minor: только добавления в публичном API),
base-пакеты не менялись. CHANGES.md дополнен.

Проверено: nightly-стенд — TestILoan/TestIDynamicMPT/TestIMPToken 41/41 passed;
юнит-тесты 869/869; сборка решения без ошибок.

* ci(protocol-watch): следить за LedgerFormats.h

TestULedgerFlagsConformance сравнивает модели с ПРИПИНЕННОЙ копией
LedgerFormats.h, поэтому сам по себе он не может заметить, что апстрим ушёл
вперёд — этот сигнал даёт protocol-watch. Заголовка в его списке не было, и
это вторая половина причины, по которой lsfMPTAMM прожил несколько релизов:
не только никто не сверял флаги, но и никто не сообщал, что файл изменился.

Заодно .ref фикстуры перестаёт врать: там написано «когда protocol-watch
сообщит об изменении этого файла — замени копию целиком», а вотчер за ним
не следил.

Путь отсутствует в baseline, поэтому первый же прогон отметит заголовок как
изменившийся один раз и внесёт его в baseline наравне с остальными — union
ключей в сравнении это предусматривает.
* feat(protocol): sfLEVersion — версия схемы Vault (rippled #7817)

protocol-watch сообщил 03.08 об изменении sfields.macro и ledger_entries.macro
на develop. Изменение одно: TYPED_SFIELD(sfLEVersion, UINT8, 6) и его появление
в ltVAULT как SoeDefault (rippled #7817, cash-basis accounting для LoanBroker).

Поле помечает, по какой схеме учёта живёт хранилище. Хранилища, созданные до
активации cash-basis, не несут LEVersion вовсе, и rippled трактует его отсутствие
как версию 0 (VaultVersion::Legacy), а не как ошибку — то есть отсутствие
значения осмысленно, это не потеря данных.

* definitions.json + сгенерированный Field.Uint8. Нужны ОБА: definitions.json
  в рантайме не читается, он вход для Tools/GenerateEnums, поэтому поле,
  добавленное только туда, не доезжает никуда. Файл перегенерирован
  инструментом, а не правился руками
* LOVault.LEVersion (uint?, как остальные UInt8-поля этого объекта) и enum
  VaultVersion (Legacy = 0, CashBasis = 1) — имена для значений, которые
  протокол определяет на сегодня
* TestULEVersion_BinaryRoundTrip — именно он показал, что одного definitions.json
  мало: до кодогенерации кодек поля не знал и тест падал
* TestULOVault_LEVersion_Deserialize — обе формы: поле присутствует, и legacy-
  хранилище без него десериализуется в null

Xrpl.BinaryCodec поднят до 10.10.0.0 (minor: добавлено поле). Остальные
base-пакеты не тронуты и сохраняют версии. Xrpl не бампится: 10.11.0.0 ещё
не выпущен и уже описывает модельные правки этой серии.

Проверено: сборка решения без ошибок, юнит-тесты 877/877.

* chore(release): выровнять версию Xrpl.BinaryCodec с Xrpl (10.11.0.0)

Кодек везёт sfLEVersion, то есть меняется вместе с моделями, — держать его на
собственной ветке нумерации незачем: потребитель читает один номер для обоих
пакетов. Вместо следующего минора кодека (10.10.0.0) ставится 10.11.0.0.

10.10.x просто пропускается: последняя опубликованная версия Xrpl.BinaryCodec —
10.9.0, номер 10.10.0.0 на NuGet не выходил, так что ничего не переиспользуется
и не переписывается.

Xrpl.AddressCodec и Xrpl.Keypairs остаются на 10.9.0.0 — они не менялись.
…_entries.macro (10.11.0.0) (#75)

* feat(models)!: гвард полей ledger-объектов и чистка моделей по ledger_entries.macro

Третья и последняя поверхность конформанса: TestUTxFormatConformance держит
поля транзакций, TestULedgerFlagsConformance — флаги ledger-объектов, а поля
самих объектов не проверял никто. ledger_entries.macro — единственное место,
где протокол объявляет состав объекта (definitions.json несёт коды полей и
типы, но не списки), и пропущенное поле симптома не даёт: чтение объекта
проходит, значение просто теряется.

Гвард:
* вендоренный Fixtures/ledger_entries.macro с пином по sha. Пин на develop,
  а не на тег (в отличие от LedgerFormats.h): модели по полям следуют develop,
  и sfLEVersion существует только после 30.07 — тег объявил бы его выдумкой SDK
* диф в обе стороны + обязательная регистрация каждого объекта, поэтому новый
  ledger-объект роняет сборку, а не пропускается
* четыре common-поля rippled (LedgerIndex, LedgerEntryType, Flags, Sponsor из
  LedgerFormats::getCommonFields()) исключаются с обеих сторон, как это уже
  сделано для commonFields в TxFormat-гварде; [JsonIgnore]-свойства не в счёт
* проверен мутацией: переименование JsonPropertyName даёт обе половины отчёта

Удалены свойства, которых нет в протоколе (BREAKING, без [Obsolete] — как при
удалении инертных ConnectionOptions в 10.10.0.0). Значение они держать не могли:
объект собирается по фиксированному SOTemplate. Проверено и на живой ноде, и по
четырём версиям (3.2.1, 3.3.0-b1, 3.3.0-rc1, develop) — нет нигде:
* LOVault.DomainID — positive control: VaultCreate с Data, AssetsMaximum и
  DomainID прошёл, первые два вернулись на объекте, DomainID — нет, он уехал
  на связанный share MPTokenIssuance
* LOLoan.PrincipalRequested — поле транзакции LoanSet; реальный займ хранит
  сумму как PrincipalOutstanding
* LOCredential.OwnerNode — у объекта IssuerNode/SubjectNode; нулевые hint'ы
  сериализуются ("OwnerNode":"0" у Loan), так что отсутствие настоящее
* LONFTokenPage.NFTokenPage, LOAmm.LedgerCurrentIndex, LOAmm.Validated —
  последние два принадлежат конверту ответа amm_info (snake_case)

Починено в LOAmm:
* AMMAccount никогда не десериализовался — поле объекта называется Account,
  а атрибута не было; имя свойства не меняется, вызовы не ломаются
* конструктор ставил LedgerEntryType.AccountRoot вместо AMM

Добавлены PreviousTxnID/PreviousTxnLgrSeq в LOAmm, LOAmendments,
LODirectoryNode, LOFeeSettings, LONegativeUNL.

Xrpl 10.11.0.0 -> 10.12.0.0. Xrpl.BinaryCodec не менялся и остаётся 10.11.0.0.

Проверено: юнит-тесты 879/879; на nightly-стенде TestILoan/TestIVault/
TestICredential/TestINFToken/TestIAmm — 64/64; сборка решения без ошибок.

* chore(release): схлопнуть невыпущенные 10.10.1.0 / 10.11.0.0 / 10.12.0.0 в один 10.11.0.0

На NuGet последняя опубликованная версия Xrpl — 10.10.0. Значит 10.10.1.0
(фикс OnConnected), 10.11.0.0 (флаги ledger-объектов, гвард по LedgerFormats.h,
DynamicMPT, sfLEVersion) и 10.12.0.0 (гвард полей ledger-объектов и чистка
моделей) существуют только в dev и ни один из номеров наружу не уходил.

Три раздела CHANGES.md слиты в один 10.11.0.0 от 08/04/2026, версия пакета
Xrpl откачена с 10.12.0.0 на 10.11.0.0. Потребитель получит один релиз вместо
трёх номеров, из которых два никогда не существовали как артефакты.

Xrpl.BinaryCodec уже стоял на 10.11.0.0 — после схлопывания он совпадает с Xrpl
ровно, как и задумывалось при выравнивании. AddressCodec и Keypairs остаются
на 10.9.0.0: они не менялись.

Проверено: сборка решения без ошибок, юнит-тесты 879/879.

* refactor(review): нитпики CodeRabbit к PR #75

* LONegativeUNL: добавлен using System.Text.Json.Serialization и атрибуты
  сокращены до [JsonPropertyName] — как в LOAmendments, LODirectoryNode,
  LOFeeSettings и LOAmm. Полная форма стояла потому, что файл этот namespace
  не импортировал; теперь импортирует
* RippledLedgerEntryFormats: парсер падает на повторно объявленном объекте
  вместо тихой перезаписи. Индексаторное присваивание позволяло второму
  объявлению вытеснить первое — объект молча уходил из таблицы конформанса,
  а fieldCount продолжал расти, так что порог минимального разбора потерю
  не замечал. Это ровно тот класс тихого отказа, против которого написан
  остальной парсер

Дубликатов имён в текущей фикстуре нет (единственный LEDGER_ENTRY_DUPLICATE —
DepositPreauth, и обычного объявления с этим именем не существует), так что
гвард forward-looking. Проверен мутацией: дублирование блока Ticket даёт
"Ticket: declared twice in ledger_entries.macro"; фикстура после проверки
восстановлена и снова байт-в-байт совпадает с пином.

Проверено: сборка без ошибок, юнит-тесты 879/879.
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 10852354-d105-45db-8d3b-0703bab4c715

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The change aligns XRPL 3.3.0 protocol definitions, ledger models, path encoding, sponsorship, DynamicMPT, autofill fees, connection recovery, JSON conversion, and nightly CI maintenance.

Changes

Protocol alignment and conformance

Layer / File(s) Summary
Protocol contracts and ledger models
Base/Xrpl.BinaryCodec/..., Xrpl/Models/Ledger/*, Xrpl/Models/Transactions/*, Xrpl/Sugar/Autofill.cs
Adds MPT path-step encoding, immutable MPT flags, sponsorship deltas, ledger fields, named flags, Vault versions, and protocol-specific autofill fee rules.
Protocol fixtures and validation
Tests/Xrpl.Tests/Fixtures/*, Tests/Xrpl.Tests/Models/*, Tests/Xrpl.Tests/Integration/transactions/*
Adds pinned ledger definitions, parsers, conformance checks, DynamicMPT integration tests, sponsorship coverage, and updated protocol fixtures.
Documentation and release metadata
CHANGES.md, DocFx/*, Base/Xrpl.BinaryCodec/Xrpl.BinaryCodec.csproj, Xrpl/Xrpl.csproj
Updates release notes, protocol documentation, package versions, and model guidance.

Connection recovery

Layer / File(s) Summary
Connection failure and reconnect state
Xrpl/Client/connection.cs, Xrpl/Client/WebSocketClient.cs
Adds health-check options, reports callback and send failures, bounds handler retries, preserves reconnect-session ownership, and handles failed server changes.
Recovery and test infrastructure
Tests/Xrpl.Tests/Client/*, Tests/Xrpl.Tests/CreateMockRippled.cs, Tests/Xrpl.Tests/MockRippled/Server.cs, Tests/Xrpl.Tests/TestUtils.cs, Tests/TestsClients/Blazor-WebAssembly/Pages/Index.razor
Adds regression coverage for handler failures, reconnect races, unavailable servers, aborted handshakes, mock-server shutdown, port allocation, and URL refresh.

Nightly automation and JSON conversion

Layer / File(s) Summary
Nightly pin maintenance
.ci-config/*, .github/workflows/*, CLAUDE.md
Adds nightly version selection, validation, stand checks, definitions comparison, automated PR creation, and failure notifications.
JSON converter option caching
Xrpl/Client/Json/*, Tests/Xrpl.Tests/Client/Json/*
Caches converter-free serializer options and preserves unknown ledger types as BaseLedgerEntry, with converter and polymorphism tests.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.37% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the release, schema updates, conformance guards, autofill changes, and reconnect hardening.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Xrpl/Models/Ledger/LOLoan.cs (1)

104-104: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Correct the lending guides. PrincipalRequested is a LoanSet transaction field, not a LOLoan field. Remove it from the Loan Fields tables in both language versions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Xrpl/Models/Ledger/LOLoan.cs` at line 104, Remove PrincipalRequested from the
Loan Fields documentation tables in both language versions, keeping it
documented only under LoanSet transaction fields. Update the relevant tables
near the LOLoan documentation without changing other field entries.
🧹 Nitpick comments (2)
Xrpl/Client/connection.cs (1)

2015-2019: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Read _reconnectCts with volatile semantics for the ownership checks.

_reconnectCts is a plain field. StopReconnectLoop, StartReconnectLoop, and RetireCurrentSessionAndReconnectAsync write it from other threads. The three new ReferenceEquals(_reconnectCts, ownCts) checks (Lines 2015, 2104, 2142) now carry ownership semantics, so a stale read can make a retired loop run one extra iteration or make the owning loop exit early.

The other cross-thread flags in this class (_permanentlyDisconnected, _reconnectMode, _isIntentionalDisconnect) are volatile. Apply the same treatment here.

♻️ Proposed change
-    private CancellationTokenSource _reconnectCts;
+    private volatile CancellationTokenSource _reconnectCts;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Xrpl/Client/connection.cs` around lines 2015 - 2019, Declare _reconnectCts
with volatile semantics, matching the existing cross-thread state fields such as
_permanentlyDisconnected and _reconnectMode. Ensure all ownership checks in
StopReconnectLoop, StartReconnectLoop, and RetireCurrentSessionAndReconnectAsync
read the updated value so retired loops cannot continue or owning loops exit
prematurely.
Tests/Xrpl.Tests/Models/RippledLedgerFlags.cs (1)

79-97: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Guard against a duplicate LEDGER_OBJECT name, as the entry parser does.

objects[name] = flags lets a second block with the same name replace the first, while flagCount still grows. The object then disappears from the conformance table and the minimum-count guard at Line 99 stays satisfied. RippledLedgerEntryFormats.Parse throws for this case; keep the two parsers consistent.

♻️ Proposed fix
+                if (objects.ContainsKey(name))
+                {
+                    throw new InvalidOperationException(
+                        $"{name}: declared twice in LedgerFormats.h — the parser would drop one " +
+                        "definition, update it before trusting this test");
+                }
+
                 objects[name] = flags;
                 flagCount += flags.Count;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Tests/Xrpl.Tests/Models/RippledLedgerFlags.cs` around lines 79 - 97, Update
the object parsing loop around objects and flagCount to reject duplicate
LEDGER_OBJECT names before assigning objects[name] or incrementing flagCount,
matching the duplicate-handling behavior of RippledLedgerEntryFormats.Parse.
Preserve the existing skip for objects with no flags and ensure duplicate names
cannot overwrite the first entry or inflate the count.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Tests/Xrpl.Tests/Client/TestUChangeServerFailure.cs`:
- Around line 101-118: Update both free-port allocation sites in
TestUChangeServerFailure so the port remains reserved until StartMock binds it,
or retry with a newly allocated port when binding fails. Preserve the test’s
intended “server starts later” behavior and apply the same race-safe handling at
both locations.

In `@Tests/Xrpl.Tests/Xrpl.Tests.csproj`:
- Around line 36-41: Change the project-local fixture entries for
LedgerFormats.h and ledger_entries.macro from None Include to None Update,
preserving their existing CopyToOutputDirectory metadata. Apply the same Update
form to all nearby project-local fixture entries and do not add entries for .ref
sidecars.

In `@Xrpl/Client/connection.cs`:
- Around line 1784-1792: Preserve the accumulated backoff when the
handler-failure path in the reconnect flow calls StopReconnectLoop and
StartReconnectLoop. Update the relevant reconnect state handling so
_reconnectAttempts is not reset for this restart, or derive CalcBackoff from
_connectHandlerFailures, while retaining normal reset behavior after a
successful connection and existing StopAfterMaxAttempts handling.

---

Outside diff comments:
In `@Xrpl/Models/Ledger/LOLoan.cs`:
- Line 104: Remove PrincipalRequested from the Loan Fields documentation tables
in both language versions, keeping it documented only under LoanSet transaction
fields. Update the relevant tables near the LOLoan documentation without
changing other field entries.

---

Nitpick comments:
In `@Tests/Xrpl.Tests/Models/RippledLedgerFlags.cs`:
- Around line 79-97: Update the object parsing loop around objects and flagCount
to reject duplicate LEDGER_OBJECT names before assigning objects[name] or
incrementing flagCount, matching the duplicate-handling behavior of
RippledLedgerEntryFormats.Parse. Preserve the existing skip for objects with no
flags and ensure duplicate names cannot overwrite the first entry or inflate the
count.

In `@Xrpl/Client/connection.cs`:
- Around line 2015-2019: Declare _reconnectCts with volatile semantics, matching
the existing cross-thread state fields such as _permanentlyDisconnected and
_reconnectMode. Ensure all ownership checks in StopReconnectLoop,
StartReconnectLoop, and RetireCurrentSessionAndReconnectAsync read the updated
value so retired loops cannot continue or owning loops exit prematurely.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 14e721fa-0e54-4587-875e-52fc0c0cff4c

📥 Commits

Reviewing files that changed from the base of the PR and between ff5714d and 713da1a.

⛔ Files ignored due to path filters (1)
  • Base/Xrpl.BinaryCodec/Enums/Field.Uint8.Generated.cs is excluded by !**/*.generated.*
📒 Files selected for processing (36)
  • .ci-config/docker-compose.ci.yml
  • .github/workflows/protocol-watch.yml
  • Base/Xrpl.BinaryCodec/Enums/definitions.json
  • Base/Xrpl.BinaryCodec/Xrpl.BinaryCodec.csproj
  • CHANGES.md
  • Tests/Xrpl.Tests/Client/TestUChangeServerFailure.cs
  • Tests/Xrpl.Tests/Client/TestUOnConnectedHandlerFailure.cs
  • Tests/Xrpl.Tests/CreateMockRippled.cs
  • Tests/Xrpl.Tests/Fixtures/LedgerFormats.h
  • Tests/Xrpl.Tests/Fixtures/LedgerFormats.h.ref
  • Tests/Xrpl.Tests/Fixtures/ledger_entries.macro
  • Tests/Xrpl.Tests/Fixtures/ledger_entries.macro.ref
  • Tests/Xrpl.Tests/Integration/AmendmentGuard.cs
  • Tests/Xrpl.Tests/Integration/transactions/TestIDynamicMPT.cs
  • Tests/Xrpl.Tests/Integration/transactions/TestILoan.cs
  • Tests/Xrpl.Tests/Models/RippledLedgerEntryFormats.cs
  • Tests/Xrpl.Tests/Models/RippledLedgerFlags.cs
  • Tests/Xrpl.Tests/Models/TestULedgerEntryFieldsConformance.cs
  • Tests/Xrpl.Tests/Models/TestULedgerFlagsConformance.cs
  • Tests/Xrpl.Tests/Models/TestUProtocolCompleteness.cs
  • Tests/Xrpl.Tests/Xrpl.Tests.csproj
  • Xrpl/Client/WebSocketClient.cs
  • Xrpl/Client/connection.cs
  • Xrpl/Models/Ledger/LOAmendments.cs
  • Xrpl/Models/Ledger/LOAmm.cs
  • Xrpl/Models/Ledger/LOCredential.cs
  • Xrpl/Models/Ledger/LODirectoryNode.cs
  • Xrpl/Models/Ledger/LOFeeSettings.cs
  • Xrpl/Models/Ledger/LOLoan.cs
  • Xrpl/Models/Ledger/LOMPToken.cs
  • Xrpl/Models/Ledger/LOMPTokenIssuance.cs
  • Xrpl/Models/Ledger/LONFTokenPage.cs
  • Xrpl/Models/Ledger/LONegativeUNL.cs
  • Xrpl/Models/Ledger/LOSignerList.cs
  • Xrpl/Models/Ledger/LOVault.cs
  • Xrpl/Xrpl.csproj
💤 Files with no reviewable changes (2)
  • Xrpl/Models/Ledger/LONFTokenPage.cs
  • Xrpl/Models/Ledger/LOCredential.cs

Comment on lines +101 to +118
int secondPort = TestUtils.GetFreePort(); // nothing is listening there yet

try
{
await _client.connection.ChangeServer($"ws://127.0.0.1:{secondPort}");
}
catch (Exception)
{
// Expected - the target is not up yet. What matters is the state it leaves behind.
}

Assert.AreNotEqual(
XrpConnectionState.Disconnected,
_client.connection.CurrentConnectionState,
"A ChangeServer target that is down is a connection failure, not a permanent disconnect.");

// The server appears afterwards - exactly the "start the node later" case.
_secondRippled = StartMock(secondPort);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate and inspect the free-port helper used by these tests.
fd -i 'TestUtils.cs' --exec cat -n {}
rg -nP -C4 '\bGetFreePort\s*\(' --type=cs

Repository: StaticBit-io/XrplCSharp

Length of output: 2573


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- target test file ---'
fd -i 'TestUChangeServerFailure.cs' --exec sh -c 'cat -n "$1"' sh {}
printf '%s\n' '--- mock startup definitions and calls ---'
rg -n -P -C8 '\b(StartMock|GetFreePort|ChangeServer|TestChangeServerAfterUserDisconnectStillReconnects)\b' --type=cs Tests

Repository: StaticBit-io/XrplCSharp

Length of output: 40908


Prevent the free-port race.

TestUtils.GetFreePort stops its listener before returning, so another process can claim secondPort before StartMock binds it. Preserve the listener through startup or retry allocation when binding fails. Apply this at Lines 101 and 158.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Tests/Xrpl.Tests/Client/TestUChangeServerFailure.cs` around lines 101 - 118,
Update both free-port allocation sites in TestUChangeServerFailure so the port
remains reserved until StartMock binds it, or retry with a newly allocated port
when binding fails. Preserve the test’s intended “server starts later” behavior
and apply the same race-safe handling at both locations.

Comment thread Tests/Xrpl.Tests/Xrpl.Tests.csproj Outdated
Comment thread Xrpl/Client/connection.cs Outdated
…abbit к релизному PR #76 (#77)

* fix(client): бэкофф при повторных сбоях OnConnected + замечания CodeRabbit к PR #76

* Бэкофф не рос при повторяющихся сбоях OnConnected-хендлера. Путь
  OnConnectHandlerFailedAsync сносит цикл переподключения и запускает заново на
  каждом сбое; StopReconnectLoop обнуляет _reconnectAttempts, свежая
  последовательность обнуляет его ещё раз, а CalcBackoff считает задержку только
  по этому счётчику. При StopAfterMaxAttempts = false ветки сдачи нет вовсе, и
  клиент бесконечно повторял connect -> сбой хендлера -> teardown с постоянной
  ReconnectBaseDelay — устойчивая нагрузка ровно на ту ноду, которая ещё не
  умеет обслуживать запросы. StartReconnectLoop получил параметр начального
  значения счётчика, путь сбоя хендлера засевает его своим числом
  последовательных сбоев. TestRepeatedOnConnectedFailuresBackOff это пинит:
  с откаченным фиксом тест показывает ~100 переподключений за 20 с с ровным
  интервалом ~200 мс
* _reconnectCts объявлен volatile: цикл сравнивает его по ссылке, решая, владеет
  ли он ещё состоянием переподключения, а пишут его три метода из других
  потоков. Устаревшее чтение дало бы retired-циклу лишнюю итерацию либо увело
  бы владеющий цикл раньше времени. Соседние кросс-потоковые поля уже volatile
* Гайд по кредитованию (обе языковые версии): таблица Loan Fields перечисляла
  четыре имени, которых у ledger-объекта нет — Account (заёмщик лежит в
  Borrower), а также Counterparty, PrincipalRequested и PaymentTotal, которые
  являются полями транзакции LoanSet. После удаления PrincipalRequested из
  LOLoan гайд обещал бы несуществующее свойство
* TestUtils.GetFreePort больше не выдаёт один порт дважды в пределах процесса:
  ОС вправе вернуть только что освобождённый порт, классы тестов идут
  параллельно, и второй mock падал бы при бинде в фоновом потоке — это выглядело
  как таймаут, а не как конфликт. TestUChangeServerFailure дополнительно
  проверяет порт прямо перед стартом второго mock, чтобы остаточная внешняя
  гонка падала внятно
* RippledLedgerFlags.Parse падает на повторно объявленном объекте, как это уже
  делает RippledLedgerEntryFormats.Parse. Проверено мутацией (дублирование
  блока Offer), фикстура после проверки восстановлена
* Фикстуры в тестовом .csproj подключены через None Update вместо None Include —
  дефолтный glob SDK их уже включает

Версия не бампится: 10.11.0.0 ещё не выпущен, правки дописаны в его раздел.

Проверено: сборка решения без ошибок, юнит-тесты 880/880; обе вендоренные
фикстуры сверены с пинами через curl | diff.

* fix(tests): ловить дубликат ledger-объекта до пропуска бесфлаговых

Замечание CodeRabbit к PR #77: проверка дубликата стояла ПОСЛЕ
`if (flags.Count == 0) continue`, поэтому имя, объявленное дважды, проскакивало,
если одно из объявлений разбиралось без флагов. Имена теперь отслеживаются
отдельным HashSet до этой ветки, а `objects` по-прежнему хранит только
флагованные записи.

Проверено мутацией именно этого сценария: вставка второго `LEDGER_OBJECT(Offer, )`
с пустым телом теперь даёт "Offer: declared twice in LedgerFormats.h", а до
правки проходила молча. Фикстура после проверки восстановлена и сверена с пином
через curl | diff.

Побочно всплыло, что PreserveNewest не обновляет копию фикстуры в bin, когда
исходник возвращают из git (у восстановленного файла время правки старше копии):
после мутационных проверок каталог Fixtures в bin нужно удалять, иначе тесты
идут против подделанного файла. На этом и попались два прогона.

Проверено: юнит-тесты 880/880.
)

* fix(client): синхронизировать жизненный цикл сессии переподключения

volatile делает атомарным каждое отдельное обращение, но не их последовательность.
StopReconnectLoop читал _reconnectCts трижды подряд — Cancel, Dispose, = null —
и старт, попавший между этими обращениями, получал свой свежий источник
уничтоженным уходящим стопом. Цикл оставался с мёртвым источником, отступал по
проверке владения, а замену никто не запускал: клиент вставал навсегда — ровно
тот исход, против которого писалась вся эта область.

Введён _reconnectStateLock, накрывающий каждое чтение-с-изменением тройки
_reconnectCts / _reconnectLoop / _reconnectAttempts во всех четырёх точках
записи: RetireCurrentSessionAndReconnectAsync (замена сессии и её же успешное
завершение), StopReconnectLoop, StartReconnectLoop и хвост ReconnectLoopAsync.
Решение «цикл уже идёт / источник переиспользуем / ставим новый / отдаём его
циклу» стало одной транзакцией — иначе гонка давала два живых цикла либо цикл
с уже уничтоженным источником.

Под локом не выполняется ничего, что может позвать потребительский код: отмена и
освобождение отставленного источника вынесены за лок, а ReconnectLoopAsync
начинается с await Task.Yield(), поэтому старт цикла под локом только планирует
его и возвращается — уведомление не выполняется на стеке вызывающего. Без этого
Disconnect из обработчика мог бы войти в тот же лок и заклинить.

volatile у поля сохранён: проверки владения в цикле читают его вне лока, а
одиночное чтение ссылки атомарно и ничего не изменяет.

TestUReconnectSessionRaces — и сразу оговорка: эти тесты НЕ воспроизводят
исходную гонку. Окно шириной в несколько инструкций из публичного API, где между
вызовами лежат целые await'ы, не достаётся: с убранным локом тесты по-прежнему
проходят (проверено мутацией, три прогона). Ценность у них обратная — сам лок
создаёт риск взаимной блокировки, и они гоняют конкурентные ChangeServer и
Disconnect, требуя, чтобы клиент затем дошёл до живого сервера. Дедлок или
потерянная сессия проявятся здесь, а не в бою.

Проверено: юнит-тесты 882/882; TestIConnectionStates 7/7 на CI-стенде; сборка
решения без ошибок.

* fix(client): не читать Token после yield и перезапускать цикл одной транзакцией

Селф-ревью предыдущего коммита нашло два дефекта, один из которых внесён им же.

* ObjectDisposedException в ReconnectLoopAsync. Добавленный await Task.Yield()
  стоял ПЕРЕД чтением ownCts.Token, а Cancel/Dispose отставленного источника
  вынесены за лок — значит за время ожидания продолжения другой поток успевал
  уничтожить именно этот источник, и обращение к .Token бросало
  ObjectDisposedException вне всякого try. Таск падал, не сделав ни одной
  попытки переподключения, а исключение терялось как unobserved. До прошлого
  коммита Token читался синхронно, так что окна не было вовсе — оно возникло
  ровно из-за yield. Теперь Token читается ДО yield, пока вызывающий ещё держит
  лок и источник заведомо жив; Task.Delay(delay, ct) дополнительно ловит
  ObjectDisposedException и трактует его как отставку, а не как сбой
* RestartReconnectLoop. Последовательность StopReconnectLoop(); _reconnectLoop =
  null; StartReconnectLoop(seed) брала лок дважды, а запись между вызовами шла
  без лока. Конкурентный старт из OnceClose или OnConnectionFailed успевал
  поставить свой цикл, после чего засеянный старт видел живой цикл и молча
  выходил, не применив seed — тихий откат роста бэкоффа, ради которого seed и
  вводился. Retire, установка нового источника, счётчика и запуск цикла делаются
  одной транзакцией под локом
* XML-док у _reconnectStateLock больше не утверждает, что лок покрывает «every
  read-modify-write»: перечислены места, которые он реально охватывает, и явно
  названы оставшиеся вне его (per-iteration инкремент, сбросы в ChangeServer и
  OnceClose)

Проверено мутацией: если в RestartReconnectLoop не применять seed,
TestRepeatedOnConnectedFailuresBackOff падает и показывает ~90 переподключений
подряд с ровным интервалом ~200 мс. Юнит-тесты 882/882.

* fix(demo): показывать фактический сервер после неудачной смены

Blazor-демо обновляло CurrentServerUrl только при успешном ChangeServer:
присваивание стояло в try сразу после await. ChangeServer переключает цель
клиента ДО попытки подключения, поэтому при переходе на недоступный сервер он
бросает по таймауту получения соединения — а клиент к этому моменту уже
переподключается к НОВОМУ адресу. Метка же продолжала показывать прежний
сервер.

Дальше ломалась и кнопка: гвард "Already connected to this server" сравнивал
выбранный адрес с этим устаревшим значением и отказывался переключаться обратно
на сервер, к которому клиент на самом деле подключён не был.

Присваивание перенесено в finally, то есть выполняется независимо от исхода.
SDK тут ни при чём: connection.GetUrl()/client.Url() возвращает новый адрес
сразу после ChangeServer.

Проверено вручную в демо против локальной ноды 3.2.1: переключение на
ws://127.0.0.1:19997 (порт мёртв) — метка показывает 19997, а не прежний
mainnet; последующее переключение на ws://localhost:6006 проходит и
подключается за 0.3 с, тогда как раньше отбивалось сообщением
"Already connected to this server".

* fix(client): снимать ссылку на цикл внутри стоп-транзакции

Замечание CodeRabbit к PR #78 (Major). StopReconnectLoop обнулял _reconnectCts,
но оставлял _reconnectLoop. Отставленный цикл выходит асинхронно — он узнаёт о
потере владения только на следующей проверке, — поэтому оставшаяся ссылка даёт
такую последовательность: StartReconnectLoop видит !IsCompleted и возвращается,
ничего не запустив, а отставленный цикл тут же отступает по проверке владения.
Не остаётся никого, кто бы переподключался. Достижимо, когда Connect или
ChangeServer гасит живой цикл, а новое подключение падает. Ссылка снимается в
той же транзакции под локом.

Нитпики оттуда же:
* TestUReconnectSessionRaces: финальный Connect обёрнут, чтобы падал ассерт с
  внятным текстом, а не сырое исключение от проигравшего гонку переключения
* StartMock вызывает mock.Start() напрямую: он биндит, слушает и уходит в
  BeginAccept, не блокируя, так что возврат из него уже означает готовность
  порта. Обёртка в поток лишь открывала окно, где тест успевал подключиться
  раньше mock'а. В соседнем TestUChangeServerFailure обёртка осталась —
  отдельная уборка, этот PR его не трогает

TestFailedConnectDuringReconnectLeavesLoopRunning закрывает функциональный путь
(Connect во время реконнекта, сервер появляется позже). Гонку он НЕ пинит: нужно,
чтобы отставленная задача была ещё жива в момент проверки IsCompleted, а к
приходу неудачного Connect она обычно уже вышла — с откаченным фиксом тест
проходит. Это записано в его XML-доке, чтобы его не приняли за регрессионный.

Проверено: юнит-тесты 883/883.
@Platonenkov

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
Tests/Xrpl.Tests/Client/TestUReconnectSessionRaces.cs (1)

234-268: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

laterPort is held across several seconds before it is bound; use the new IsPortStillFree guard.

TestUtils.GetFreePort() at Line 234 stops its listener before it returns, so the port is closed. StartMock(laterPort) at Line 268 binds it only after a ChangeServer and a Connect that runs to a 3s acquisition timeout. In that window another process can take the port. StartMock then throws SocketException and the test reports a bind failure instead of the reconnect assertion it is written for.

TestUtils.IsPortStillFree was added in this PR for exactly this case. This test does not call it.

♻️ Proposed guard
             // The server appears. Nobody touches the client from here on.
+            Assert.IsTrue(
+                TestUtils.IsPortStillFree(laterPort),
+                $"Port {laterPort} was taken by another process while the test held it; " +
+                "the reconnect assertion below cannot be evaluated.");
             _secondRippled = StartMock(laterPort);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Tests/Xrpl.Tests/Client/TestUReconnectSessionRaces.cs` around lines 234 -
268, Use TestUtils.IsPortStillFree to guard laterPort after the delayed
connection attempt and before StartMock(laterPort), asserting that the port
remains available. Keep the existing reconnect scenario and StartMock flow
unchanged otherwise.
Xrpl/Client/connection.cs (1)

2053-2098: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The initialAttempts parameter is no longer used, and its documentation now describes a path that calls a different method.

OnConnectHandlerFailedAsync calls RestartReconnectLoop(initialAttempts: failures) at Line 1839. The three StartReconnectLoop() call sites (Line 689, Line 1537, Line 1999) all use the default 0. The doc comment at Line 2053-2059 still attributes the parameter to "The OnConnected-handler path", which is now inaccurate.

The seed also has a silent failure mode for any future caller: on the hasValidPreCreatedCts branch at Line 2096 the value is discarded without notice.

Remove the parameter, or keep it and document that it applies only when a fresh source is created.

♻️ Proposed simplification
-    /// <param name="initialAttempts">
-    /// Value to seed <c>_reconnectAttempts</c> with when a fresh reconnect sequence starts.
-    /// Defaults to 0 — a genuine new sequence begins at the base delay. The OnConnected-handler
-    /// path passes its own consecutive-failure count instead: that path tears the loop down and
-    /// starts it again on every failure, so with a 0 seed the backoff would restart at
-    /// ReconnectBaseDelay each time and never grow.
-    /// </param>
-    private void StartReconnectLoop(int initialAttempts = 0)
+    /// <summary>
+    /// Starts a reconnect loop if none is running. A seeded restart is done by
+    /// <see cref="RestartReconnectLoop"/> instead.
+    /// </summary>
+    private void StartReconnectLoop()
     {
                 retired = existingCts;
                 _reconnectCts = new CancellationTokenSource();
-                _reconnectAttempts = initialAttempts;
+                _reconnectAttempts = 0;
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Xrpl/Client/connection.cs` around lines 2053 - 2098, Remove the unused
initialAttempts parameter and its associated documentation from
StartReconnectLoop, then update its callers to invoke the parameterless method.
Ensure reconnect attempt initialization remains handled by the fresh-CTS path
and no seed value is silently discarded when reusing a pre-created CTS.
Tests/TestsClients/Blazor-WebAssembly/Pages/Index.razor (1)

731-737: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use client.Url() consistently for the server URL.

IXrplClient.Url() delegates to connection.GetUrl(). Replace the direct client.connection.GetUrl() calls at lines 400, 620, and 630 with client.Url().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Tests/TestsClients/Blazor-WebAssembly/Pages/Index.razor` around lines 731 -
737, Replace the direct client.connection.GetUrl() calls in the server URL
handling paths around Index.razor with client.Url(), including the usages near
lines 400, 620, and 630. Use IXrplClient.Url() consistently while preserving the
existing server-switch and comparison behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Tests/Xrpl.Tests/Client/TestUOnConnectedHandlerFailure.cs`:
- Around line 301-310: Update the comments above the gap assertions to account
for CreateClient’s 1-second ReconnectMaxDelay cap and the handler-failure
attempts used by CalcBackoff: describe the first-to-last range as approximately
2.5x at jitter extremes rather than 4x, and explicitly note that the assertion
depends on the configured cap remaining above the earlier backoff values.

In `@Xrpl/Client/connection.cs`:
- Around line 666-676: Update the fast-reconnect success cleanup in OnceOpen to
retain the CancellationTokenSource created or installed by this method and,
under _reconnectStateLock, clear and reset _reconnectAttempts only when
_reconnectCts still references that owned source. If a newer source was
installed by RestartReconnectLoop, leave it untouched and do not cancel or
dispose it.

---

Nitpick comments:
In `@Tests/TestsClients/Blazor-WebAssembly/Pages/Index.razor`:
- Around line 731-737: Replace the direct client.connection.GetUrl() calls in
the server URL handling paths around Index.razor with client.Url(), including
the usages near lines 400, 620, and 630. Use IXrplClient.Url() consistently
while preserving the existing server-switch and comparison behavior.

In `@Tests/Xrpl.Tests/Client/TestUReconnectSessionRaces.cs`:
- Around line 234-268: Use TestUtils.IsPortStillFree to guard laterPort after
the delayed connection attempt and before StartMock(laterPort), asserting that
the port remains available. Keep the existing reconnect scenario and StartMock
flow unchanged otherwise.

In `@Xrpl/Client/connection.cs`:
- Around line 2053-2098: Remove the unused initialAttempts parameter and its
associated documentation from StartReconnectLoop, then update its callers to
invoke the parameterless method. Ensure reconnect attempt initialization remains
handled by the fresh-CTS path and no seed value is silently discarded when
reusing a pre-created CTS.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 188c3bcc-f2b3-428a-9adb-ab30182b5d2b

📥 Commits

Reviewing files that changed from the base of the PR and between 713da1a and f3d5d61.

📒 Files selected for processing (11)
  • CHANGES.md
  • DocFx/LendingProtocol-Guide.md
  • DocFx/LendingProtocol-Guide.ru.md
  • Tests/TestsClients/Blazor-WebAssembly/Pages/Index.razor
  • Tests/Xrpl.Tests/Client/TestUChangeServerFailure.cs
  • Tests/Xrpl.Tests/Client/TestUOnConnectedHandlerFailure.cs
  • Tests/Xrpl.Tests/Client/TestUReconnectSessionRaces.cs
  • Tests/Xrpl.Tests/Models/RippledLedgerFlags.cs
  • Tests/Xrpl.Tests/TestUtils.cs
  • Tests/Xrpl.Tests/Xrpl.Tests.csproj
  • Xrpl/Client/connection.cs
🚧 Files skipped from review as they are similar to previous changes (1)
  • Tests/Xrpl.Tests/Client/TestUChangeServerFailure.cs

Comment thread Tests/Xrpl.Tests/Client/TestUOnConnectedHandlerFailure.cs Outdated
Comment thread Xrpl/Client/connection.cs
Platonenkov and others added 3 commits August 12, 2026 13:18
…ии (#79)

* fix(client): не трогать чужой источник отмены в быстром переподключении

Замечания CodeRabbit к релизному мержу. Все пять проверены по актуальному коду,
все оказались в силе.

* Ветка успеха RetireCurrentSessionAndReconnectAsync безусловно забирала
  _reconnectCts и диспозила его. За время await ConnectInternalAsync и
  WaitForConnectionAsync конкурентный путь (RestartReconnectLoop из падающего
  OnConnected-хендлера) успевает установить НОВЫЙ источник — и его отменяли
  вместе с обнулением счётчика, оставляя новую последовательность без источника.
  Метод теперь запоминает созданный им самим источник и снимает состояние только
  если _reconnectCts всё ещё ссылается на него — так же, как проверки владения
  в ReconnectLoopAsync
* StartReconnectLoop: параметр initialAttempts стал мёртвым после появления
  RestartReconnectLoop (все три вызова шли без аргумента), удалён вместе с
  документацией; свежая последовательность начинается с нуля явно
* TestUOnConnectedHandlerFailure: комментарий к ассерту роста бэкоффа врал.
  С ReconnectBaseDelay 100 мс и ReconnectMaxDelay 1 с задержки идут 400, 800,
  1000 мс — третья упирается в cap, то есть первый-к-последнему это ~2.5x, а не
  4x. Добавлено, что ассерт держится лишь пока cap выше более ранних значений:
  при cap 400 мс все интервалы легли бы на него и сравнялись
* TestUReconnectSessionRaces: laterPort выдаётся до await'ов, поэтому перед
  StartMock добавлена проверка IsPortStillFree — иначе занятый порт проявился бы
  таймаутом финального ассерта, а не внятным сообщением
* Blazor-демо: client.connection.GetUrl() заменён на client.Url() во всех трёх
  оставшихся местах — интерфейсный метод, и он же уже использовался в правке
  смены сервера

Гонку из первого пункта тестом не воспроизвести: нужно попасть в окно между
await'ами, а через публичный API оно недостижимо — с убранной проверкой владения
все тесты переподключения проходят (проверено мутацией). Правка обоснована тем
же инвариантом, что и остальные проверки владения в этом файле.

Проверено: сборка решения без ошибок, юнит-тесты 883/883.

* fix(client): снимать ссылку на цикл и в ветке успеха быстрого переподключения

Селф-ревью PR #79 тремя независимыми проходами (Opus) — нашлось одно
блокирующее и четыре неточности в моих же комментариях.

Блокирующее: ветка успеха RetireCurrentSessionAndReconnectAsync забирала
_reconnectCts, но оставляла _reconnectLoop — та же дыра, которую PR #78 закрыл в
StopReconnectLoop, и здесь я её не заметил. Метод обнуляет _reconnectLoop на
входе, поэтому за время await'ов конкурентный OnConnectionFailed видит «цикла
нет», а StartReconnectLoop переиспользует ещё валидный ownCts и ставит на него
новый цикл. Дальше ветка успеха отменяла источник, но ссылку на задачу
оставляла: все проверки loopIsRunning видели выходящую задачу и отступали, а сам
цикл вставал по проверке владения. Переподключаться было некому. Ссылка теперь
снимается в той же транзакции.

Неточности комментариев, все мои:
* обоснование guard'а порта было скопировано из TestUChangeServerFailure, где
  StartMock уходит в фоновый поток. В этом файле он теперь синхронный (я сам так
  сделал по прошлому нитпику), поэтому занятый порт даёт SocketException сразу,
  а не таймаут ассерта. Плюс отмечено, что guard — диагностика: он сам биндит и
  отпускает, TOCTOU остаётся
* XML-док StartReconnectLoop обещал «always begins at the base delay». Счётчик
  свежей последовательности равен нулю, инкремент идёт до расчёта, значит первая
  задержка — CalcBackoff(1), то есть вдвое больше базовой; а на путях
  ping-timeout и network-drop первая попытка вообще без задержки
* remarks у _reconnectStateLock перечисляли известные исключения, но пропускали
  предпроверки «цикл уже идёт?» в OnConnectionFailed и OnceClose — они читают
  невалатильный _reconnectLoop вне лока
* комментарий в catch-ветке утверждал, что источник заведомо на месте и будет
  переиспользован, — ровно то допущение, которое ветка успеха теперь объявляет
  неверным. Переписан с разбором обоих исходов
* добавлена явная фраза, что при потере владения ownCts освобождать не нужно:
  его отменил и задиспозил тот, кто вытеснил

Проверено: сборка решения без ошибок, юнит-тесты 883/883. Живой прогон Blazor-демо
против локальной ноды 3.2.1 — обрыв соединения (нода убита при активном клиенте,
код 1005) даёт восстановление следующей же попыткой после подъёма ноды,
отключение/подключение, переключения на мёртвые порты и возврат на живой
проходят, стрим ledger-ов идёт, необработанных исключений в консоли нет.

* fix(client): не терять цикл на исключении потребителя и не оживлять после Disconnect

Два предсуществующих дефекта в RetireCurrentSessionAndReconnectAsync, найденные
селф-ревью PR #79. Оба в том же методе, чей жизненный цикл этот PR и приводит в
порядок, поэтому закрываются здесь же.

1. Исключение из потребительского обработчика уносило сессию с собой.
   SetConnectionState синхронно зовёт OnConnectionStatus, а метод исполняется
   только на ping-путях, где вызывающие глушат всё. Бросок из обработчика вылетал
   из метода, оставляя установленный источник отмены без цикла и без владельца:
   клиент замирал в RestoringConnection, а CheckIfNotConnected по непустому
   _reconnectCts считал, что попытка идёт, из-за чего WaitForConnectionAsync
   выжигал весь ConnectionAcquisitionTimeout. Обе нотификации обёрнуты.
   В catch-ветке вдобавок изменён порядок: StartReconnectLoop вызывается ДО
   уведомления — без цикла клиент не вернётся, а сообщение вторично.

2. Метод оживлял клиент после пользовательского Disconnect(). Disconnect ставит
   _permanentlyDisconnected, чистит состояние переподключения и ждёт ping-задачу —
   ту самую, внутри которой крутится этот метод, то есть он ещё не завершился.
   Метод же безусловно сбрасывал флаг и поднимал новую сессию: Disconnect
   возвращался с отчётом об успехе, пока за ним собиралось живое соединение.
   Теперь флаг проверяется перед сбросом: если пользователь отключился, метод
   освобождает свой источник (по проверке владения) и уходит. Та же проверка
   добавлена в catch-ветку перед запуском цикла.

Тестами эти пути не покрыты, и это не оговорка: все три вызова метода — на
ping-путях, а во всех юнит-тестах клиента стоит UseCustomPing = false, то есть
метод в тестах не исполняется вовсе. Тест возможен (UseCustomPing = true плюс мок,
не отвечающий на ping), но интервал пинга захардкожен 20 с в трёх местах и
конфигом не управляется, так что прогон вышел бы на 25-40 с. Дешёвый тест
появится, если интервал сделать настраиваемым — отдельная задача.

Проверено: сборка решения без ошибок, юнит-тесты 883/883.

* refactor(client): вынести интервал и порог health-check в конфиг

Проверка живости соединения жила на двух захардкоженных числах: интервал таймера
20 с (в трёх местах — обычный Timer и WASM-таймер) и порог неактивности 60 с.
Теперь это HealthCheckInterval и InactivityTimeout в ConnectionOptions, значения
по умолчанию прежние.

Мотивация — тестируемость fast-reconnect. Все вызовы
RetireCurrentSessionAndReconnectAsync стоят за этим таймером, поэтому путь,
правленный в трёх PR подряд (#72, #78, #79), ни разу не исполнялся ни в одном
тесте: во всех тестах клиента UseCustomPing = false. С настраиваемыми порогами он
достижим за миллисекунды вместо минуты.

Тесты этим коммитом НЕ добавлены, и вот почему: воспроизвести обрыв на
CreateMockRippled не удалось. mock.Stop() гасит listener, но клиентский
ClientWebSocket остаётся в состоянии Open — он узнаёт о смерти пира только на
следующем вводе-выводе, а ошибка keepalive-пинга состояние не меняет. Пробовал
паузы до 8 с: клиент так и не входит в RestoringConnection, то есть тест зелёный
на несработавшем сценарии — хуже, чем никакого. Ветка порога неактивности тоже
недостижима: мок отвечает даже на неизвестные команды, поэтому активность
обновляется каждый тик и порог не переступается.

Чтобы покрыть путь по-настоящему, нужно одно из двух: научить мок обрывать
соединение (сейчас у него только Start/Stop) или поставить в тестах TCP-прокси
между клиентом и сервером и рвать его. Обе опции — отдельная задача; эти два
свойства делают её выполнимой и попутно убирают магические числа.

Проверено: сборка решения без ошибок, юнит-тесты 883/883.

* fix(client): гасить исключения нотификации в одной точке и валидировать health-check

Три замечания CodeRabbit к PR #79.

1. (Major) Исключение обработчика внутри ReconnectLoopAsync. В прошлом коммите я
   обернул нотификации в fast-reconnect, но сам цикл переподключения тоже зовёт
   SetConnectionState — перед первой попыткой соединения. Бросок из
   OnConnectionStatus ронял задачу цикла, оставляя _reconnectCts установленным без
   живого цикла: восстанавливать соединение становилось некому. Латать по местам
   бессмысленно — защита перенесена в саму SetConnectionState, через которую
   проходят все уведомления класса. Две локальные обёртки убраны как избыточные;
   обработчик потребителя больше не может уронить машину состояний ниоткуда.

2. (Major, частично) Пользовательский Disconnect против летящего fast reconnect.
   ConnectInternalAsync и WaitForConnectionAsync теперь получают токен сессии,
   которой владеет метод: Disconnect отменяет её, и попытка прекращается вместо
   открытия сокета за спиной у отключённого клиента. Проверки флага для этого
   недостаточно — Disconnect ждёт ping-задачу считанные секунды, а получение
   соединения может длиться дольше. Полностью замечание не закрыто: остаётся
   принципиальный вопрос, вправе ли автоматические пути вообще сбрасывать
   _permanentlyDisconnected, — это отдельная задача, здесь взято то, что
   устраняет главное окно.

3. (Minor) Валидация новых опций. HealthCheckInterval на WASM-пути кастится в int
   миллисекунд: ноль выстреливает один раз и больше не повторяется, значения вне
   диапазона таймер отвергает сам. Оба свойства проверяются в ValidateConfig, то
   есть в конструкторе Connection, с указанием имени опции. TestUHealthCheckOptions
   покрывает границы: ноль, отрицательное, за пределами int.MaxValue,
   неположительный InactivityTimeout, и приёмку 1 мс и значений по умолчанию.

Проверено: сборка решения без ошибок, юнит-тесты 888/888.
* feat(sugar): комиссии LoanSet/LoanPay/ConfidentialMPT в Autofill

Три транзактора со специальной базовой комиссией считались как обычные,
и все три — в сторону НЕДОплаты: комиссия ниже требуемого минимума
отклоняется с telINSUF_FEE_P, а не дотягивается сетью.

LoanSet: плоское baseFee*2 верно только при одиночной подписи контрагента.
LoanSet::calculateBaseFee берёт по одной базовой комиссии за каждую запись
CounterpartySignature.Signers. Если подпись уже проставлена — считаем её
записи напрямую; если нет (обычный порядок в autofill) — запрашиваем
signer list контрагента, как это делает xrpl.js.

LoanPay: LoanPay::calculateBaseFee умножает ВЕСЬ Transactor-костыль,
включая подписи, на один инкремент за каждые kLoanPaymentsPerFeeIncrement (5)
платежей, с потолком kLoanMaximumPaymentsPerTransaction/5 (20). Оценка читает
объект Loan, считает платёж как roundPeriodicPayment(PeriodicPayment, LoanScale)
+ LoanServiceFee и делит на него Amount транзакции. Все ветки, где rippled
возвращает normalCost, воспроизведены: tfLoanFullPayment/tfLoanLatePayment,
PaymentRemaining <= 5 и недоступный объект Loan.

ConfidentialMPT (5 типов): Transactor::calculateBaseFee вызывается с
kConfidentialFeeMultiplier = 9 — десять базовых комиссий за одиночно
подписанную транзакцию. Автозаполнялись одной, т.е. в 10 раз меньше.

12 юнит-тестов на новые формулы; весь набор TestU (895) зелёный.

* fix(sugar): current-леджер и защита от переполнения в комиссиях займов

Замечания CodeRabbit к PR #81.

account_info контрагента LoanSet и ledger_entry объекта Loan запрашивались
на validated-леджере. SignerListSet или LoanSet, попавшие в последний леджер,
там ещё не видны: в первом случае комиссия считается по одной подписи вместо
списка, во втором объект Loan выглядит недоступным и множитель платежей
теряется — обе ошибки в сторону недоплаты. Оба запроса переведены на current,
как это уже делает SetNextValidSequenceNumber.

Проверка потолка платежей масштабировала regularPayment вверх: PeriodicPayment
у верхней границы decimal ронял autofill с OverflowException. Сравнение
переставлено на деление amount на константу, которое переполниться не может.

Кламп оценки платежей по PaymentRemaining, предложенный в том же ревью,
не применён: LoanPay::calculateBaseFee читает sfPaymentRemaining только как
short-circuit <= 5 и не ограничивает им оценку. Кламп дал бы 2 инкремента там,
где сеть требует 20, то есть недоплату и telINSUF_FEE_P.
Зафиксировано тестом FewPaymentsRemainingWithLargeAmount.
…ина nightly (#83)

* ci: bump standalone stand to rippled 3.3.0

* feat(models)!: ImmutableFlags и дельты SponsorshipSet по схеме rippled 3.3.0

Стенд перешёл на релиз 3.3.0, что включает Sponsor, DynamicMPT, BatchV1_1,
PermissionDelegationV1_1 и ConfidentialTransfer на генезисе — 44 теста, ранее
скипавшихся через AmendmentGuard, начали выполняться, и 17 из них упали: обе
фичи были реализованы против nightly-сборки 3.3.0~b1 от 11.07.2026, а до релиза
upstream поменял их схему.

DynamicMPT:
- sfMutableFlags переименовано в sfImmutableFlags (тот же UInt32 nth 53, те же
  значения битов) с инверсией смысла: установленный бит означает «заморожено»,
  а не «разрешено менять». Выпуск без поля теперь полностью изменяемый —
  противоположность прежнему поведению
- MPTokenIssuanceCreateMutableFlags и MPTokenIssuanceSetMutableFlags заменены
  общим MPTokenIssuanceImmutableFlags (tif* = lsif*)
- включение возможности переехало из отдельного поля в флаги транзакции:
  MPTokenIssuanceSetFlags пополнен tfMPTSet* (0x04–0x100)

Sponsor:
- SponsorshipSet принимает FeeAmountDelta (Amount 34) и RemainingOwnerCountDelta
  (Int32 2) вместо абсолютных FeeAmount/RemainingOwnerCount, которые остались
  полями только у объекта Sponsorship. Старые поля нода отвергает на разборе:
  STObject::applyTemplate → «Field 'FeeAmount' found in disallowed location»
- клиентская валидация повторяет preflight: дельты ненулевые, FeeAmountDelta в
  XRP, при tfDeleteObject модификационные поля запрещены
- добавлен Common.TryGetInt32 для знаковой дельты

Фикстуры гвардов перепиныны на тег 3.3.0 (transactions.macro, LedgerFormats.h);
ledger_entries.macro остаётся на develop из-за sfLEVersion. RippledLedgerFlags
научился читать lsif*-константы, которые в 3.3.0 объявлены вне LEDGER_OBJECT, и
падает, если не нашёл ни одной.

Пин nightly-стенда сдвинут на 3.4.0~b0+202608111815.26cc683e, конфиг
перегенерирован с того же ref: Definitions Watch поднимает стенд именно из него,
поэтому устаревший пин делал еженедельный мониторинг слепым к этим двум
переименованиям.

* ci: еженедельный сторож пина nightly-стенда

definitions-watch поднимает стенд из ARG XRPLD_VERSION, поэтому устаревший пин
сужает еженедельную проверку до состояния rippled на момент последнего касания
пина: оба переименования 3.3.0 месяц просидели за пином от 11.07. Снять пин
нельзя — формат таймстампа nightly-сборок сократился с 14 до 12 цифр, и порядок
версий Debian ставит старые сборки выше новых.

- .ci-config/bump-nightly-pin.sh: берёт свежую сборку xrpld из nightly-канала,
  переписывает ARG XRPLD_VERSION и перегенерирует rippled.batchv11.cfg по
  develop-коммиту, зашитому в саму версию — конфиг и бинарник не могут разойтись.
  Режим --check только отчитывается: текущий пин, свежая сборка, возраст пина
- .github/workflows/nightly-pin-watch.yml: раз в неделю; бампает, когда пин
  старше MAX_PIN_AGE_DAYS (21), поднимает стенд на новом пине, требует
  включённого на генезисе AMM-сентинела и прикладывает к PR дифф definitions
  против новой сборки. Учётки, идемпотентность и запасной путь через
  tracking issue — как в release-watch

* fix(ci): валидное имя ветки в nightly-pin-watch и правка changelog

Замечания CodeRabbit к PR #83.

- имя ветки собиралось из версии xrpld как есть, а `~` git в ref не пускает:
  `git checkout -b` упал бы и PR с бампом не создавался. Версия чистится по
  классу допустимых символов, а не по одному символу текущего формата, плюс
  проверка через git check-ref-format перед использованием
- в секции 10.11.0.0 CHANGES.md остались пункты, описывающие MutableFlags и
  tmfMPTSet* как актуальный API, хотя тот же релиз их и заменяет. Приведены к
  ImmutableFlags/tfMPTSet*; более ранние секции не тронуты — они описывают
  состояние API на момент своего релиза

---------

Co-authored-by: github-merge-queue <github-merge-queue@users.noreply.github.com>
@Platonenkov Platonenkov changed the title Fix client connection handling and update ledger object flags rippled 3.3.0 schema, three conformance guards, autofill fees and reconnect hardening (10.11.0.0) Aug 12, 2026
…ге пути (#82)

* feat(codec): MPT-хопы (0x40) в PathSet, mpt_issuance_id в шаге пути

PathSet знал только три классических бита типа хопа — 0x01 account,
0x10 currency, 0x20 issuer. rippled добавил STPathElement::TypeMpt = 0x40
в 3.2.0: хоп может нести 24-байтный MPTokenIssuanceID вместо валюты.
Пробел был тихим в обе стороны. FromParser не совпадал ни с одной маской
на байте 0x40, создавал пустой хоп и не считывал 24 байта MPTID — дальше
весь блоб разбирался со сдвигом, без единого исключения. SynthesizeType
не умел выставить бит вовсе, то есть собрать MPT-путь было нечем.

Что сделано в кодеке (Base/Xrpl.BinaryCodec/Types/PathSet.cs):

* PathHop.MptIssuanceId (Hash192), второй конструктор, HasMpt(),
  константы TypeMpt и TypeAll (0x71). currency и mpt_issuance_id в одном
  шаге — InvalidJsonException, как rippled бросает "bad path element:
  MPT and Currency"
* порядок записи повторяет STPathSet::add(): байт типа, account(20),
  MPTID(24), currency(20), issuer(20)
* FromParser отвергает то же, что и rippled: байт типа с битами вне
  TypeAll, currency вместе с MPT и пустой путь (ведущий или сдвоенный
  разделитель 0xFF). Раньше любой мусорный байт принимался, а пустой путь
  переживал декодирование и исчезал при обратной сборке — блоб и хеш
  транзакции не совпадали с исходными
* не-строковый mpt_issuance_id даёт InvalidJsonException, а не сырой
  InvalidOperationException из JsonNode — так же, как это делают Amount
  и Issue

Модель (Xrpl/Models/Methods/Path.cs):

* добавлено MPTokenIssuanceID (mpt_issuance_id) — шаг с MPT из
  ripple_path_find/path_find было нечем представить
* TypeHex помечен [Obsolete]: rippled убрал type_hex из STPath::getJson
  в 1.7.0 (коммит f0724694), в jss.h осталось только неиспользуемое
  объявление. Проверено на mainnet: 19 транзакций с Paths в трёх подряд
  идущих леджерах, 21 шаг пути, type присутствует во всех 21, type_hex —
  ни в одном; то же в ripple_path_find на s1/s2.ripple.com
* у Type уточнена документация: документация XRPL помечает поле
  устаревшим, но rippled по-прежнему отдаёт его в каждом шаге каждого
  ответа, поэтому поле остаётся. На исходящем пути оно игнорируется —
  STParsedJSON читает только account/currency/mpt_issuance_id/issuer,
  а кодек синтезирует байт из фактически заданных полей
* Payment.IsPathStep принимает mpt_issuance_id как актив шага и отвергает
  его в паре с currency

Тесты (Tests/Xrpl.BinaryCodec.Test/Types/TestUPathSet.cs, 9 штук):
раскладка классического хопа (0x30), MPT-хопа (0x40) и MPT+issuer (0x60)
против rippled, round-trip декодирования, три пути отказа и тест,
пиннящий синтез байта типа: удаление type или заведомо неверный
type/type_hex не меняют блоб.

Опережает сеть осознанно: MPTokensV2 на mainnet не включена и сейчас не
голосуется, xrpl.js и xrpl-py бит 0x40 тоже не знают.

Проверено: юнит-тесты 1036/1036. Отдельно — регрессия на реальной
транзакции 1D813B78FC55ABF9054AEBD2AF9DD7C90361F9985B7897E8E9A592D63BF0CC43:
блоб, собранный кодеком из её tx_json, совпадает с mainnet-блобом
побайтово.

* refactor(models)!: тип шага пути — [Flags] enum PathStepType вместо int

Байт типа хопа — битовая маска, но оба слоя писали её голым числом:
Path.Type был int?, PathHop.Type — int, и в коде вызывающего это
превращалось в сравнения с магическим 48. Ledger-объекты давно живут
иначе: AccountRootFlags и ещё восемь [Flags]-энумов стоят прямо в
свойстве. Приводим шаг пути к тому же виду.

PathStepType (Base/Xrpl.BinaryCodec/Enums/PathStepType.cs) — один enum
на оба слоя: Account 0x01, Currency 0x10, Issuer 0x20,
MPTokenIssuanceID 0x40, All 0x71. Живёт в кодеке, потому что участвует
в бинарной сериализации; модель ссылается на него так же, как TxFormat
и MPTokenIssuanceCreate уже ссылаются на типы BinaryCodec.

Ломающее: Path.Type теперь PathStepType?, PathHop.Type — PathStepType,
а константы PathHop.TypeAccount/TypeCurrency/TypeIssuer/TypeMpt/TypeAll
удалены в пользу enum. Код вида step.Type == 48 перестанет собираться,
чинится как PathStepType.Currency | PathStepType.Issuer.

Формат на проводе не меняется: XrplJsonOptions намеренно не
регистрирует глобальный JsonStringEnumConverter — протокольные enum
числовые, — поэтому type по-прежнему пишется и читается как число.
Значение с битом, которого enum не объявляет, переживает
десериализацию без потерь (проверено на 176). Единственная потеря
терпимости: "type":"48" строкой больше не разбирается, так как
NumberHandling.AllowReadingFromString на enum не распространяется;
rippled всегда шлёт число.

TestUPathStep (5 тестов) пиннит разбор 48 и 96 во флаги, числовую форму
на проводе, сохранение необъявленного бита и null при отсутствии поля.

Проверено: юнит-тесты 1060/1060, блоб реальной mainnet-транзакции
1D813B78FC55ABF9054AEBD2AF9DD7C90361F9985B7897E8E9A592D63BF0CC43
по-прежнему собирается побайтово идентично.

* fix(models): замечания CodeRabbit к PR #82 — шаги пути по правилам toStrand

Три находки, все приняты.

1. Payment.IsPathStep пропускал account вместе с активом (Major).
   Метод был портом xrpl.js isPathStep, который принимает {account,
   currency} и {account, issuer}; после добавления MPT туда же попало
   {account, mpt_issuance_id}. rippled такие пути отвергает до
   исполнения — toStrand() в PaySteps.cpp:

       if (hasAccount && (hasIssuer || hasCurrency))
           return {temBAD_PATH, Strand{}};
       if (hasMPT && (hasCurrency || hasAccount))
           return {temBAD_PATH, Strand{}};

   То есть SDK формировал транзакцию с гарантированным temBAD_PATH.
   Валидация приведена к правилам toStrand; расхождение с xrpl.js
   осознанное — там пробел, а не контракт. Проверено эмпирически:
   в 40 подряд идущих mainnet-леджерах 166 шагов пути, типы только 48
   (currency+issuer), 1 (account) и 16 (currency) — ни одной комбинации
   account с активом, так что ужесточение не отвергает то, что сеть
   реально использует.

2. Пустые пути отвергались только на одной границе кодека (Major).
   Прошлый коммит закрыл ведущий и сдвоенный разделитель при разборе, но
   мимо прошли ещё два случая: ToBytes молча выбрасывал пустой Path (на
   выходе получался блоб без этого пути), а разбор принимал терминатор
   сразу после разделителя — хвостовой пустой путь. Оба места теперь
   бросают BinaryCodecException, как rippled в STPathSet(SerialIter&).

3. CHANGES.md ссылался на удалённые константы (Minor).
   TypeMpt/TypeAll исчезли вместе с переходом на PathStepType — текст
   исправлен на PathStepType.MPTokenIssuanceID и PathStepType.All.

Тесты: TestUPathSetEmptyPathThrowsOnEncode,
TestUPathSetTrailingSeparatorThrowsOnDecode,
TestUPathStepValidationMatchesRippledToStrand.

Проверено: юнит-тесты 1063/1063, блоб mainnet-транзакции
1D813B78FC55ABF9054AEBD2AF9DD7C90361F9985B7897E8E9A592D63BF0CC43
по-прежнему собирается побайтово идентично.

* refactor(models): PathStepType переезжает в модели, кодек остаётся байтовым

Кодек — низкоуровневый слой, ему незачем экспортировать перечисление,
которое становится частью публичной поверхности моделей, а моделям
незачем тянуть namespace кодека ради имени типа. В репозитории эта
граница уже проведена: Xrpl.Models.Enums.TransactionType и
LedgerEntryType живут отдельно от BinaryCodec.Types.TransactionType.

PathStepType переехал в Xrpl/Models/Enums/PathStepType.cs и используется
только в Path.Type. В кодеке возвращены байтовые константы
TypeAccount/TypeCurrency/TypeIssuer/TypeMpt/TypeAll, PathHop.Type снова
byte, маски в FromParser считаются по ним. Base/Xrpl.BinaryCodec/Enums/
PathStepType.cs удалён; Xrpl/Models/Methods/Path.cs больше не ссылается
на BinaryCodec.

Отдельно: ToJson пишет байт типа как int, а не как byte. JsonValue<byte>
отказывается отдавать GetValue<int>(), то есть потребитель Decode,
читавший type как int, получил бы InvalidOperationException — это
поймал TestUPathSetMptHopRoundTrips после перевода поля в byte.

Проверено: юнит-тесты 1063/1063, блоб mainnet-транзакции
1D813B78FC55ABF9054AEBD2AF9DD7C90361F9985B7897E8E9A592D63BF0CC43
собирается побайтово идентично, decode возвращает type: 48.

* refactor(models)!: удалить Path.TypeHex вместо пометки [Obsolete]

Помечать устаревшим имеет смысл то, что работает и однажды перестанет.
type_hex не работает с февраля 2021: rippled убрал его из
STPath::getJson в 1.7.0 (коммит f0724694), в jss.h осталось только
неиспользуемое объявление. Свойство не могло принять ничего, кроме null,
ни на одной существующей ноде — это не устаревающий контракт, а мёртвая
поверхность. Удалено сразу, без периода [Obsolete], как в 10.11.0.0
удалялись свойства ledger-объектов, не являющиеся полями протокола.

Ответ ноды старее 1.7.0 по-прежнему разбирается: неизвестный ключ
System.Text.Json игнорирует, UnmappedMemberHandling.Disallow в
XrplJsonOptions не выставлен. Закреплено тестом
TestUPathStepIgnoresLegacyTypeHex.

Тест кодека, доказывающий что лишний type_hex во входном JSON не влияет
на блоб, оставлен как есть — он о поведении кодека, а не о модели.

Проверено: юнит-тесты 1064/1064.
…брыва (#84)

* fix(tests): мок-сервер переставал принимать соединения после одного обрыва

Причина флейка TestConcurrentChangeServerKeepsClientRecoverable: unit упал на
push-прогоне dev, тогда как тот же коммит в PR-прогоне прошёл.

Server.connectionCallback вызывал BeginAccept последней строкой try-блока,
поэтому любое исключение выше по телу навсегда обрывало приём соединений. Пир,
рвущий соединение во время handshake — ровно то, что порождают конкурентные
ChangeServer/Disconnect — глушил мок именно так. Со стороны всё выглядело
исправно: слушающий сокет оставался связан, порт числился занятым, TCP-соединения
устанавливались ядром, и клиент видел сервер, который принял подключение и молчит.
Отсюда 2m06s у упавшего прогона против 10s у обычного и сообщение, обвиняющее
клиент в том, что он «не дошёл до живого сервера».

- BeginAccept перезапускается безусловно после колбэка, чем бы тот ни кончился;
  ObjectDisposedException обрабатывается отдельно и без перезапуска — он означает,
  что Stop() закрыл listener
- сокет, чей handshake упал, закрывается, а не течёт до конца процесса
- TestUMockRippledAcceptLoop фиксирует поведение: обрыв на handshake (RST через
  LingerOption(true, 0)) поодиночке и десять подряд, затем обычный клиент, который
  обязан подключиться. На старом колбэке оба падают, после фикса проходят за 0.6s
- TestUtils.MockCompletesHandshake: проба реальным WS-upgrade. Упавший
  reconnect-тест теперь сообщает, отвечает ли мок, — глухой сервер больше не
  будет прочитан как баг клиента. Проба покрыта на живом моке, свободном порту и
  слушающем сокете, который никогда не принимает

* docs(changes): убрать запись о фиксе мок-сервера из changelog

Правка чисто в тестовой инфраструктуре: код SDK не менялся, потребителю
пакета изменение не видно, поэтому в changelog релиза ему места нет.
Разбор причины остаётся в описании PR и в комментариях к самому коду.
…с неизвестного LedgerEntryType (#85)

* perf(json): кэш производных JsonSerializerOptions + фикс неизвестного LedgerEntryType

Полиморфные конвертеры (LOConverter, оба конвертера транзакций, MetaBinaryConverter,
LedgerBinaryConverter, LONFTokenConverter, GenericStringConverter<T>) заново входят в
сериализатор с удалённым собственным конвертером, и каждый вызов строил производные
JsonSerializerOptions с нуля. System.Text.Json кэширует метаданные типов per-instance,
а копирующий конструктор кэш не переносит — страница account_objects на 200 записей
перестраивала контракты 200 раз.

JsonSerializerOptionsCache строит их один раз на пару (исходные options, тип конвертера),
слабо ключуясь по исходному экземпляру. Безопасно: STJ замораживает options при первом
использовании, поэтому то, что конвертер получает на вход, измениться уже не может.

Попутно — LOConverter.DetermineType разрешал неизвестный LedgerEntryType в LOAccountRoot:
Enum.TryParse при неудаче пишет в out default(TEnum), а AccountRoot — нулевое значение,
и инициализация Unknown затиралась. Тип объекта новее SDK десериализовался как account root
с молча потерянными полями вместо отката к BaseLedgerEntry.

Снят //todo change from class to interface на AccountObjects.AccountObjectList: парсинг
работает как в transactionResponse с момента глобальной регистрации LOConverter, а
BaseLedgerEntry обязан оставаться конкретным классом именно как fallback для Unknown.

Тесты: TestUJsonSerializerOptionsCache (переиспользование экземпляра, изоляция по источнику
и типу конвертера), TestUAccountObjectsPolymorphism (разнотипная страница account_objects,
round-trip, неизвестный тип, страница на 200 записей), два теста в TestULOConverter.

* docs(json): уточнить механизм выигрыша от кэша и усилить его тесты

Селф-ревью показал, что заявленное обоснование неверно для .NET 8+. Пробник-конвертер,
записывающий полученный экземпляр options при разборе списка, видит один и тот же
экземпляр на всех элементах даже при копии-на-вызов: с .NET 8 STJ разделяет
caching-context между структурно равными options, поэтому метаданные типов не
перестраивались. Настоящая цена копии — аллокация, копирование списка конвертеров и
структурный поиск в пуле контекстов на каждое конвертируемое значение.

Оптимизация остаётся: 200 страниц account_objects по 200 записей — 456 мс / 47 МБ до,
217 мс / 29 МБ после; плюс снимается зависимость от пула контекстов, ограниченного 64
записями. Формулировки в XML-doc и CHANGES.md приведены к измеренному.

Deserialize_ReusesTheCachedOptionsInstance не проверял заявленного: обе стороны сравнения
читали кэш напрямую, тест оставался зелёным при откате LOConverter.Read на копию-на-вызов.
Заменён на Deserialize_GoesThroughTheCache — приватный экземпляр options, которого не
касается ни один другой тест, и проверка появления записи в кэше через internal
HasCachedEntry. Откат конвертера теперь роняет тест (проверено).

Добавлен WithoutConverter_LeavesTheSourceOptionsIntact: если Build когда-нибудь начнёт
снимать конвертер с исходного экземпляра вместо копии, XrplJsonOptions.Default потеряет
LOConverter на весь процесс, и раньше этого не поймал бы ни один тест класса.

* docs(json): убрать устаревший механизм из summary тестов кэша options
@Platonenkov

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
Xrpl/Client/connection.cs (1)

2538-2546: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: derive the keepalive/ping split from InactivityTimeout too.

The inactivity limit is now configurable, but the keepalive threshold at Line 2548 stays a hard-coded 30 seconds. If a consumer sets InactivityTimeout below 30 seconds, the full ping-request branch becomes unreachable, because this branch returns first. The defaults keep the previous behavior, so this is a clarity concern only.

Consider expressing the keepalive threshold as a fraction of InactivityTimeout, or document the coupling next to the option.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Xrpl/Client/connection.cs` around lines 2538 - 2546, Align the keepalive
threshold in the surrounding connection-monitoring logic with configurable
config.InactivityTimeout instead of leaving it hard-coded at 30 seconds, so the
ping-request branch remains reachable for shorter timeout values. Prefer
deriving the threshold as a documented fraction of InactivityTimeout while
preserving the existing default behavior.
Xrpl/Models/Transactions/MPTokenIssuanceSet.cs (1)

244-255: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Parse Flags with Common.TryGetUInt32 for consistent validation errors.

Convert.ToUInt32(flags) throws FormatException or InvalidCastException when Flags is a non-numeric value. The adjacent ImmutableFlags check uses Common.TryGetUInt32 and reports a ValidationException. Callers that catch ValidationException do not catch the conversion exceptions.

♻️ Proposed change
             uint flagValue = 0;
             if (tx.TryGetValue("Flags", out var flags) && flags is not null)
             {
-                flagValue = Convert.ToUInt32(flags);
+                if (!Common.TryGetUInt32(flags, out flagValue))
+                {
+                    throw new ValidationException("MPTokenIssuanceSet: Flags must be a number");
+                }
                 bool hasLock = (flagValue & (uint)MPTokenIssuanceSetFlags.tfMPTLock) != 0;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Xrpl/Models/Transactions/MPTokenIssuanceSet.cs` around lines 244 - 255, In
the Flags parsing block of MPTokenIssuanceSet, replace Convert.ToUInt32(flags)
with Common.TryGetUInt32 so invalid values produce the same
ValidationException-based validation behavior as the adjacent ImmutableFlags
check. Preserve the existing tfMPTLock/tfMPTUnlock conflict validation after
successful parsing.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Xrpl/Sugar/Autofill.cs`:
- Around line 340-351: Update the catch clauses in FetchCounterpartySignerCount
at Xrpl/Sugar/Autofill.cs lines 340-351 and FetchLoan at lines 413-431 to apply
the cancellationToken exception filter when
(!cancellationToken.IsCancellationRequested). This must let cancellation
propagate while preserving the existing fallback values for other exceptions.

---

Nitpick comments:
In `@Xrpl/Client/connection.cs`:
- Around line 2538-2546: Align the keepalive threshold in the surrounding
connection-monitoring logic with configurable config.InactivityTimeout instead
of leaving it hard-coded at 30 seconds, so the ping-request branch remains
reachable for shorter timeout values. Prefer deriving the threshold as a
documented fraction of InactivityTimeout while preserving the existing default
behavior.

In `@Xrpl/Models/Transactions/MPTokenIssuanceSet.cs`:
- Around line 244-255: In the Flags parsing block of MPTokenIssuanceSet, replace
Convert.ToUInt32(flags) with Common.TryGetUInt32 so invalid values produce the
same ValidationException-based validation behavior as the adjacent
ImmutableFlags check. Preserve the existing tfMPTLock/tfMPTUnlock conflict
validation after successful parsing.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 562ff2b7-88b1-4d34-b9f1-7593104c3a0b

📥 Commits

Reviewing files that changed from the base of the PR and between f3d5d61 and 5e6ed6e.

⛔ Files ignored due to path filters (3)
  • Base/Xrpl.BinaryCodec/Enums/Field.Amount.Generated.cs is excluded by !**/*.generated.*
  • Base/Xrpl.BinaryCodec/Enums/Field.Int32.Generated.cs is excluded by !**/*.generated.*
  • Base/Xrpl.BinaryCodec/Enums/Field.Uint32.Generated.cs is excluded by !**/*.generated.*
📒 Files selected for processing (65)
  • .ci-config/Dockerfile.nightly
  • .ci-config/bump-nightly-pin.sh
  • .ci-config/docker-compose.ci.yml
  • .ci-config/rippled.batchv11.cfg
  • .ci-config/rippled.cfg
  • .github/workflows/definitions-watch.yml
  • .github/workflows/nightly-pin-watch.yml
  • .github/workflows/release-watch.yml
  • Base/Xrpl.BinaryCodec/Enums/definitions.json
  • Base/Xrpl.BinaryCodec/Types/PathSet.cs
  • CHANGES.md
  • CLAUDE.md
  • DocFx/ConfidentialMPT-Guide.md
  • DocFx/ConfidentialMPT-Guide.ru.md
  • DocFx/Sponsorship-Guide.md
  • DocFx/Sponsorship-Guide.ru.md
  • Tests/TestsClients/Blazor-WebAssembly/Pages/Index.razor
  • Tests/Xrpl.BinaryCodec.Test/Types/TestUPathSet.cs
  • Tests/Xrpl.Tests/Client/Json/Converters/AccountObjectsPolymorphismTests.cs
  • Tests/Xrpl.Tests/Client/Json/Converters/LOConverterTests.cs
  • Tests/Xrpl.Tests/Client/Json/JsonSerializerOptionsCacheTests.cs
  • Tests/Xrpl.Tests/Client/TestUHealthCheckOptions.cs
  • Tests/Xrpl.Tests/Client/TestUMockRippledAcceptLoop.cs
  • Tests/Xrpl.Tests/Client/TestUOnConnectedHandlerFailure.cs
  • Tests/Xrpl.Tests/Client/TestUReconnectSessionRaces.cs
  • Tests/Xrpl.Tests/Fixtures/LedgerFormats.h
  • Tests/Xrpl.Tests/Fixtures/LedgerFormats.h.ref
  • Tests/Xrpl.Tests/Fixtures/ledger_entries.macro
  • Tests/Xrpl.Tests/Fixtures/ledger_entries.macro.ref
  • Tests/Xrpl.Tests/Fixtures/transactions.macro
  • Tests/Xrpl.Tests/Fixtures/transactions.macro.ref
  • Tests/Xrpl.Tests/Integration/transactions/TestIBatchSponsorship.cs
  • Tests/Xrpl.Tests/Integration/transactions/TestIDynamicMPT.cs
  • Tests/Xrpl.Tests/Integration/transactions/TestISponsorship.cs
  • Tests/Xrpl.Tests/Integration/transactions/TestISponsorshipSigningMatrix.cs
  • Tests/Xrpl.Tests/MockRippled/Server.cs
  • Tests/Xrpl.Tests/Models/RippledLedgerFlags.cs
  • Tests/Xrpl.Tests/Models/TestUConfidentialMPT.cs
  • Tests/Xrpl.Tests/Models/TestULedgerFlagsConformance.cs
  • Tests/Xrpl.Tests/Models/TestUPathStep.cs
  • Tests/Xrpl.Tests/Models/TestUProtocolCompleteness.cs
  • Tests/Xrpl.Tests/Sugar/TestUAutofillFees.cs
  • Tests/Xrpl.Tests/TestUtils.cs
  • Xrpl/Client/Json/Converters/GenericStringConverter.cs
  • Xrpl/Client/Json/Converters/LONFTokenConverter.cs
  • Xrpl/Client/Json/Converters/LedgerBinaryConverter.cs
  • Xrpl/Client/Json/Converters/LedgerObjectConverter.cs
  • Xrpl/Client/Json/Converters/MetaBinaryConverter.cs
  • Xrpl/Client/Json/Converters/TransactionRequestConverter.cs
  • Xrpl/Client/Json/Converters/TransactionResponseConverter.cs
  • Xrpl/Client/Json/JsonSerializerOptionsCache.cs
  • Xrpl/Client/connection.cs
  • Xrpl/Models/Enums/PathStepType.cs
  • Xrpl/Models/Ledger/LOMPTokenIssuance.cs
  • Xrpl/Models/Methods/AccountObjects.cs
  • Xrpl/Models/Methods/Path.cs
  • Xrpl/Models/Transactions/Common.cs
  • Xrpl/Models/Transactions/MPTokenIssuanceCreate.cs
  • Xrpl/Models/Transactions/MPTokenIssuanceImmutableFlags.cs
  • Xrpl/Models/Transactions/MPTokenIssuanceSet.cs
  • Xrpl/Models/Transactions/Payment.cs
  • Xrpl/Models/Transactions/SponsorshipSet.cs
  • Xrpl/Models/Transactions/TxFormat.cs
  • Xrpl/Sugar/Autofill.cs
  • Xrpl/Xrpl.csproj
🚧 Files skipped from review as they are similar to previous changes (6)
  • Tests/Xrpl.Tests/Fixtures/LedgerFormats.h.ref
  • .ci-config/docker-compose.ci.yml
  • Tests/Xrpl.Tests/Fixtures/ledger_entries.macro.ref
  • Tests/Xrpl.Tests/Client/TestUReconnectSessionRaces.cs
  • Tests/Xrpl.Tests/Fixtures/ledger_entries.macro
  • Tests/Xrpl.Tests/Models/TestULedgerFlagsConformance.cs

Comment thread Xrpl/Sugar/Autofill.cs
Comment on lines +340 to +351
try
{
AccountInfo data = await client.AccountInfo(request, cancellationToken);
int? entries = data?.SignerLists?.Length > 0 ? data.SignerLists[0].SignerEntries?.Count : null;
return entries is > 0 ? entries.Value : 1;
}
catch (Exception)
{
// The counterparty account may not exist yet; preclaim rejects the transaction anyway.
return 1;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Unfiltered catch (Exception) hides cancellation in both fee lookups. Both helpers wrap a client call in a bare catch, so an OperationCanceledException from cancellationToken becomes a silent fee fallback instead of stopping Autofill. Add an exception filter at each site.

  • Xrpl/Sugar/Autofill.cs#L340-L351: add when (!cancellationToken.IsCancellationRequested) to the catch in FetchCounterpartySignerCount so cancellation propagates instead of returning 1.
  • Xrpl/Sugar/Autofill.cs#L413-L431: add when (!cancellationToken.IsCancellationRequested) to the catch in FetchLoan so cancellation propagates instead of returning null.
📍 Affects 1 file
  • Xrpl/Sugar/Autofill.cs#L340-L351 (this comment)
  • Xrpl/Sugar/Autofill.cs#L413-L431
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Xrpl/Sugar/Autofill.cs` around lines 340 - 351, Update the catch clauses in
FetchCounterpartySignerCount at Xrpl/Sugar/Autofill.cs lines 340-351 and
FetchLoan at lines 413-431 to apply the cancellationToken exception filter when
(!cancellationToken.IsCancellationRequested). This must let cancellation
propagate while preserving the existing fallback values for other exceptions.

…ption для Flags (#86)

Замечания CodeRabbit к релизному PR #76.

- FetchCounterpartySignerCount и FetchLoan оборачивают вызов клиента широким
  catch — это верно для случая, ради которого он существует (аккаунт
  контрагента или объект Loan ещё не созданы, дальше отработает preclaim), но
  туда же уходил OperationCanceledException от токена вызывающего. Autofill
  продолжал работу и записывал комиссию, посчитанную по fallback — один
  подписант, отсутствующий заём, — для запроса, который вызывающий уже отменил.
  Добавлен фильтр when (!cancellationToken.IsCancellationRequested):
  отмена вызывающего всплывает, а таймаут внутри клиента, который этот токен не
  отменяет, по-прежнему уходит в fallback
- MPTokenIssuanceSet: Flags разбирался через Convert.ToUInt32, бросающий
  FormatException/InvalidCastException на нечисловом значении, тогда как
  соседняя проверка ImmutableFlags и остальные валидаторы сообщают
  ValidationException — её и ловят вызывающие

Тесты: отменённый токен обязан бросить и не оставить Fee (LoanSet и LoanPay),
нечитаемый объект Loan обязан по-прежнему уходить в fallback, нечисловой Flags —
давать ValidationException. Первые два падают, если убрать фильтры.
@Platonenkov
Platonenkov enabled auto-merge August 12, 2026 19:01
@Platonenkov
Platonenkov merged commit 2d3a728 into release Aug 12, 2026
10 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Aug 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant