Skip to content

C# (.NET 10) port of the API service#48

Merged
feruzm merged 8 commits into
mainfrom
feature/csharp-port
Jul 11, 2026
Merged

C# (.NET 10) port of the API service#48
feruzm merged 8 commits into
mainfrom
feature/csharp-port

Conversation

@feruzm

@feruzm feruzm commented Jul 11, 2026

Copy link
Copy Markdown
Member

What

A drop-in ASP.NET Core (.NET 10) implementation of this service under dotnet/: same port, same environment variables, same healthcheck contract, and the same route surface (113 routes; two dead endpoints removed — see behavior notes) as the Node build.

Merging this PR cuts the deployment over to the C# service. CI now: runs dotnet test (gates PRs too) → builds dotnet/Dockerfile → pushes ecency/api:latest and ecency/api:sha-<commit> → deploys by immutable digest to all regions, exactly like before. The live service env vars carry over untouched (docker service update --image preserves them; the C# config reads the identical names with identical defaults).

Rollback

Every merge gets a durable per-commit tag, and the last Node build is preserved once as ecency/api:node-legacy before the first C# image overwrites :latest (the deploy hosts prune local images, so rollback pulls from the registry):

docker service update --image ecency/api:sha-<previous-commit> vision_vapi   # any prior build
docker service update --image ecency/api:node-legacy vision_vapi            # last Node build

The Node source stays in the repo unchanged (apart from the route-table fixes below, mirrored in both implementations).

Verification

  • 42 unit tests (dotnet test, a CI gate): crypto verified byte-for-byte against dhive-generated golden vectors (key-from-login, canonical ECDSA signing including the deterministic-nonce retry loop, public-key recovery, hs-token-create, validateCode re-serialization); V8-exact number formatting (27 node-generated vectors); RPC node failover incl. rate-limit parking and latency-EWMA demotion; the JS numeric-coercion semantics the wallet math depends on.
  • Differential parity harness (dotnet/parity/): 301 request variants across every route fired at the running Node image and the C# build side by side — 0 unexplained mismatches (intentional divergences below are recorded in the harness). portfolio-v2 additionally compared field-by-field on live data and matches to full double precision.
  • Docker image built and smoke-tested: starts as the non-root app user, serves, built-in HEALTHCHECK reports healthy.

Behavior changes (intentional, both implementations)

  • /private-api/request-delete now returns the account-deletion acknowledgment stub (200). Hive accounts cannot be deleted on-chain; the endpoint satisfies the app-store account-deletion requirement, but the old route table pointed it at the report handler, whose validation rejected the mobile payload with 400.
  • /private-api/post-reblogs and /private-api/post-reblog-count removed: no client callers, no traffic, and they read route params their POST routes never declared, so they always queried undefined/undefined upstream.
  • hs-token-refresh with a missing code returns 401 instead of the Node behavior (an unhandled async rejection that left the socket hanging).

Node failover

HiveRpcClient keeps the dhive parameters (timeout 2000, failoverThreshold 2) and adopts the health-tracker design from the vision-next SDK, simplified for proxy call rates: per-node consecutive-failure and rate-limit state, 429 parking honoring Retry-After (else 10s→60s escalating windows with 120s streak reset), recent-failure deprioritization, and latency-EWMA best-first ordering with an unproven-node prior. RPC-level errors still surface without failover. The two former dhive clients are unified into one node list (the node the explorer client preferred first is decommissioned).

Performance

Measured side by side against an identical local upstream (method + full table in dotnet/README.md): about 3x proxied throughput and about 4x lower p50 latency at the same CPU limit the stack file sets; 6-17x unconstrained. Memory is ~100-155 MiB under load (Server GC) vs ~49 MiB for Node — both far inside the stack memory limit.

Docs

dotnet/README.md covers setup end to end: SDK install, build, tests, running locally with the full env-var table, Docker build/run, the swarm compose drop-in, self-contained publish, deployment tags + rollback, failover design, and regenerating the crypto golden vectors.

ecency added 5 commits July 10, 2026 13:33
…ion byte-exact vs Node

- Port remaining wallet-api (market helpers, portfolio layers, engine + portfolioV2)
- Add JsVal (JS parseFloat/Number/typeof semantics over JsonNode)
- Wire full route table with Express-faithful fallback (GET template / 404 finalhandler)
- Fix getAccount null-vs-undefined: missing username -> RPC [null] not @null account
- Differential parity harness: 305 request variants, 0 real mismatches vs Node image
…s, docs

