Skip to content
Merged
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
8 changes: 8 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changes

## 11.5.1.0 15/09/2026

* **Transaction validation is synchronous, and `BatchUtils.Build` validates what it assembles** (**breaking**). `Validation.Validate` and all 86 per-transaction validators behind it were declared `async Task` without ever awaiting anything - every one of them is straight-line field checking. Nothing in the SDK is measurably faster for it, but one caller paid for the disguise: `BatchUtils.Build` called `Validation.Validate(...)` and discarded the task. An `async` method captures **every** exception into the task it returns, including the ones thrown before the first `await`, so a discarded task is a discarded verdict: `ValidateBatch` ran, decided the batch was malformed, and reported it to nobody. A Batch built around a single inner transaction - which rippled answers with `temARRAY_EMPTY` - came back from `Build` looking well formed, and so did one with more than eight inners, with a `Vault`/`Loan` inner, with an inner missing `tfInnerBatchTxn`, or with an inner carrying a non-zero `Fee`. The compiler had been saying so since the method was written (CS4014).
* the validators now return `void` and throw on the calling thread. `await Validation.Validate(tx)` no longer compiles: drop the `await`. This is the whole migration - the exception type, the message and the conditions are unchanged, and a `try`/`catch` around the call keeps working as it is.
* **this is a breaking change released as a patch, deliberately.** Semver would call it a major: the return type is part of a method's signature in IL, so an assembly built against 11.5.0.0 meets a `MissingMethodException` on 11.5.1.0 even where it never wrote `await`, and its sources need the `await` dropped before they compile again. It is numbered a patch because the validators are opt-in - nothing inside the SDK calls them, and `BatchUtils.Build`, the one caller that did, is the method this release fixes. If you call `Validation.*` or `Common.ValidateBaseTransaction` directly, treat this upgrade as a major one: rebuild, and drop the `await`.
* making the signature honest is what fixes the defect, rather than adding the missing `await`: `Build` is synchronous and public, so awaiting would have meant `Task<Batch> BuildAsync`, and `.GetAwaiter().GetResult()` would have left the next caller the same trap. A validator that cannot be forgotten is a validator that has no task to forget.
* `TestUCredentialsValidator` wrapped the already-synchronous `CredentialsValidator.ValidateCredentialsList` in `Task.Run` purely to fit the async assertion helper's `Func<Task>`. The wrapper worked, because the helper awaited the task - but it is exactly the shape that stops working the moment the assertion becomes synchronous, since `Action` accepts a lambda whose value is discarded. The nine tests call the validator directly now.

## 11.5.0.0 13/09/2026

* **What happened to the connection is readable from the type, instead of the message text** (the follow-up to #179). 11.4.0 made the behaviour correct - one owner per transition, an operation that was overtaken says so - but gave the caller no way to read that answer. `NotConnectedException` carried five different events and `OperationCanceledException` two, so the only way to tell "the consumer disconnected the client" from "this endpoint is not answering" was to classify by message text - which the release notes of 11.3.2.0 told consumers not to do, while the library gave them no type capable of it.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,7 @@ public async Task TestOfferCreate_HybridFlag_WithoutDomainID_ShouldFail()

try
{
await Validation.ValidateOfferCreate(hybridOfferNoDoamin);
Validation.ValidateOfferCreate(hybridOfferNoDoamin);
Assert.Fail("Should have thrown ValidationException for tfHybrid without DomainID");
}
catch (ValidationException ex)
Expand All @@ -512,7 +512,7 @@ public async Task TestOfferCreate_InvalidDomainID_ShouldFail()

try
{
await Validation.ValidateOfferCreate(invalidDomainIdOffer);
Validation.ValidateOfferCreate(invalidDomainIdOffer);
Assert.Fail("Should have thrown ValidationException for invalid DomainID");
}
catch (ValidationException ex)
Expand All @@ -536,7 +536,7 @@ public async Task TestPayment_InvalidDomainID_ShouldFail()

try
{
await Validation.ValidatePayment(invalidDomainIdPayment);
Validation.ValidatePayment(invalidDomainIdPayment);
Assert.Fail("Should have thrown ValidationException for invalid DomainID");
}
catch (ValidationException ex)
Expand Down
29 changes: 14 additions & 15 deletions Tests/Xrpl.Tests/Models/TestAMMBid.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;

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

using Xrpl.Client.Exceptions;
using Xrpl.Models.Transactions;
Expand Down Expand Up @@ -63,36 +62,36 @@ public static void MyClassInitialize(TestContext testContext)
}

