Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
bd2b604
Сырой JSON узла доходит до вызывающего: модель перестаёт приписывать …
Platonenkov Aug 19, 2026
0e9cf67
Поток доступен через IXrplClient: события на клиенте, проброс к соеди…
Platonenkov Aug 20, 2026
9f855c2
Потерянные события потока перестают быть невидимыми: счётчик вытеснен…
Platonenkov Aug 20, 2026
0821c5d
Один путь потока для всех платформ: браузер перестаёт обходить очеред…
Platonenkov Aug 20, 2026
f405b11
Очередь потока существует до того, как потребитель подпишется: пинг-т…
Platonenkov Aug 21, 2026
a5f9e59
Doc-комментарии: cref, которые не разрешаются, XML, который не разбир…
Platonenkov Aug 21, 2026
8bdd7ab
Неизвестное поле отвергается на любой глубине, а не только сверху: по…
Platonenkov Aug 23, 2026
193c8ca
Поля ответов, которые нода шлёт, получают типизированные свойства: за…
Platonenkov Aug 24, 2026
cba7657
Models.Path становится Common.PathStep: имя перестаёт спорить с Syste…
Platonenkov Aug 24, 2026
634d810
A session that ends now says so: OnSessionEnded, and the receive loop…
Platonenkov Aug 24, 2026
bf38da6
Giving up on a connection reports giving up, and a failure the client…
Platonenkov Aug 24, 2026
b46af28
A memo a node will refuse is refused before signing, not after (#126)
Platonenkov Aug 25, 2026
f411401
Four places where the model said something that was not so (#136)
Platonenkov Aug 25, 2026
ac35730
A transaction declared as its interface serializes like the transacti…
Platonenkov Aug 25, 2026
c968c5a
test(streams): замер удержания кадра перестаёт вычитать два независим…
Platonenkov Aug 25, 2026
e2fd363
A failed submission arrives as something a caller can act on (#138)
Platonenkov Aug 25, 2026
9f1aaef
nft_info and nft_history: the two Clio commands NFT work needs (#140)
Platonenkov Aug 25, 2026
168a23b
AMM deposit and withdrawal arithmetic the node agrees with (#141)
Platonenkov Aug 26, 2026
cb75899
docs(changelog): stamp the release heading for 11.0.0.0 (#143)
Platonenkov Aug 26, 2026
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
7 changes: 3 additions & 4 deletions Base/Xrpl.BinaryCodec/Binary/BinaryParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,10 @@ public abstract class BinaryParser
/// </summary>
/// <returns></returns>
public int ReadOneInt() => ReadOne() & 0xFF;
/// <summary> Consume the first n bytes of the BinaryParser </summary>
/// <param name="n">n the number of bytes to skip</param>
/// <summary> The next byte in the BinaryParser, without consuming it </summary>
public abstract byte Peek();
/// <summary> todo </summary>
/// <param name="n">n the number of bytes to skip</param>
/// <summary> Consume the first n bytes of the BinaryParser </summary>
/// <param name="n">the number of bytes to skip</param>
public abstract void Skip(int n);
/// <summary> read the byte from the BinaryParser by current cursor position </summary>
public abstract byte ReadOne();
Expand Down
9 changes: 6 additions & 3 deletions Base/Xrpl.BinaryCodec/Types/AccountId.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,12 @@ public override string ToString()
public static readonly AccountId Zero = 0;
public static readonly AccountId Neutral = 1;

/// <summary> create instance from binary parser</summary>
/// <param name="parser">parser</param>
/// <param name="hint"></param>
/// <summary> create instance from an account id in hex, or from a classic address </summary>
/// <param name="value">
/// 40 <b>uppercase</b> hex characters, or a base58 classic address. The hex matcher is
/// <c>^[A-F0-9]{40}$</c> with no ignore-case option, so lowercase hex is not recognised as
/// hex and falls through to the address decoder.
/// </param>
public static AccountId FromValue(string value)
{
Regex rg = new Regex(HEX_REGEX);
Expand Down
6 changes: 6 additions & 0 deletions Base/Xrpl.BinaryCodec/Types/Issue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,12 @@ public static Issue FromJson(JsonNode token)
// MPT format: { "mpt_issuance_id": "..." }
if (obj.ContainsKey("mpt_issuance_id"))
{
// Counted like the two forms below, which have always done this. Without it the MPT
// form was the one shape of Issue where a member this codec does not know was
// dropped on the way into a blob or an id, with nothing said about it.
if (obj.Count != 1)
throw new InvalidJsonException("MPT Issue object must contain only 'mpt_issuance_id'.");

string mptId = obj["mpt_issuance_id"]?.GetValue<string>();
if (mptId is null)
throw new InvalidJsonException("Issue mpt_issuance_id must be a string.");
Expand Down
30 changes: 30 additions & 0 deletions Base/Xrpl.BinaryCodec/Types/PathSet.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
Expand Down Expand Up @@ -65,8 +67,36 @@ public PathHop(AccountId account, AccountId issuer, Currency currency, Hash192 m
/// <summary> Deserialize Hot </summary>
/// <param name="json">json token</param>
/// <returns></returns>
/// <summary>
/// The members a path step may carry.
/// </summary>
/// <remarks>
/// <c>type</c> and <c>type_hex</c> are in the set although nothing here reads them: the
/// node sends <c>type</c> on every step of a <c>ripple_path_find</c> answer, and this SDK
/// emits it back out of <see cref="PathHop.ToJson"/>, so a path taken from a response and
/// put into a payment carries it. The byte is synthesised from which of account, currency
/// and issuer are present, so the member is redundant rather than unknown - refusing it
/// would break the ordinary path-finding flow outright. <c>type_hex</c> has not been sent
/// since rippled 1.7.0 and is tolerated for old data.
/// </remarks>
private static readonly HashSet<string> PathStepMembers = new HashSet<string>(StringComparer.Ordinal)
{
"account", "currency", "issuer", "mpt_issuance_id", "type", "type_hex",
};

public static PathHop FromJson(JsonNode json)
{
if (json is JsonObject stepObject)
{
foreach (KeyValuePair<string, JsonNode> member in stepObject)
{
if (!PathStepMembers.Contains(member.Key))
{
throw new InvalidJsonException($"unknown path step property `{member.Key}`");
}
}
}

JsonNode mptIssuanceId = json["mpt_issuance_id"];
if (mptIssuanceId != null
&& (!(mptIssuanceId is JsonValue mptJv) || mptJv.GetValueKind() != JsonValueKind.String))
Expand Down
76 changes: 72 additions & 4 deletions Base/Xrpl.BinaryCodec/Types/StObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ public BuildFrom(FromJson json, FromParser parser)

private static readonly Dictionary<FieldType, BuildFrom> DispatchTable = new Dictionary<FieldType, BuildFrom>
{
// The JSON halves of these two are no longer the route ParseObject takes - it walks
// objects and arrays itself, which is the only way the strictness flag reaches them.
// They stay because both are public API a caller can reach directly, and because
// FromParser still dispatches through here. Equivalent to ParseObject(node, false)
// today; keep them that way.
[FieldType.StObject] = new BuildFrom(FromJson, FromParser),
[FieldType.StArray] = new BuildFrom(StArray.FromJson, StArray.FromParser),
[FieldType.Uint8] = new BuildFrom(Uint8.FromJson, Uint8.FromParser),
Expand Down Expand Up @@ -143,10 +148,54 @@ public static StObject FromJson(JsonNode token)
/// Construct a STObject from a JSON object
/// </summary>
/// <param name="token">An object to include</param>
/// <param name="strict">optional, denote which field to include in serialized object</param>
/// <param name="signingOnly">include only the fields that take part in signing</param>
/// <returns></returns>
/// <exception cref="InvalidJsonException">unknown field or token is not an object</exception>
public static StObject FromJson(JsonNode token, bool signingOnly)
{
StObject so = ParseObject(token, strict: signingOnly);
return signingOnly ? so.FilterIsSigning() : so;
}

/// <summary>
/// Builds an STObject from JSON, refusing members this codec does not know at any level,
/// and keeping every field it does know.
/// </summary>
/// <remarks>
/// For callers that need the strictness without the signing filter - computing an id over
/// a transaction, where dropping the non-signing fields would hash something other than
/// the transaction. <see cref="FromJson(JsonNode, bool)"/> ties the two together because
/// one flag used to mean both.
/// </remarks>
public static StObject FromJsonStrict(JsonNode token)
{
return ParseObject(token, strict: true);
}

/// <summary>
/// Builds an STObject from JSON, rejecting members this codec does not know when
/// <paramref name="strict"/> - at every level, not just this one.
/// </summary>
/// <remarks>
/// The recursion is the point. Nested objects used to reach this class through the
/// <see cref="BuildFrom"/> dispatch table, whose delegate signature carries no flag, so
/// they went through the lenient single-argument overload and an unknown member one level
/// down was dropped without a word. A caller could be shown a transaction containing a
/// member - a typo, or a field from an amendment newer than this SDK's definitions.json -
/// sign it, and put it in the ledger without that member. The top level failed loudly; one
/// level down it did not.
/// <para>
/// rippled does the opposite: <c>STParsedJSON::parseObject</c> recurses and answers
/// <c>unknownField</c> at every level, so such a transaction does not parse at all.
/// </para>
/// <para>
/// Strictness and the signing filter are separate concerns, and only look alike because
/// one flag used to carry both. <c>FilterIsSigning</c> still applies to the top
/// level alone - dropping non-signing fields out of nested objects would change what gets
/// signed, which is not what this fixes.
/// </para>
/// </remarks>
private static StObject ParseObject(JsonNode token, bool strict)
{
if (!(token is JsonObject))
throw new InvalidJsonException($"{token.GetValueKind()} is not an object");
Expand All @@ -156,7 +205,7 @@ public static StObject FromJson(JsonNode token, bool signingOnly)
{
if (!Field.Values.Has(pair.Key))
{
if (signingOnly)
if (strict)
throw new InvalidJsonException($"unknown field {pair.Key}");
continue;
}
Expand All @@ -165,15 +214,34 @@ public static StObject FromJson(JsonNode token, bool signingOnly)
ISerializedType st;
try
{
st = fieldForType.FromJson(jsonForField);
// Objects and arrays are walked here rather than through the dispatch table,
// which is the only way the flag reaches them.
st = fieldForType.Type == FieldType.StObject
? ParseObject(jsonForField, strict)
: fieldForType.Type == FieldType.StArray
? ParseArray(jsonForField, strict)
: fieldForType.FromJson(jsonForField);
}
catch (Exception e) when (e is InvalidOperationException || e is FormatException || e is OverflowException || e is PrecisionException)
{
throw new InvalidJsonException($"Can't decode `{fieldForType}` from `{jsonForField}`", e);
}
so.Fields[fieldForType] = st;
}
return signingOnly ? so.FilterIsSigning() : so;
return so;
}

/// <summary>
/// Builds an STArray from JSON, carrying <paramref name="strict"/> into every element.
/// </summary>
/// <remarks>
/// Arrays are how the common case is reached: <c>Memos</c>, <c>Signers</c> and the rest
/// hold objects, so a member inside <c>Memos[0].Memo</c> is two levels down and was the
/// original report on this.
/// </remarks>
private static StArray ParseArray(JsonNode token, bool strict)
{
return new StArray(token.AsArray().Select(n => ParseObject(n, strict)));
}

/// <inheritdoc />
Expand Down
18 changes: 18 additions & 0 deletions Base/Xrpl.BinaryCodec/Types/XChainBridgeType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,24 @@ public static XChainBridgeType FromJson(JsonNode token)
if (token is not JsonObject obj)
throw new InvalidJsonException("XChainBridge must be a JSON object.");

// Exactly these four, by name. A fifth member used to be read past in silence and left
// out of whatever blob or id this bridge ended up in.
//
// Counting alone would not do it: three of the four plus one unknown member also comes
// to four, and the check would pass while the missing one reached AccountId.FromJson or
// Issue.FromJson as null - reporting whatever those make of nothing rather than naming
// the member that does not belong.
if (obj.Count != 4
|| !obj.ContainsKey("LockingChainDoor")
|| !obj.ContainsKey("LockingChainIssue")
|| !obj.ContainsKey("IssuingChainDoor")
|| !obj.ContainsKey("IssuingChainIssue"))
{
throw new InvalidJsonException(
"XChainBridge object must contain exactly 'LockingChainDoor', 'LockingChainIssue', " +
"'IssuingChainDoor' and 'IssuingChainIssue'.");
}

AccountId lockingDoor = AccountId.FromJson(obj["LockingChainDoor"]);
Issue lockingIssue = Issue.FromJson(obj["LockingChainIssue"]);
AccountId issuingDoor = AccountId.FromJson(obj["IssuingChainDoor"]);
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.11.0.0</PackageVersion>
<PackageVersion>11.0.0.0</PackageVersion>
</PropertyGroup>

<PropertyGroup>
Expand Down
Loading
Loading