diff --git a/dotnet/EcencyApi.Tests/WalletActionsTests.cs b/dotnet/EcencyApi.Tests/WalletActionsTests.cs
new file mode 100644
index 00000000..dee23509
--- /dev/null
+++ b/dotnet/EcencyApi.Tests/WalletActionsTests.cs
@@ -0,0 +1,39 @@
+using System.Text.Json.Nodes;
+using EcencyApi.Handlers;
+using Xunit;
+
+namespace EcencyApi.Tests;
+
+///
+/// The token action ids are a client-facing contract: web and mobile map them to
+/// concrete wallet operations, and an id neither client recognizes either renders
+/// nothing (web drops unknown ids) or renders an unlabeled control (mobile passes
+/// them through). Pin the ids and their order.
+///
+public class WalletActionsTests
+{
+ private static string[] Ids(JsonArray actions) =>
+ actions.Select(a => a!["id"]!.GetValue()).ToArray();
+
+ [Fact]
+ public void HpActions_AreTheChainOperationNamesInRenderOrder()
+ {
+ Assert.Equal(
+ new[] { "delegate_vesting_shares", "withdraw_vesting", "set_withdraw_vesting_route" },
+ Ids(WalletApi.HpActions()));
+ }
+
+ [Fact]
+ public void HiveActions_OnlyOfferSavingsWithdrawalWhenSavingsExist()
+ {
+ Assert.DoesNotContain("transfer_from_savings", Ids(WalletApi.BuildHiveActions(0)));
+ Assert.Contains("transfer_from_savings", Ids(WalletApi.BuildHiveActions(1.5)));
+ }
+
+ [Fact]
+ public void HbdActions_OnlyOfferSavingsWithdrawalWhenSavingsExist()
+ {
+ Assert.DoesNotContain("transfer_from_savings", Ids(WalletApi.BuildHbdActions(0)));
+ Assert.Contains("transfer_from_savings", Ids(WalletApi.BuildHbdActions(1.5)));
+ }
+}
diff --git a/dotnet/EcencyApi/Handlers/WalletApi.Market.cs b/dotnet/EcencyApi/Handlers/WalletApi.Market.cs
index 06e8c58a..1cd00e52 100644
--- a/dotnet/EcencyApi/Handlers/WalletApi.Market.cs
+++ b/dotnet/EcencyApi/Handlers/WalletApi.Market.cs
@@ -32,7 +32,10 @@ internal static JsonArray BuildHiveActions(double savings)
return Actions(ids.ToArray());
}
- internal static JsonArray HpActions() => Actions("delegate_vesting_shares", "withdraw_vesting");
+ // set_withdraw_vesting_route lets clients link straight to the withdraw routes UI.
+ // Deliberately added after the original two ids: clients render actions in order.
+ internal static JsonArray HpActions() =>
+ Actions("delegate_vesting_shares", "withdraw_vesting", "set_withdraw_vesting_route");
internal static JsonArray BuildHbdActions(double savings)
{
diff --git a/dotnet/parity/driver.py b/dotnet/parity/driver.py
index 606be668..ed367eac 100644
--- a/dotnet/parity/driver.py
+++ b/dotnet/parity/driver.py
@@ -230,6 +230,15 @@ def norm_body(text):
"Same request-delete rerouting as ::min.",
}
+# Deliberately NOT listed above: /wallet-api/portfolio-v2::pop, whose HP action list
+# gained set_withdraw_vesting_route (the reference build emits only
+# delegate_vesting_shares and withdraw_vesting). A KNOWN_DIVERGENCES entry skips the
+# case entirely — status and content-type included — which would blind this large
+# aggregation endpoint to unrelated regressions. Its body is already exempt for a
+# better reason: balances and APR move every block, so the run-vs-run comparison puts
+# it in `loose`, which still checks status and content-type. Add an entry here only
+# for a divergence that is deterministic and not already loose.
+
def diff(a_name, b_name, loose_name=None):
a = json.loads((HERE / f"run-{a_name}.json").read_text())