[TestMethod]
public async Task TestVerifyValid()
public void TestVerifyValid()
{
//verifies valid AMMBid
await Validation.Validate(bid);
Validation.Validate(bid);

//throws w/ missing field Asset
bid.Remove("Asset");
await Helper.ThrowsExceptionAsync<ValidationException>(() => Validation.Validate(bid), "AMMBid: missing field Asset");
Helper.ThrowsException<ValidationException>(() => Validation.Validate(bid), "AMMBid: missing field Asset");
bid["Asset"] = new Dictionary<string, object>() { { "currency", "XRP" } };
//throws w/ Asset must be an Issue
bid["Asset"] = 1234;
await Helper.ThrowsExceptionAsync<ValidationException>(() => Validation.Validate(bid), "AMMBid: Asset must be an Issue");
Helper.ThrowsException<ValidationException>(() => Validation.Validate(bid), "AMMBid: Asset must be an Issue");
bid["Asset"] = new Dictionary<string, object>() { { "currency", "XRP" } };
//throws w/ missing field Asset2
bid.Remove("Asset2");
await Helper.ThrowsExceptionAsync<ValidationException>(() => Validation.Validate(bid), "AMMBid: missing field Asset2");
Helper.ThrowsException<ValidationException>(() => Validation.Validate(bid), "AMMBid: missing field Asset2");
bid["Asset2"] = new Dictionary<string, object>() { { "currency", "ETH" }, { "issuer", "rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd" } };
//throws w/ Asset2 must be an Issue
bid["Asset2"] = 1234;
await Helper.ThrowsExceptionAsync<ValidationException>(() => Validation.Validate(bid), "AMMBid: Asset2 must be an Issue");
Helper.ThrowsException<ValidationException>(() => Validation.Validate(bid), "AMMBid: Asset2 must be an Issue");
bid["Asset2"] = new Dictionary<string, object>() { { "currency", "ETH" }, { "issuer", "rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd" } };

//throws w/ BidMin must be an Amount
bid["BidMin"] = 5;
await Helper.ThrowsExceptionAsync<ValidationException>(() => Validation.Validate(bid), "AMMBid: BidMin must be an Amount");
Helper.ThrowsException<ValidationException>(() => Validation.Validate(bid), "AMMBid: BidMin must be an Amount");
bid["BidMin"] = "5";

//throws w/ BidMax must be an Amount
bid["BidMax"] = 10;
await Helper.ThrowsExceptionAsync<ValidationException>(() => Validation.Validate(bid), "AMMBid: BidMax must be an Amount");
Helper.ThrowsException<ValidationException>(() => Validation.Validate(bid), "AMMBid: BidMax must be an Amount");
bid["BidMax"] = "10";

//throws w/ AuthAccounts length must not be greater than 4
Expand Down Expand Up @@ -130,11 +129,11 @@ public async Task TestVerifyValid()
}}
},
};
await Helper.ThrowsExceptionAsync<ValidationException>(() => Validation.Validate(bid), "AMMBid: AuthAccounts length must not be greater than 4");
Helper.ThrowsException<ValidationException>(() => Validation.Validate(bid), "AMMBid: AuthAccounts length must not be greater than 4");

//throws w/ AuthAccounts must be an AuthAccount array
bid["AuthAccounts"] = 1234;
await Helper.ThrowsExceptionAsync<ValidationException>(() => Validation.Validate(bid), "AMMBid: AuthAccounts must be an AuthAccount array");
Helper.ThrowsException<ValidationException>(() => Validation.Validate(bid), "AMMBid: AuthAccounts must be an AuthAccount array");

bid["AuthAccounts"] = new List<Dictionary<string, object>>()
{
Expand Down Expand Up @@ -163,7 +162,7 @@ public async Task TestVerifyValid()
};