- HiveRpcClient: 429/502/503/504 advance to next node immediately (no wasted
  retry on a throttled node); transient errors retry per failoverThreshold;
  sticky healthy node; RPC-level errors still surface without failover
- Tests (40): dhive crypto golden vectors, RPC failover (rate-limit/timeout/
  sticky/all-down), JS coercion semantics (parseFloat/Number/base-units)
- Commit the differential parity harness (305 variants, 0 real mismatches)
- README + docker-compose for drop-in deployment
@greptile-apps

greptile-apps Bot commented Jul 11, 2026

Copy link
Copy Markdown

Greptile Summary

This PR ports the API service to ASP.NET Core under dotnet/. The main changes are:

  • A new .NET API implementation with matching route and healthcheck behavior.
  • Docker, CI, deployment, and rollback updates for the C# service.
  • Chain, wallet, auth, search, and upstream proxy handling in the new service.
  • Unit tests, parity tooling, fixtures, and setup documentation for the port.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
dotnet/EcencyApi/Infrastructure/Upstream.cs Request forwarding keeps the observed Node wire behavior for upstream calls.
dotnet/EcencyApi/Handlers/PrivateApi.Chain.cs Chain balance handling includes provider failover and preserves Solana balance literals on the normal response path.

Reviews (4): Last reviewed commit: "fix: dead-endpoint cleanup, request-dele..." | Re-trigger Greptile

Comment on lines +95 to +96
var bodyJson = payload?.ToJsonString(RawJsonOptions) ?? "{}";
req.Content = new StringContent(bodyJson, Encoding.UTF8, "application/json");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Empty GETs Carry Bodies

When a proxy handler calls BaseApiRequest without a payload, this still sends {} with Content-Type: application/json. The Node helper sends empty GET requests without a body, so upstreams or intermediaries that reject or vary on GET bodies can return a different status or response through the C# port.

Fix in Claude Code

