C# (.NET 10) port of the API service#48
Conversation
…ve golden vectors
…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 SummaryThis PR ports the API service to ASP.NET Core under
Confidence Score: 5/5This looks safe to merge.
Important Files Changed
Reviews (4): Last reviewed commit: "fix: dead-endpoint cleanup, request-dele..." | Re-trigger Greptile |
| var bodyJson = payload?.ToJsonString(RawJsonOptions) ?? "{}"; | ||
| req.Content = new StringContent(bodyJson, Encoding.UTF8, "application/json"); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
💡 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".
| if (!isJson || ctx.Request.ContentLength is 0) | ||
| { | ||
| return new JsonObject(); |
There was a problem hiding this comment.
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 👍 / 👎.
| // Objects, arrays, booleans, null -> res.json() | ||
| ctx.Response.ContentType = "application/json; charset=utf-8"; | ||
| await ctx.Response.WriteAsync(json?.ToJsonString(RawJsonOptions) ?? "null"); |
There was a problem hiding this comment.
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 👍 / 👎.
📝 WalkthroughWalkthroughThe 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
Validation and parity
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (6)
dotnet/EcencyApi/Handlers/WalletApi.Market.cs (1)
462-468: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winRemove or guard the per-lookup debug log.
GetTokenPriceruns once per token per layer, so for any non-USD currency thisConsole.WriteLinefires 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
WithTimeoutleaves the timer running and can orphan a faulted task.When the timeout wins,
taskis abandoned; if it later faults, its exception is unobserved (TaskScheduler noise / potential process-level handler). TheTask.Delaytimer 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 tradeoffConsider reusing the shared
JsVal.ToJsStringcoercion.This
JsToStringis duplicated near-verbatim inSearchApi,UserData1Helpers.Template, andUserData2Js.JsString, whileWalletApialready relies on the canonicalJsVal.ToJsString. Consolidating avoids the four copies drifting apart. Please confirmJsVal.ToJsStringreproduces 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 winTwo divergent ports of
signupClientIp().This copy returns only the first
x-real-ipvalue, whilePaymentsSignupClientIpinPrivateApi.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 samePrivateApipartial 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 winDuplicated Turnstile verifier.
VerifyTurnstile/PostTurnstileFormhere andPaymentsVerifyTurnstileinPrivateApi.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 liftConsolidate the JS
Number(string)coercion.
JsNumberStringandChainJsNumberalready diverge on whitespace trimming,Infinityhandling, 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
⛔ Files ignored due to path filters (3)
dotnet/EcencyApi/public/favicon.icois excluded by!**/*.icodotnet/EcencyApi/public/favicon.pngis excluded by!**/*.pngdotnet/EcencyApi/public/logo512.pngis excluded by!**/*.png
📒 Files selected for processing (46)
dotnet/.gitignoredotnet/Dockerfiledotnet/EcencyApi.Tests/EcencyApi.Tests.csprojdotnet/EcencyApi.Tests/HiveCryptoTests.csdotnet/EcencyApi.Tests/HiveRpcFailoverTests.csdotnet/EcencyApi.Tests/JsValTests.csdotnet/EcencyApi.Tests/fixtures/crypto-vectors.jsondotnet/EcencyApi/Config.csdotnet/EcencyApi/EcencyApi.csprojdotnet/EcencyApi/Handlers/Announcements.csdotnet/EcencyApi/Handlers/AuthApi.csdotnet/EcencyApi/Handlers/ChainProviders.csdotnet/EcencyApi/Handlers/Constants.csdotnet/EcencyApi/Handlers/HiveExplorer.csdotnet/EcencyApi/Handlers/PrivateApi.Accounts.csdotnet/EcencyApi/Handlers/PrivateApi.Chain.csdotnet/EcencyApi/Handlers/PrivateApi.Core.csdotnet/EcencyApi/Handlers/PrivateApi.Feeds.csdotnet/EcencyApi/Handlers/PrivateApi.Misc.csdotnet/EcencyApi/Handlers/PrivateApi.Payments.csdotnet/EcencyApi/Handlers/PrivateApi.UserData1.csdotnet/EcencyApi/Handlers/PrivateApi.UserData2.csdotnet/EcencyApi/Handlers/Routes.csdotnet/EcencyApi/Handlers/SearchApi.csdotnet/EcencyApi/Handlers/Spotlights.csdotnet/EcencyApi/Handlers/WalletApi.Engine.csdotnet/EcencyApi/Handlers/WalletApi.Layers.csdotnet/EcencyApi/Handlers/WalletApi.Market.csdotnet/EcencyApi/Infrastructure/ApiClient.csdotnet/EcencyApi/Infrastructure/B64u.csdotnet/EcencyApi/Infrastructure/HiveCrypto.csdotnet/EcencyApi/Infrastructure/HiveRpcClient.csdotnet/EcencyApi/Infrastructure/HttpContextExtensions.csdotnet/EcencyApi/Infrastructure/JsJson.csdotnet/EcencyApi/Infrastructure/JsVal.csdotnet/EcencyApi/Infrastructure/MemCache.csdotnet/EcencyApi/Infrastructure/Upstream.csdotnet/EcencyApi/Models/HiveEngine.csdotnet/EcencyApi/Program.csdotnet/EcencyApi/public/robots.txtdotnet/README.mddotnet/docker-compose.ymldotnet/parity/README.mddotnet/parity/driver.pydotnet/parity/mock_upstream.pydotnet/tools/gen-vectors.js
- 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
| var bodyJson = payload?.ToJsonString(RawJsonOptions) ?? "{}"; | ||
| req.Content = new StringContent(bodyJson, Encoding.UTF8, "application/json"); |
There was a problem hiding this comment.
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.
| } | ||
|
|
||
| var parsed = resp.Json; | ||
| var rawText = parsed?.ToJsonString() ?? "null"; |
There was a problem hiding this comment.
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.
- 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)
|
Review feedback triaged — every claim was verified against the running Node image before acting. Fixes pushed in 440126b: Fixed
Dismissed with evidence
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. |
| 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 = |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
📒 Files selected for processing (14)
.github/workflows/main.ymldotnet/.dockerignoredotnet/Dockerfiledotnet/EcencyApi.Tests/HiveCryptoTests.csdotnet/EcencyApi.Tests/fixtures/crypto-vectors.jsondotnet/EcencyApi/Handlers/HiveExplorer.csdotnet/EcencyApi/Infrastructure/HiveRpcClient.csdotnet/EcencyApi/Infrastructure/HttpContextExtensions.csdotnet/EcencyApi/Infrastructure/JsJson.csdotnet/EcencyApi/Infrastructure/JsVal.csdotnet/EcencyApi/Infrastructure/Upstream.csdotnet/README.mddotnet/parity/mock_upstream.pydotnet/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
…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
|
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
Worth knowing: while re-verifying, the parity rig proved the Node missing- Dismissed again: the repeated "GETs still send bodies" finding — see the threaded reply; the recorded traffic from the production image shows axios sends |
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) → buildsdotnet/Dockerfile→ pushesecency/api:latestandecency/api:sha-<commit>→ deploys by immutable digest to all regions, exactly like before. The live service env vars carry over untouched (docker service update --imagepreserves 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-legacybefore the first C# image overwrites:latest(the deploy hosts prune local images, so rollback pulls from the registry):The Node source stays in the repo unchanged (apart from the route-table fixes below, mirrored in both implementations).
Verification
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.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.appuser, serves, built-in HEALTHCHECK reports healthy.Behavior changes (intentional, both implementations)
/private-api/request-deletenow 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-reblogsand/private-api/post-reblog-countremoved: no client callers, no traffic, and they read route params their POST routes never declared, so they always queriedundefined/undefinedupstream.hs-token-refreshwith a missingcodereturns 401 instead of the Node behavior (an unhandled async rejection that left the socket hanging).Node failover
HiveRpcClientkeeps 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 honoringRetry-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.mdcovers 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.