//throws w/ invalid AuthAccounts when AuthAccount is undefined
await Helper.ThrowsExceptionAsync<ValidationException>(() => Validation.Validate(bid), "AMMBid: invalid AuthAccounts");
Helper.ThrowsException<ValidationException>(() => Validation.Validate(bid), "AMMBid: invalid AuthAccounts");
//throws w/ invalid AuthAccounts when AuthAccount is not an object
bid["AuthAccounts"] = new List<Dictionary<string, object>>()
{
Expand All @@ -190,7 +189,7 @@ public async Task TestVerifyValid()
}}
}
};
await Helper.ThrowsExceptionAsync<ValidationException>(() => Validation.Validate(bid), "AMMBid: invalid AuthAccounts");
Helper.ThrowsException<ValidationException>(() => Validation.Validate(bid), "AMMBid: invalid AuthAccounts");
// throws w/ invalid AuthAccounts when AuthAccount.Account is not a string
bid["AuthAccounts"] = new List<Dictionary<string, object>>()
{
Expand Down Expand Up @@ -220,7 +219,7 @@ public async Task TestVerifyValid()
}}
}
};
await Helper.ThrowsExceptionAsync<ValidationException>(() => Validation.Validate(bid), "AMMBid: invalid AuthAccounts");
Helper.ThrowsException<ValidationException>(() => Validation.Validate(bid), "AMMBid: invalid AuthAccounts");
//throws w/ AuthAccounts must not include sender's address
bid["AuthAccounts"] = new List<Dictionary<string, object>>()
{
Expand Down Expand Up @@ -250,7 +249,7 @@ public async Task TestVerifyValid()
}}
}
};
await Helper.ThrowsExceptionAsync<ValidationException>(() => Validation.Validate(bid), "AMMBid: AuthAccounts must not include sender's address");
Helper.ThrowsException<ValidationException>(() => Validation.Validate(bid), "AMMBid: AuthAccounts must not include sender's address");

}
}
Expand Down
33 changes: 16 additions & 17 deletions Tests/Xrpl.Tests/Models/TestAMMClawback.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;

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

using Xrpl.Client.Exceptions;
using Xrpl.Models.Transactions;
Expand All @@ -28,63 +27,63 @@ public static void MyClassInitialize(TestContext testContext)
}

[TestMethod]
public async Task TestVerifyValid()
public void TestVerifyValid()
{
await Validation.Validate(ammClawback);
Validation.Validate(ammClawback);
}