Comment thread dotnet/EcencyApi/Handlers/PrivateApi.Chain.cs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4a023aba37

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +30 to +32
if (!isJson || ctx.Request.ContentLength is 0)
{
return new JsonObject();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Parse urlencoded request bodies

The Node service registers express.urlencoded({limit: '50mb'}), so existing clients can POST application/x-www-form-urlencoded payloads to these endpoints. Here every non-JSON content type is converted to an empty object, so a form post such as code=... to any auth/login-required route is treated as missing input and either 401s or forwards undefined values instead of preserving the existing API contract.

Useful? React with 👍 / 👎.

Comment on lines +261 to +263
// Objects, arrays, booleans, null -> res.json()
ctx.Response.ContentType = "application/json; charset=utf-8";
await ctx.Response.WriteAsync(json?.ToJsonString(RawJsonOptions) ?? "null");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Mirror Express send(null) for proxied nulls

When an upstream returns the JSON body null, axios parses it as JS null and the original pipe() passes that to res.send, which Express serializes as an empty response. This path instead writes the literal null with an application/json content type, so any proxied private/search/engine endpoint that uses JSON null to mean “no value” changes both body and headers for existing clients.

Useful? React with 👍 / 👎.

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds a .NET 10 ASP.NET Core implementation of the API, including route handlers, Hive and chain integrations, wallet portfolio logic, authentication, deployment configuration, and parity tooling. It also adds cryptographic golden-vector tests, failover tests, JavaScript-semantics tests, and differential HTTP validation.

Changes

.NET service foundation

Layer / File(s) Summary
Runtime and shared infrastructure
dotnet/EcencyApi/..., dotnet/EcencyApi/EcencyApi.csproj
Adds configuration, HTTP forwarding, caching, JavaScript-compatible serialization/coercion, Hive cryptography, Hive Engine models, and project settings.
Hive RPC and chain providers
dotnet/EcencyApi/Infrastructure/HiveRpcClient.cs, dotnet/EcencyApi/Handlers/ChainProviders.cs, dotnet/EcencyApi/Handlers/PrivateApi.Chain.cs, dotnet/EcencyApi/Handlers/HiveExplorer.cs
Adds sticky Hive RPC failover, configurable blockchain provider pools, chain balance/RPC proxying, and Hive account/global-property retrieval.
API routing and private handlers
dotnet/EcencyApi/Handlers/Routes.cs, dotnet/EcencyApi/Handlers/*Api*.cs, dotnet/EcencyApi/Handlers/PrivateApi.*.cs
Registers API routes and implements authentication, search, feeds, accounts, payments, wallet, notification, user-data, and miscellaneous private endpoints.
Wallet portfolio aggregation
dotnet/EcencyApi/Handlers/WalletApi.*.cs
Adds engine, market, chain, Hive, SPK, points, pricing, and portfolio-layer orchestration with timeout and concurrency handling.
Application startup and packaging
dotnet/EcencyApi/Program.cs, dotnet/Dockerfile, dotnet/docker-compose.yml, dotnet/EcencyApi/public/robots.txt
Adds health-check startup mode, middleware, static-file serving, Docker packaging, compose deployment settings, and crawler directives.

Validation and parity

Layer / File(s) Summary
Cross-implementation validation
dotnet/EcencyApi.Tests/*, dotnet/parity/*, dotnet/tools/gen-vectors.js
Adds crypto reference vectors, Hive failover tests, JavaScript coercion tests, a differential HTTP harness, deterministic mock upstreams, and vector generation tooling.
Operational documentation
dotnet/README.md, dotnet/parity/README.md
Documents service behavior, parity validation, environment configuration, local execution, Docker usage, and vector regeneration.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Poem

A rabbit reviews the routes with delight,
Hive signs messages clean and bright.
Wallets hop through chains and pools,
Tests guard parity with careful rules.
Docker carries the service away—
“Ship it!” I thump, then munch hay.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 39.89% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: a .NET 10 C# port of the API service.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/csharp-port

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (6)
dotnet/EcencyApi/Handlers/WalletApi.Market.cs (1)

462-468: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Remove or guard the per-lookup debug log.

GetTokenPrice runs once per token per layer, so for any non-USD currency this Console.WriteLine fires on every price lookup, producing significant log spam on a hot path. It reads like leftover debug output.

♻️ Proposed change
-        if (currencyKey != "usd")
-        {
-            var tokenData = JsVal.Prop(marketData, tokenKey) ?? JsVal.Prop(marketData, upperToken);
-            var quotesNode = JsVal.Prop(tokenData, "quotes");
-            var availableQuotes = quotesNode is JsonObject qo ? string.Join(",", qo.Select(kv => kv.Key)) : "";
-            Console.WriteLine($"Looking for {tokenKey} price in {currencyKey}. Available quotes: {availableQuotes}");
-        }
-
         var containers = CollectContainers(marketData, 3);
🤖 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 `@dotnet/EcencyApi/Handlers/WalletApi.Market.cs` around lines 462 - 468, Remove
the per-lookup Console.WriteLine from GetTokenPrice, or guard it behind an
explicitly enabled diagnostic/debug setting so it cannot run during normal
requests. Preserve the quote lookup behavior and avoid logging on every non-USD
token price lookup.
dotnet/EcencyApi/Handlers/WalletApi.Engine.cs (1)

347-354: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

WithTimeout leaves the timer running and can orphan a faulted task.

When the timeout wins, task is abandoned; if it later faults, its exception is unobserved (TaskScheduler noise / potential process-level handler). The Task.Delay timer is also never cancelled, so it lingers until it fires. Observe the exception and cancel the timer.

♻️ Proposed change
 private static async Task<T> WithTimeout<T>(Task<T> task, int ms, T fallback)
 {
-    var timeout = Task.Delay(ms);
-    var completed = await Task.WhenAny(task, timeout);
-    if (completed != task) return fallback;
+    using var cts = new CancellationTokenSource();
+    var timeout = Task.Delay(ms, cts.Token);
+    var completed = await Task.WhenAny(task, timeout);
+    if (completed != task)
+    {
+        _ = task.ContinueWith(t => { _ = t.Exception; }, TaskContinuationOptions.OnlyOnFaulted);
+        return fallback;
+    }
+    cts.Cancel();
     try { return await task; }
     catch { return fallback; }
 }
🤖 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 `@dotnet/EcencyApi/Handlers/WalletApi.Engine.cs` around lines 347 - 354, Update
WithTimeout<T> to use a CancellationTokenSource for the Task.Delay timer and
cancel it when the operation completes. If the timeout wins, attach continuation
or otherwise await the original task to observe any eventual exception, while
still returning fallback immediately; preserve fallback handling for task faults
and cancellation.
dotnet/EcencyApi/Handlers/AuthApi.cs (1)

51-69: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Consider reusing the shared JsVal.ToJsString coercion.

This JsToString is duplicated near-verbatim in SearchApi, UserData1Helpers.Template, and UserData2Js.JsString, while WalletApi already relies on the canonical JsVal.ToJsString. Consolidating avoids the four copies drifting apart. Please confirm JsVal.ToJsString reproduces the same array/object/bool rules before collapsing them.

🤖 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 `@dotnet/EcencyApi/Handlers/AuthApi.cs` around lines 51 - 69, The local
JsToString implementation duplicates shared coercion logic. Verify that
JsVal.ToJsString matches the required null, object, array element joining,
string, and boolean behavior, then replace this method and the equivalent
implementations in SearchApi, UserData1Helpers.Template, and
UserData2Js.JsString with calls to JsVal.ToJsString, updating callers as needed.
dotnet/EcencyApi/Handlers/PrivateApi.Accounts.cs (2)

56-61: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Two divergent ports of signupClientIp().

This copy returns only the first x-real-ip value, while PaymentsSignupClientIp in PrivateApi.Payments.cs (Lines 374-385) joins duplicate values with ", ". Node/Express joins duplicate request headers (except a small denylist), so the joining variant is the faithful port and this one diverges when the header appears more than once. Since both live on the same PrivateApi partial class, consider collapsing to a single shared helper to remove the divergence.

♻️ Align to the joining behavior
-        var realIp = ctx.Request.Headers["x-real-ip"];
-        var first = realIp.Count > 0 ? realIp[0] : null;
-        return string.IsNullOrEmpty(first) ? "" : first;
+        var realIp = ctx.Request.Headers["x-real-ip"];
+        var value = realIp.Count switch
+        {
+            0 => (string?)null,
+            1 => realIp[0],
+            _ => string.Join(", ", realIp.ToArray()),
+        };
+        return string.IsNullOrEmpty(value) ? "" : value;
🤖 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 `@dotnet/EcencyApi/Handlers/PrivateApi.Accounts.cs` around lines 56 - 61,
Update SignupClientIp to match PaymentsSignupClientIp by joining all x-real-ip
header values with ", " instead of returning only the first value. Prefer
consolidating both methods into one shared helper on the PrivateApi partial
class, then have each caller use it to eliminate divergent behavior.

69-154: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated Turnstile verifier.

VerifyTurnstile/PostTurnstileForm here and PaymentsVerifyTurnstile in PrivateApi.Payments.cs (Lines 392-449) are near-identical fail-closed implementations of the same siteverify call. Consider extracting one shared verifier to keep the security-critical fail-closed logic in a single place.

🤖 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 `@dotnet/EcencyApi/Handlers/PrivateApi.Accounts.cs` around lines 69 - 154,
Consolidate the duplicated Turnstile siteverify logic from
VerifyTurnstile/PostTurnstileForm and PaymentsVerifyTurnstile into one shared
verifier, preferably reusing a common form-post helper. Update both account and
payment callers to use it while preserving token validation, timeout/error
handling, and fail-closed behavior.
dotnet/EcencyApi/Handlers/HiveExplorer.cs (1)

283-324: 🎯 Functional Correctness | 🔵 Trivial | 🏗️ Heavy lift

Consolidate the JS Number(string) coercion.

JsNumberString and ChainJsNumber already diverge on whitespace trimming, Infinity handling, and radix parsing, so a shared helper would keep the Hive and chain paths from drifting apart.

🤖 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 `@dotnet/EcencyApi/Handlers/HiveExplorer.cs` around lines 283 - 324,
Consolidate JavaScript string-to-number coercion into one shared helper used by
both JsNumberString and ChainJsNumber. Move trimming, Infinity recognition, and
hexadecimal/binary/octal radix parsing into that helper, then replace the
duplicated or divergent logic in both callers while preserving existing Number
semantics and invalid-input handling.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@dotnet/Dockerfile`:
- Around line 15-30: Add a USER directive in the runtime stage of the
Dockerfile, after copying files and before the health check/entrypoint,
selecting the built-in ASP.NET app user (UID 101) so EcencyApi.dll runs without
root privileges.

In `@dotnet/EcencyApi/Infrastructure/JsJson.cs`:
- Around line 203-212: The JsonValue handling branch can throw for CLR-backed
values such as JsonValue.Create(0) because it unconditionally calls
GetValue<JsonElement>(). In the JsonValue case, handle numeric and other
CLR-backed values using TryGetValue<JsonElement>() or an appropriate numeric
helper before falling back, while preserving the existing string and boolean
behavior.
- Around line 124-143: The FormatNumber method must match JSON.stringify’s
fixed/scientific notation thresholds, not merely normalize the exponent marker.
Update its finite-number formatting so values in [1e-6, 1e21) are emitted in
fixed-point notation (including expanding exponent output and preserving
necessary decimal digits), while values outside that range use normalized
lowercase scientific notation; retain invariant culture and NaN/Infinity
handling.

In `@dotnet/parity/mock_upstream.py`:
- Line 16: Replace the hardcoded sandbox path in mock_upstream.py’s LOG_PATH
with a portable, configurable path, such as an environment-variable override
with a sensible local default. Update record() and the __main__ setup to create
the log file’s parent directory before writing or truncating it, preventing
FileNotFoundError across environments.

---

Nitpick comments:
In `@dotnet/EcencyApi/Handlers/AuthApi.cs`:
- Around line 51-69: The local JsToString implementation duplicates shared
coercion logic. Verify that JsVal.ToJsString matches the required null, object,
array element joining, string, and boolean behavior, then replace this method
and the equivalent implementations in SearchApi, UserData1Helpers.Template, and
UserData2Js.JsString with calls to JsVal.ToJsString, updating callers as needed.

In `@dotnet/EcencyApi/Handlers/HiveExplorer.cs`:
- Around line 283-324: Consolidate JavaScript string-to-number coercion into one
shared helper used by both JsNumberString and ChainJsNumber. Move trimming,
Infinity recognition, and hexadecimal/binary/octal radix parsing into that
helper, then replace the duplicated or divergent logic in both callers while
preserving existing Number semantics and invalid-input handling.

In `@dotnet/EcencyApi/Handlers/PrivateApi.Accounts.cs`:
- Around line 56-61: Update SignupClientIp to match PaymentsSignupClientIp by
joining all x-real-ip header values with ", " instead of returning only the
first value. Prefer consolidating both methods into one shared helper on the
PrivateApi partial class, then have each caller use it to eliminate divergent
behavior.
- Around line 69-154: Consolidate the duplicated Turnstile siteverify logic from
VerifyTurnstile/PostTurnstileForm and PaymentsVerifyTurnstile into one shared
verifier, preferably reusing a common form-post helper. Update both account and
payment callers to use it while preserving token validation, timeout/error
handling, and fail-closed behavior.

In `@dotnet/EcencyApi/Handlers/WalletApi.Engine.cs`:
- Around line 347-354: Update WithTimeout<T> to use a CancellationTokenSource
for the Task.Delay timer and cancel it when the operation completes. If the
timeout wins, attach continuation or otherwise await the original task to
observe any eventual exception, while still returning fallback immediately;
preserve fallback handling for task faults and cancellation.

In `@dotnet/EcencyApi/Handlers/WalletApi.Market.cs`:
- Around line 462-468: Remove the per-lookup Console.WriteLine from
GetTokenPrice, or guard it behind an explicitly enabled diagnostic/debug setting
so it cannot run during normal requests. Preserve the quote lookup behavior and
avoid logging on every non-USD token price lookup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1f1db203-ca69-4319-abb5-71622e1e05fa

📥 Commits

Reviewing files that changed from the base of the PR and between cea5095 and 4a023ab.

⛔ Files ignored due to path filters (3)
  • dotnet/EcencyApi/public/favicon.ico is excluded by !**/*.ico
  • dotnet/EcencyApi/public/favicon.png is excluded by !**/*.png
  • dotnet/EcencyApi/public/logo512.png is excluded by !**/*.png
📒 Files selected for processing (46)
  • dotnet/.gitignore
  • dotnet/Dockerfile
  • dotnet/EcencyApi.Tests/EcencyApi.Tests.csproj
  • dotnet/EcencyApi.Tests/HiveCryptoTests.cs
  • dotnet/EcencyApi.Tests/HiveRpcFailoverTests.cs
  • dotnet/EcencyApi.Tests/JsValTests.cs
  • dotnet/EcencyApi.Tests/fixtures/crypto-vectors.json
  • dotnet/EcencyApi/Config.cs
  • dotnet/EcencyApi/EcencyApi.csproj
  • dotnet/EcencyApi/Handlers/Announcements.cs
  • dotnet/EcencyApi/Handlers/AuthApi.cs
  • dotnet/EcencyApi/Handlers/ChainProviders.cs
  • dotnet/EcencyApi/Handlers/Constants.cs
  • dotnet/EcencyApi/Handlers/HiveExplorer.cs
  • dotnet/EcencyApi/Handlers/PrivateApi.Accounts.cs
  • dotnet/EcencyApi/Handlers/PrivateApi.Chain.cs
  • dotnet/EcencyApi/Handlers/PrivateApi.Core.cs
  • dotnet/EcencyApi/Handlers/PrivateApi.Feeds.cs
  • dotnet/EcencyApi/Handlers/PrivateApi.Misc.cs
  • dotnet/EcencyApi/Handlers/PrivateApi.Payments.cs
  • dotnet/EcencyApi/Handlers/PrivateApi.UserData1.cs
  • dotnet/EcencyApi/Handlers/PrivateApi.UserData2.cs
  • dotnet/EcencyApi/Handlers/Routes.cs
  • dotnet/EcencyApi/Handlers/SearchApi.cs
  • dotnet/EcencyApi/Handlers/Spotlights.cs
  • dotnet/EcencyApi/Handlers/WalletApi.Engine.cs
  • dotnet/EcencyApi/Handlers/WalletApi.Layers.cs
  • dotnet/EcencyApi/Handlers/WalletApi.Market.cs
  • dotnet/EcencyApi/Infrastructure/ApiClient.cs
  • dotnet/EcencyApi/Infrastructure/B64u.cs
  • dotnet/EcencyApi/Infrastructure/HiveCrypto.cs
  • dotnet/EcencyApi/Infrastructure/HiveRpcClient.cs
  • dotnet/EcencyApi/Infrastructure/HttpContextExtensions.cs
  • dotnet/EcencyApi/Infrastructure/JsJson.cs
  • dotnet/EcencyApi/Infrastructure/JsVal.cs
  • dotnet/EcencyApi/Infrastructure/MemCache.cs
  • dotnet/EcencyApi/Infrastructure/Upstream.cs
  • dotnet/EcencyApi/Models/HiveEngine.cs
  • dotnet/EcencyApi/Program.cs
  • dotnet/EcencyApi/public/robots.txt
  • dotnet/README.md
  • dotnet/docker-compose.yml
  • dotnet/parity/README.md
  • dotnet/parity/driver.py
  • dotnet/parity/mock_upstream.py
  • dotnet/tools/gen-vectors.js

Comment thread dotnet/Dockerfile
Comment thread dotnet/EcencyApi/Infrastructure/JsJson.cs Outdated
Comment thread dotnet/EcencyApi/Infrastructure/JsJson.cs Outdated
Comment thread dotnet/parity/mock_upstream.py Outdated
- Gate PRs and deploys on dotnet test
- Build dotnet/Dockerfile as ecency/api:latest + ecency/api:sha-<commit>
  (every merge gets a durable rollback tag; deploy stays digest-pinned)
- Preserve the last Node build as ecency/api:node-legacy before first overwrite
- Drop the Node yarn/build jobs from CI (Node source stays in-repo)
- .dockerignore for the dotnet build context; README rollback instructions
Comment on lines +95 to +96
var bodyJson = payload?.ToJsonString(RawJsonOptions) ?? "{}";
req.Content = new StringContent(bodyJson, Encoding.UTF8, "application/json");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 GETs Still Send Bodies

When BaseApiRequest is called for a GET without a payload, this still creates a JSON body containing {} and attaches content headers. The Node service's axios path does not put a body on these GET requests, so reachable proxy calls like promoted-post lookups, engine reads, and Esplora balance reads can still behave differently when an upstream or intermediary rejects or varies on GET bodies.

Fix in Claude Code

}

