Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .ci-config/docker-compose.ci.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
services:
xrpld:
image: xrpllabsofficial/xrpld:3.2.0
image: xrpllabsofficial/xrpld:3.2.1
container_name: rippled-service
command: ["-a", "--start"]
ports:
Expand Down
9 changes: 6 additions & 3 deletions .github/workflows/protocol-watch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ name: Protocol Watch
#
# Coverage is an explicit list of full repo-relative paths (WATCH), so files
# from any directory can be tracked: the five protocol .macro definition files
# plus TxFlags.h (transaction flags) and TER.h (result codes), which the SDK
# mirrors as flag enums and the EngineResult enum but which live outside the
# macros.
# plus three headers the SDK mirrors but which live outside the macros —
# TxFlags.h (transaction flags), TER.h (result codes) and LedgerFormats.h
# (lsf ledger-object flags, vendored as a test fixture and diffed by
# TestULedgerFlagsConformance, which compares against a pinned copy and so
# cannot notice upstream moving on its own).
#
# State lives in the tracking issue body (labeled protocol-watch), not in the
# repo and not in actions cache: no commits through branch protection, no
Expand Down Expand Up @@ -40,6 +42,7 @@ env:
include/xrpl/protocol/detail/transactions.macro
include/xrpl/protocol/TxFlags.h
include/xrpl/protocol/TER.h
include/xrpl/protocol/LedgerFormats.h
ISSUE_TITLE: 'Protocol watch: rippled develop'
ISSUE_LABEL: protocol-watch

