diff --git a/Base/Xrpl.BinaryCodec/Enums/Field.Uint8.Generated.cs b/Base/Xrpl.BinaryCodec/Enums/Field.Uint8.Generated.cs index 28287e34..01a4ca2f 100644 --- a/Base/Xrpl.BinaryCodec/Enums/Field.Uint8.Generated.cs +++ b/Base/Xrpl.BinaryCodec/Enums/Field.Uint8.Generated.cs @@ -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); diff --git a/Base/Xrpl.BinaryCodec/Enums/definitions.json b/Base/Xrpl.BinaryCodec/Enums/definitions.json index 0943aa48..14555c74 100644 --- a/Base/Xrpl.BinaryCodec/Enums/definitions.json +++ b/Base/Xrpl.BinaryCodec/Enums/definitions.json @@ -3150,6 +3150,16 @@ "type": "UInt8" } ], + [ + "LEVersion", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 6, + "type": "UInt8" + } + ], [ "TickSize", { diff --git a/Base/Xrpl.BinaryCodec/Xrpl.BinaryCodec.csproj b/Base/Xrpl.BinaryCodec/Xrpl.BinaryCodec.csproj index 770d5a8f..d38c8777 100644 --- a/Base/Xrpl.BinaryCodec/Xrpl.BinaryCodec.csproj +++ b/Base/Xrpl.BinaryCodec/Xrpl.BinaryCodec.csproj @@ -13,7 +13,7 @@ Apache-2.0 https://github.com/StaticBit-io/XrplCSharp XrplCSharp - 10.9.0.0 + 10.11.0.0 diff --git a/CHANGES.md b/CHANGES.md index 4466a442..2cb96daa 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,12 @@ # Changes ## 10.11.0.0 08/03/2026 +* **`sfLEVersion` — the Vault ledger entry's schema version** ([rippled #7817](https://github.com/XRPLF/rippled/pull/7817), merged into `develop` 07/30/2026, reported by protocol-watch). `UInt8` nth 6, `SoeDefault` on `ltVAULT`: it marks which accounting scheme a vault follows. Vaults created before cash-basis accounting was activated carry no `LEVersion` at all, and rippled resolves that absence as version 0 rather than an error — so an absent value is meaningful, not missing data: + * `definitions.json` + the generated `Field.Uint8` entry. **Both are required**: `definitions.json` is not read at runtime, it is the input to `Tools/GenerateEnums`, so a field added there alone travels nowhere. `TestULEVersion_BinaryRoundTrip` is what proves the round trip actually works rather than that the JSON was edited + * `LOVault.LEVersion` (`uint?`, matching the other UInt8 fields of that object) plus a `VaultVersion` enum naming the two values the protocol defines so far (`Legacy` = 0, `CashBasis` = 1) + * `TestULOVault_LEVersion_Deserialize` covers both shapes — the field present, and a legacy vault without it deserializing to `null` + * `Xrpl.BinaryCodec` bumped to **10.11.0.0**, aligned with `Xrpl` rather than to its own next minor (10.10.0.0): the codec ships the field, so the two move together and a consumer can read one version number off both. 10.10.x is simply skipped — the codec's last published version is 10.9.0, so no number is being reused. `Xrpl.AddressCodec` and `Xrpl.Keypairs` are untouched and keep 10.9.0.0 + * **Ledger-object flags the protocol declares but the models never named** — an unnamed bit still arrives in the model as a number, so reading the object kept working and only the consumer's ability to test it by name was lost. That is why these went unnoticed; a field-by-field diff of rippled `LedgerFormats.h` (tag `3.3.0-rc1`) against every flag enum found four gaps: * `MPTokenIssuanceFlags` + **`MPTCanHoldConfidentialBalance`** (0x80) — introduced by ConfidentialTransfer. The rest of the amendment was already complete (transactions 85–89, `IssuerEncryptionKey`/`AuditorEncryptionKey`, `ConfidentialOutstandingAmount`); only the flag had no name. Value confirmed against a live node: `MPTokenIssuanceSet` with `MutableFlags = tmfMPTSetCanHoldConfidentialBalance` moves the issuance from `Flags = 0` to `Flags = 128` * `MPTokenFlags` + **`lsfMPTAMM`** (0x4) — a much older gap: the flag is present as far back as 3.2.1. `AMMCreate` sets it together with `lsfMPTAuthorized` to implicitly authorize an MPT asset for the AMM pseudo-account diff --git a/Tests/Xrpl.Tests/Models/TestUProtocolCompleteness.cs b/Tests/Xrpl.Tests/Models/TestUProtocolCompleteness.cs index 367382b5..1a27dbcc 100644 --- a/Tests/Xrpl.Tests/Models/TestUProtocolCompleteness.cs +++ b/Tests/Xrpl.Tests/Models/TestUProtocolCompleteness.cs @@ -322,5 +322,47 @@ public void TestULORippleState_SponsorFields_Deserialize() Assert.AreEqual(Account1, state.HighSponsor); Assert.AreEqual(Account2, state.LowSponsor); } + + [TestMethod] + public void TestULOVault_LEVersion_Deserialize() + { + string json = JsonSerializer.Serialize(new Dictionary + { + ["LedgerEntryType"] = "Vault", + ["Account"] = Account1, + ["Owner"] = Account2, + ["ShareMPTID"] = "00000001A407AF5856CCF3C42619DAA925813FC955C72983", + ["WithdrawalPolicy"] = 1, + ["Scale"] = 6, + ["LEVersion"] = (uint)VaultVersion.CashBasis, + }); + LOVault vault = JsonSerializer.Deserialize(json, XrplJsonOptions.Default); + Assert.AreEqual((uint)VaultVersion.CashBasis, vault.LEVersion); + + // A vault created before cash-basis accounting carries no LEVersion at all; + // rippled resolves that absence as VaultVersion.Legacy rather than an error + string legacy = JsonSerializer.Serialize(new Dictionary + { + ["LedgerEntryType"] = "Vault", + ["Account"] = Account1, + ["Owner"] = Account2, + }); + Assert.IsNull(JsonSerializer.Deserialize(legacy, XrplJsonOptions.Default).LEVersion); + } + + [TestMethod] + public void TestULEVersion_BinaryRoundTrip() + { + // The field only travels if definitions.json knows it — this fails with an + // encoding error, not an assertion, when the entry is missing. + // Parsed from text rather than built from int literals: that is the shape a + // node response arrives in, and Uint8.FromJson takes a byte, not an Int32 + JsonObject json = JsonNode.Parse("""{"LEVersion":1,"Scale":6}""")!.AsObject(); + string blob = XrplBinaryCodec.Encode(json); + JsonObject decoded = XrplBinaryCodec.Decode(blob).AsObject(); + + Assert.AreEqual(1u, decoded["LEVersion"]!.GetValue()); + Assert.AreEqual(6u, decoded["Scale"]!.GetValue()); + } } } diff --git a/Xrpl/Models/Ledger/LOVault.cs b/Xrpl/Models/Ledger/LOVault.cs index f50c08f8..ae7b15be 100644 --- a/Xrpl/Models/Ledger/LOVault.cs +++ b/Xrpl/Models/Ledger/LOVault.cs @@ -23,6 +23,27 @@ public enum VaultLedgerFlags : uint lsfVaultPrivate = 0x00010000, } +/// +/// Values of the Vault ledger entry's LEVersion field (rippled VaultVersion). +/// +/// +/// stays a plain uint?, matching the other UInt8 +/// fields of this object; these constants name the values the protocol defines so far. +/// +public enum VaultVersion : uint +{ + /// + /// Accrual-basis accounting. Vaults created before cash-basis accounting was activated + /// carry no LEVersion at all and are treated as this version implicitly. + /// + Legacy = 0, + + /// + /// Cash-basis accounting (rippled #7817). + /// + CashBasis = 1, +} + /// /// Recommended structure for the Vault Data field. /// The JSON is whitespace-removed and hex-encoded (max 256 bytes). @@ -144,6 +165,14 @@ public LOVault() [JsonPropertyName("Scale")] public uint? Scale { get; init; } + /// + /// Schema version of this ledger entry (UInt8), see . + /// Absent on vaults created before cash-basis accounting was activated, which + /// rippled resolves as (0) rather than an error. + /// + [JsonPropertyName("LEVersion")] + public uint? LEVersion { get; init; } + /// /// Arbitrary hex-encoded data associated with the vault, limited to 256 bytes. /// Use for a human-readable representation.