var parsed = resp.Json;
var rawText = parsed?.ToJsonString() ?? "null";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Lamports Still Round

This builds the text scanner input from the parsed JSON instead of the original response text. Numeric JsonElement values are serialized through GetDouble() before ExtractSolanaLamports scans them, so a valid Solana getBalance response with a u64 lamport value above the exact integer range of double can already be rounded here. The balance route then returns the rounded lamport string instead of the exact value from the RPC response.

Fix in Claude Code

- Unify HiveClients into one client/node list (hapi.ecency.com decommissioned;
  also avoids cold-start timeouts on the dead first node)
- Express parity: res.send(null) -> empty body without Content-Type
- JsJson.FormatNumber: exact ECMA Number::toString thresholds (fixed notation
  for exponents in (-6,21], scientific outside), shared with JsVal string
  coercion; verified against 27 V8-generated vectors
- JsJson.IsTruthy: handle CLR-backed JsonValues without throwing
- express.urlencoded parity: parse form-encoded request bodies
- Upstream requests send bare application/json content-type like axios
- Dockerfile: run as the aspnet image's non-root app user
- parity/mock_upstream.py: portable log path (env-overridable)
@feruzm

feruzm commented Jul 11, 2026

Copy link
Copy Markdown
Member Author

Review feedback triaged — every claim was verified against the running Node image before acting. Fixes pushed in 440126b:

Fixed

  • res.send(null) parity (codex): verified against Express — a JSON null upstream body must produce an empty response body with no Content-Type. Now matched.
  • JSON.stringify number thresholds (coderabbit): confirmed against V8 — fixed-point for decimal exponents in (-6, 21], scientific outside. FormatNumber now implements the ECMA algorithm and is verified against 27 V8-generated vectors (new test).
  • IsTruthy could throw on programmatically-built JsonValues (coderabbit): now handles CLR-backed values.
  • application/x-www-form-urlencoded bodies (codex): now parsed like express.urlencoded (flat form fields).
  • Container now runs as the aspnet image's non-root app user (coderabbit); image rebuilt and smoke-tested healthy.
  • parity/mock_upstream.py hardcoded log path (coderabbit): portable default + env override.
  • Bonus parity tightening: upstream requests now send bare application/json (no charset), byte-matching axios.

Dismissed with evidence

  • "Empty GETs carry bodies" (greptile, P1 ×2): recorded the actual upstream traffic from the running Node image — axios sends {} with application/json on every GET (data: {...payload} always yields {}). The port matches the real behavior.
  • "Large lamports lose precision" (greptile, P1 ×2): System.Text.Json preserves raw numeric literals through parse→re-serialize (verified: a u64 above 2^53 survives ToJsonString() byte-exact), so the text scanner sees exact digits like Node's raw-text path. The GetDouble() fallback mirrors Node's own Math.trunc(parsed.result.value) — identical double rounding in both.
  • GetTokenPrice debug log (coderabbit nitpick): intentionally kept — the Node service logs the identical line on the same path; removing it belongs in both implementations at once.