Expand Down
1 change: 1 addition & 0 deletions Base/Xrpl.BinaryCodec/Enums/Field.Uint8.Generated.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public partial class Field
public static readonly Uint8Field Method = new Uint8Field(nameof(Method), 2);
public static readonly Uint8Field Scale = new Uint8Field(nameof(Scale), 4);
public static readonly Uint8Field AssetScale = new Uint8Field(nameof(AssetScale), 5);
public static readonly Uint8Field LEVersion = new Uint8Field(nameof(LEVersion), 6);
public static readonly Uint8Field TickSize = new Uint8Field(nameof(TickSize), 16);
public static readonly Uint8Field UNLModifyDisabling = new Uint8Field(nameof(UNLModifyDisabling), 17);
public static readonly Uint8Field HookResult = new Uint8Field(nameof(HookResult), 18);
Expand Down
10 changes: 10 additions & 0 deletions Base/Xrpl.BinaryCodec/Enums/definitions.json
Original file line number Diff line number Diff line change
Expand Up @@ -3150,6 +3150,16 @@
"type": "UInt8"
}
],
[
"LEVersion",
{
"isSerialized": true,
"isSigningField": true,
"isVLEncoded": false,
"nth": 6,
"type": "UInt8"
}
],
[
"TickSize",
{
Expand Down
2 changes: 1 addition & 1 deletion Base/Xrpl.BinaryCodec/Xrpl.BinaryCodec.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/StaticBit-io/XrplCSharp</PackageProjectUrl>
<Title>XrplCSharp</Title>
<PackageVersion>10.9.0.0</PackageVersion>
<PackageVersion>10.11.0.0</PackageVersion>
</PropertyGroup>

<PropertyGroup>
Expand Down
64 changes: 64 additions & 0 deletions CHANGES.md

Large diffs are not rendered by default.

10 changes: 6 additions & 4 deletions DocFx/LendingProtocol-Guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -365,18 +365,20 @@ The lending protocol creates the following ledger objects:

| Field | Type | Description |
|-------|------|-------------|
| `Account` | AccountID | Borrower account |
| `Counterparty` | AccountID | Broker account |
| `Borrower` | AccountID | Borrower account |
| `LoanBrokerID` | Hash256 | Reference to loan broker |
| `PrincipalRequested` | Number | Original loan amount |
| `LoanSequence` | UInt32 | Sequence number within the broker |
| `PrincipalOutstanding` | Number | Remaining principal |
| `TotalValueOutstanding` | Number | Total amount owed |
| `PeriodicPayment` | Number | Amount due per interval |
| `InterestRate` | UInt32 | Annual interest rate |
| `PaymentInterval` | UInt32 | Seconds between payments |
| `PaymentTotal` | UInt32 | Total number of payments |
| `GracePeriod` | UInt32 | Seconds before late fees apply |
| `PaymentRemaining` | UInt32 | Remaining payments |
| `StartDate` | UInt32 | Loan start (Ripple epoch) |

> `Counterparty`, `PrincipalRequested` and `PaymentTotal` are fields of the **`LoanSet` transaction**, not of the `Loan` object. rippled records the requested principal as `PrincipalOutstanding`, so the ledger entry carries no `PrincipalRequested` — and neither does `LOLoan`.

### Querying Loan State

Use `account_objects` to retrieve loans owned by an account:
Expand Down
10 changes: 6 additions & 4 deletions DocFx/LendingProtocol-Guide.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -365,18 +365,20 @@ await client.SubmitRequest(fullySigned.TxBlob);

| Поле | Тип | Описание |
|------|-----|----------|
| `Account` | AccountID | Аккаунт заёмщика |
| `Counterparty` | AccountID | Аккаунт брокера |
| `Borrower` | AccountID | Аккаунт заёмщика |
| `LoanBrokerID` | Hash256 | Ссылка на кредитного брокера |
| `PrincipalRequested` | Number | Исходная сумма кредита |
| `LoanSequence` | UInt32 | Порядковый номер в рамках брокера |
| `PrincipalOutstanding` | Number | Остаток основной суммы |
| `TotalValueOutstanding` | Number | Общая задолженность |
| `PeriodicPayment` | Number | Сумма платежа за интервал |
| `InterestRate` | UInt32 | Годовая процентная ставка |
| `PaymentInterval` | UInt32 | Интервал между платежами (секунды) |
| `PaymentTotal` | UInt32 | Общее количество платежей |
| `GracePeriod` | UInt32 | Отсрочка до начисления пеней (секунды) |
| `PaymentRemaining` | UInt32 | Оставшиеся платежи |
| `StartDate` | UInt32 | Начало кредита (Ripple epoch) |

> `Counterparty`, `PrincipalRequested` и `PaymentTotal` — поля **транзакции `LoanSet`**, а не объекта `Loan`. Запрошенную сумму rippled записывает в `PrincipalOutstanding`, поэтому у ledger-объекта поля `PrincipalRequested` нет — как нет его и у `LOLoan`.

### Запрос состояния кредита

Используйте `account_objects` для получения кредитов аккаунта:
Expand Down
9 changes: 7 additions & 2 deletions Tests/TestsClients/Blazor-WebAssembly/Pages/Index.razor
Original file line number Diff line number Diff line change
Expand Up @@ -719,8 +719,6 @@
AddStatusMessage($"Changing server to: {newServerUrl}...", MessageType.Info);

await client.ChangeServer(newServerUrl);

CurrentServerUrl = client.Url();
}
catch (Exception ex)
{
Expand All @@ -730,6 +728,13 @@
}
finally
{
// Read the address back whether or not the connection succeeded. ChangeServer switches
// the client's target before it starts connecting, so a switch to a server that is down
// throws (the acquisition timeout) while the client is already reconnecting to the NEW
// address. Updating this only on success left the label showing the previous server, and
// the "Already connected to this server" guard above then compared against a stale value
// and refused a legitimate switch back.
CurrentServerUrl = client.Url();
IsChangingServer = false;
StateHasChanged();
}
Expand Down
190 changes: 190 additions & 0 deletions Tests/Xrpl.Tests/Client/TestUChangeServerFailure.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

using Xrpl.Client;

namespace Xrpl.Tests
{
/// <summary>
/// Regression tests for <c>ChangeServer</c> pointed at a server that is not up yet.
/// <para>
/// <c>ChangeServer</c> used to set the global <c>_isIntentionalDisconnect</c> flag, which was only ever
/// reset in <c>OnceOpen</c>. When the new server never came up, the flag stayed set, the failure of the
/// new connection was read as a user disconnect ("Connection closed permanently."), no reconnect loop
/// was started, and every later call failed with "No connection attempt in progress. Call Connect()
/// first." - the client was dead even after the server came up.
/// </para>
/// </summary>
[TestClass]
public class TestUChangeServerFailure
{
private CreateMockRippled _mockedRippled;
private CreateMockRippled _secondRippled;
private XrplClient _client;
private int _port;

private static Dictionary<string, object> ServerInfoResponse() => new Dictionary<string, object>
{
{ "type", "response" },
{ "status", "success" },
{ "result", new Dictionary<string, object>
{
{ "info", new Dictionary<string, object>
{
{ "build_version", "test-mock" },
{ "complete_ledgers", "1-1" },
{ "server_state", "full" },
}
},
}
},
};

private static CreateMockRippled StartMock(int port)
{
CreateMockRippled mock = new CreateMockRippled(port) { suppressOutput = true };
mock.AddResponse("server_info", ServerInfoResponse());

Thread listenerThread = new Thread(() => mock.Start()) { IsBackground = true };
listenerThread.Start();
return mock;
}

[TestInitialize]
public void MyTestInitialize()
{
_port = TestUtils.GetFreePort();
_mockedRippled = StartMock(_port);
}

[TestCleanup]
public async Task MyTestCleanup()
{
if (_client != null)
{
await _client.Disconnect();
_client = null;
}

_mockedRippled?.Stop();
_secondRippled?.Stop();
}

/// <summary>
/// Switching to a server that is not listening yet must leave the client reconnecting, so it comes
/// up on its own once that server appears - not stranded in a permanent disconnect.
/// </summary>
[TestMethod]
public async Task TestChangeServerToUnreachableServerRecoversWhenItComesUp()
{
_client = new XrplClient($"ws://127.0.0.1:{_port}", new XrplClient.ClientOptions
{
RequestPolicy = RequestFailurePolicy.ImmediateFail,
ReconnectBaseDelay = TimeSpan.FromMilliseconds(200),
ReconnectMaxDelay = TimeSpan.FromSeconds(1),
MaxReconnectAttempts = 50,
StopAfterMaxAttempts = false,
// Short on purpose: ChangeServer gives up waiting quickly, but the reconnect loop it left
// behind is what this test is about.
ConnectionAcquisitionTimeout = TimeSpan.FromSeconds(3),
ConnectionAttemptTimeout = TimeSpan.FromSeconds(3),
UseCustomPing = false,
});

await _client.Connect();
Assert.IsTrue(_client.connection.IsConnected(), "Precondition: client must be connected to the first server.");

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.
// The mock binds on a background thread, so a port taken in the meantime would
// surface as a 30s timeout below rather than as a bind error; check first.
Assert.IsTrue(
TestUtils.IsPortStillFree(secondPort),
$"Port {secondPort} was taken by another process while the test held it — rerun.");
_secondRippled = StartMock(secondPort);
Comment on lines +101 to +123

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.


DateTime deadline = DateTime.UtcNow + TimeSpan.FromSeconds(30);
while (!_client.connection.IsConnected() && DateTime.UtcNow < deadline)
{
await Task.Delay(TimeSpan.FromMilliseconds(200));
}

Assert.IsTrue(
_client.connection.IsConnected(),
$"Client never reached the new server after it came up (state: {_client.connection.CurrentConnectionState}).");
Assert.AreEqual($"ws://127.0.0.1:{secondPort}", _client.connection.GetUrl());

Dictionary<string, object> response =
await _client.Request(new Dictionary<string, object> { { "command", "server_info" } });
Assert.IsNotNull(response, "Client must be usable on the new server.");
}

/// <summary>
/// The same, after an explicit user <c>Disconnect()</c>: the global intentional-disconnect flag left
/// behind by it must not suppress reconnection for the server <c>ChangeServer</c> switches to.
/// </summary>
[TestMethod]
public async Task TestChangeServerAfterUserDisconnectStillReconnects()
{
_client = new XrplClient($"ws://127.0.0.1:{_port}", new XrplClient.ClientOptions
{
RequestPolicy = RequestFailurePolicy.ImmediateFail,
ReconnectBaseDelay = TimeSpan.FromMilliseconds(200),
ReconnectMaxDelay = TimeSpan.FromSeconds(1),
MaxReconnectAttempts = 50,
StopAfterMaxAttempts = false,
ConnectionAcquisitionTimeout = TimeSpan.FromSeconds(3),
ConnectionAttemptTimeout = TimeSpan.FromSeconds(3),
UseCustomPing = false,
});

await _client.Connect();
await _client.Disconnect();

int secondPort = TestUtils.GetFreePort();

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

Assert.IsTrue(
TestUtils.IsPortStillFree(secondPort),
$"Port {secondPort} was taken by another process while the test held it — rerun.");
_secondRippled = StartMock(secondPort);

DateTime deadline = DateTime.UtcNow + TimeSpan.FromSeconds(30);
while (!_client.connection.IsConnected() && DateTime.UtcNow < deadline)
{
await Task.Delay(TimeSpan.FromMilliseconds(200));
}

Assert.IsTrue(
_client.connection.IsConnected(),
$"Client never reached the new server after a user disconnect (state: {_client.connection.CurrentConnectionState}).");
}
}
}
Loading
Loading