[TestMethod]
public async Task TestThrowsMissingHolder()
public void TestThrowsMissingHolder()
{
var tx = new Dictionary<string, object>(ammClawback);
tx.Remove("Holder");
await Helper.ThrowsExceptionAsync<ValidationException>(
Helper.ThrowsException<ValidationException>(
() => Validation.Validate(tx),
"AMMClawback: missing field Holder");
}

[TestMethod]
public async Task TestThrowsMissingAsset()
public void TestThrowsMissingAsset()
{
var tx = new Dictionary<string, object>(ammClawback);
tx.Remove("Asset");
await Helper.ThrowsExceptionAsync<ValidationException>(
Helper.ThrowsException<ValidationException>(
() => Validation.Validate(tx),
"AMMClawback: missing field Asset");
}

[TestMethod]
public async Task TestThrowsAssetMustBeIssue()
public void TestThrowsAssetMustBeIssue()
{
var tx = new Dictionary<string, object>(ammClawback);
tx["Asset"] = 1234;
await Helper.ThrowsExceptionAsync<ValidationException>(
Helper.ThrowsException<ValidationException>(
() => Validation.Validate(tx),
"AMMClawback: Asset must be an Issue");
}

[TestMethod]
public async Task TestThrowsMissingAsset2()
public void TestThrowsMissingAsset2()
{
var tx = new Dictionary<string, object>(ammClawback);
tx.Remove("Asset2");
await Helper.ThrowsExceptionAsync<ValidationException>(
Helper.ThrowsException<ValidationException>(
() => Validation.Validate(tx),
"AMMClawback: missing field Asset2");
}

[TestMethod]
public async Task TestThrowsAsset2MustBeIssue()
public void TestThrowsAsset2MustBeIssue()
{
var tx = new Dictionary<string, object>(ammClawback);
tx["Asset2"] = 1234;
await Helper.ThrowsExceptionAsync<ValidationException>(
Helper.ThrowsException<ValidationException>(
() => Validation.Validate(tx),
"AMMClawback: Asset2 must be an Issue");
}

[TestMethod]
public async Task TestValidWithOptionalAmount()
public void TestValidWithOptionalAmount()
{
var tx = new Dictionary<string, object>(ammClawback);
tx["Amount"] = new Dictionary<string, object>()
Expand All @@ -93,15 +92,15 @@ public async Task TestValidWithOptionalAmount()
{"issuer","rp6abvbTbjoce8ZDJkT6snvxTZSYMBCC9S"},
{"value","100"},
};
await Validation.Validate(tx);
Validation.Validate(tx);
}

[TestMethod]
public async Task TestThrowsInvalidAmountXRP()
public void TestThrowsInvalidAmountXRP()
{
var tx = new Dictionary<string, object>(ammClawback);
tx["Amount"] = "1000000";
await Helper.ThrowsExceptionAsync<ValidationException>(
Helper.ThrowsException<ValidationException>(
() => Validation.Validate(tx),
"AMMClawback: invalid Amount");
}
Expand Down
21 changes: 10 additions & 11 deletions Tests/Xrpl.Tests/Models/TestAMMCreate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;

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

using Xrpl.Client.Exceptions;
using Xrpl.Models.Transactions;
Expand Down Expand Up @@ -35,23 +34,23 @@ public static void MyClassInitialize(TestContext testContext)
}

[TestMethod]
public async Task TestVerifyValid()
public void TestVerifyValid()
{
//verifies valid AMMCreate
await Validation.Validate(ammCreate);
Validation.Validate(ammCreate);

//throws w/ missing Amount
ammCreate.Remove("Amount");
await Helper.ThrowsExceptionAsync<ValidationException>(() => Validation.Validate(ammCreate), "AMMCreate: missing field Amount");
Helper.ThrowsException<ValidationException>(() => Validation.Validate(ammCreate), "AMMCreate: missing field Amount");
ammCreate["Amount"] = "1000";
//throws w/ Amount must be an Amount
ammCreate["Amount"] = 1000;
await Helper.ThrowsExceptionAsync<ValidationException>(() => Validation.Validate(ammCreate), "AMMCreate: Amount must be an Amount");
Helper.ThrowsException<ValidationException>(() => Validation.Validate(ammCreate), "AMMCreate: Amount must be an Amount");
ammCreate["Amount"] = "1000";

//throws w/ missing Amount2
ammCreate.Remove("Amount2");
await Helper.ThrowsExceptionAsync<ValidationException>(() => Validation.Validate(ammCreate), "AMMCreate: missing field Amount2");
Helper.ThrowsException<ValidationException>(() => Validation.Validate(ammCreate), "AMMCreate: missing field Amount2");
ammCreate["Amount2"] = new Dictionary<string, object>()
{
{"currency","USD"},
Expand All @@ -60,7 +59,7 @@ public async Task TestVerifyValid()
};
//throws w/ Amount must be an Amount2
ammCreate["Amount2"] = 1000;
await Helper.ThrowsExceptionAsync<ValidationException>(() => Validation.Validate(ammCreate), "AMMCreate: Amount2 must be an Amount");
Helper.ThrowsException<ValidationException>(() => Validation.Validate(ammCreate), "AMMCreate: Amount2 must be an Amount");
ammCreate["Amount2"] = new Dictionary<string, object>()
{
{"currency","USD"},
Expand All @@ -69,20 +68,20 @@ public async Task TestVerifyValid()
};
//throws w/ missing TradingFee
ammCreate.Remove("TradingFee");
await Helper.ThrowsExceptionAsync<ValidationException>(() => Validation.Validate(ammCreate), "AMMCreate: missing field TradingFee");
Helper.ThrowsException<ValidationException>(() => Validation.Validate(ammCreate), "AMMCreate: missing field TradingFee");
ammCreate["TradingFee"] = 12u;
//throws w/ TradingFee must be a number
ammCreate["TradingFee"] = "12";
await Helper.ThrowsExceptionAsync<ValidationException>(() => Validation.Validate(ammCreate), "AMMCreate: TradingFee must be a number");
Helper.ThrowsException<ValidationException>(() => Validation.Validate(ammCreate), "AMMCreate: TradingFee must be a number");
ammCreate["TradingFee"] = 12u;

//throws when TradingFee is greater than 1000
ammCreate["TradingFee"] = 1001u;
await Helper.ThrowsExceptionAsync<ValidationException>(() => Validation.Validate(ammCreate), "AMMCreate: TradingFee must be between 0 and 1000");
Helper.ThrowsException<ValidationException>(() => Validation.Validate(ammCreate), "AMMCreate: TradingFee must be between 0 and 1000");
ammCreate["TradingFee"] = 12u;
//throws TradingFee must be a number
ammCreate["TradingFee"] = -1;
await Helper.ThrowsExceptionAsync<ValidationException>(() => Validation.Validate(ammCreate), "AMMCreate: TradingFee must be a number");
Helper.ThrowsException<ValidationException>(() => Validation.Validate(ammCreate), "AMMCreate: TradingFee must be a number");
ammCreate["TradingFee"] = 12u;

}
Expand Down
Loading
Loading