Also in this push: the two Hive RPC client instances are unified into one node list — the node the explorer client preferred first has been decommissioned, so the split only added cold-start failover latency.

Full 305-case differential parity re-run after all changes: 0 mismatches. 41 unit tests green.

Comment on lines +95 to +99
var bodyJson = payload?.ToJsonString(RawJsonOptions) ?? "{}";
req.Content = new StringContent(bodyJson, Encoding.UTF8);
// axios sends bare "application/json" (no charset); keep upstream
// requests byte-identical to the Node service.
req.Content.Headers.ContentType =

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 GETs Still Send Bodies

BaseApiRequest still assigns StringContent for every request, so a GET with no payload becomes a request with body {} and Content-Type: application/json. This is reachable from bodyless proxy calls such as token refresh, Hive Engine reads, and the Esplora balance lookup. The Node/axios path sends those GETs without a body, so strict upstreams or intermediaries can still reject or vary these requests after the C# service takes over.

Fix in Claude Code

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was verified empirically before the port shipped: the recorded upstream traffic from the running production Node image shows axios sends a {} body with Content-Type: application/json on every GET — baseApiRequest always passes data: {...payload}, which is {} when no payload is given, and axios's Node adapter serializes and sends it. The C# port intentionally matches that real behavior byte-for-byte (including the bare application/json content type). Sending GETs without a body would be the divergence from what upstreams receive today.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, and I was wrong about this. I assumed axios suppresses the body on GETs by default, but baseApiRequest always passes data: {...payload} and axios's Node HTTP adapter serializes and sends it regardless of method. The C# code faithfully replicates the actual wire behavior rather than the idealized RFC behavior. Thanks for the empirical verification — that's exactly the kind of evidence that should override my assumption here.

Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with 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.

Inline comments:
In @.github/workflows/main.yml:
- Around line 17-27: Add a top-level permissions block granting only contents:
read, and update the checkout step in the test job to set persist-credentials:
false. Use the existing test job and actions/checkout@v4 step as the
implementation targets.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b8fbc8c6-d623-4ff6-a916-6d723fe4b4ca

📥 Commits

Reviewing files that changed from the base of the PR and between 4a023ab and 440126b.

📒 Files selected for processing (14)
  • .github/workflows/main.yml
  • dotnet/.dockerignore
  • dotnet/Dockerfile
  • dotnet/EcencyApi.Tests/HiveCryptoTests.cs
  • dotnet/EcencyApi.Tests/fixtures/crypto-vectors.json
  • dotnet/EcencyApi/Handlers/HiveExplorer.cs
  • dotnet/EcencyApi/Infrastructure/HiveRpcClient.cs
  • dotnet/EcencyApi/Infrastructure/HttpContextExtensions.cs
  • dotnet/EcencyApi/Infrastructure/JsJson.cs
  • dotnet/EcencyApi/Infrastructure/JsVal.cs
  • dotnet/EcencyApi/Infrastructure/Upstream.cs
  • dotnet/README.md
  • dotnet/parity/mock_upstream.py
  • dotnet/tools/gen-vectors.js
✅ Files skipped from review due to trivial changes (2)
  • dotnet/.dockerignore
  • dotnet/EcencyApi.Tests/fixtures/crypto-vectors.json
🚧 Files skipped from review as they are similar to previous changes (8)
  • dotnet/Dockerfile
  • dotnet/EcencyApi.Tests/HiveCryptoTests.cs
  • dotnet/tools/gen-vectors.js
  • dotnet/parity/mock_upstream.py
  • dotnet/EcencyApi/Infrastructure/JsVal.cs
  • dotnet/EcencyApi/Infrastructure/JsJson.cs
  • dotnet/EcencyApi/Infrastructure/Upstream.cs
  • dotnet/EcencyApi/Handlers/HiveExplorer.cs

Comment thread .github/workflows/main.yml
…lover, CI hardening

- Remove post-reblogs/post-reblog-count (both impls): no callers, no traffic,
  and they read route params their POST routes never declared, so every call
  queried undefined/undefined upstream
- Wire request-delete to its account-deletion acknowledgment stub (both impls):
  Hive accounts cannot be deleted on-chain; the endpoint satisfies the
  app-store requirement, but the old table sent it to the report handler whose
  validation 400'd the mobile payload. In the Node build the companion
  hs-token-refresh missing-code bug crashes the process outright (unhandled
  rejection), so bad requests bounce the service; the C# port is immune
- HiveRpcClient: adopt @ecency/sdk NodeHealthTracker design (simplified):
  per-node health state, 429 parking with Retry-After + escalating windows,
  recent-failure deprioritization, latency-EWMA best-first ordering with
  unproven prior; RPC-level errors still surface without failover
- CI: least-privilege permissions, no credential persistence on PR checkouts
- Parity: 301 cases, 0 unexplained mismatches; intentional changes recorded
  in the harness; 42 unit tests
@feruzm

feruzm commented Jul 11, 2026

Copy link
Copy Markdown
Member Author

Round 2 pushed in 7a89747 — endpoint cleanup, node-failover upgrade, and the new review items, verified end-to-end before pushing (42 unit tests; 301-case differential parity vs the running Node image with 0 unexplained mismatches).

Changes

  • Removed post-reblogs / post-reblog-count from both implementations: dead endpoints (no client callers) that also read route params their POST routes never declared, so every call queried undefined/undefined upstream.
  • request-delete now returns its account-deletion acknowledgment stub (200) in both implementations. Accounts cannot be deleted on Hive; the endpoint exists for the app-store account-deletion requirement, but the old route table pointed it at the report handler, whose validation rejected the mobile payload with 400.
  • HiveRpcClient adopts the health-tracker design from the vision-next SDK (simplified for proxy call rates): per-node failure/rate-limit state, 429 parking honoring Retry-After with escalating fallback windows, recent-failure deprioritization, and latency-EWMA best-first ordering. New tests cover parking and EWMA demotion.
  • Workflow hardening (coderabbit): top-level permissions: contents: read and persist-credentials: false on the PR-triggered test job.

Worth knowing: while re-verifying, the parity rig proved the Node missing-code bug on hs-token-refresh is not just a hung socket — the unhandled rejection crashes the whole Node process (the reference container exited mid-run at exactly that request). Under swarm's restart policy this silently bounces the service on any malformed request. The C# port returns 401 and is immune.

Dismissed again: the repeated "GETs still send bodies" finding — see the threaded reply; the recorded traffic from the production image shows axios sends {} on every GET, and matching that is the point of a parity port.

@feruzm feruzm merged commit 28a7b3e into main Jul 11, 2026
5 checks passed
@feruzm feruzm deleted the feature/csharp-port branch July 11, 2026 09:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant