Skip to content

portfolio-v2: engine and chain layers silently empty when the account fetch lands on a metadata-less RPC node #64

Description

@feruzm

Summary

/wallet-api/portfolio-v2 with onlyEnabled: true (what the web and mobile clients send) returns zero engine tokens and zero chain wallets for accounts that have explicitly enabled them. The response is a normal 200, the Hive layer is correct, and nothing is logged.

Root cause

The enabled-token allowlist is derived from the account's posting_json_metadata:

  • PortfolioV2 builds it via ExtractEnabledEngineTokenSymbols(accountData) (Handlers/WalletApi.Engine.cs), and ExtractExternalWallets reads the same metadata for the chain layer.
  • accountData comes from HiveExplorer.GetAccount -> HiveClients.Default.GetAccounts -> condenser_api.get_accounts.

Two nodes in HiveClients.Default (Infrastructure/HiveRpcClient.cs) serve accounts with posting_json_metadata always empty, while every other field (balances, reputation) is correct:

node posting_json_metadata
https://techcoderx.com empty for every account tested
https://hiveapi.actifit.io empty for every account tested
api.hive.blog, api.deathwing.me, rpc.mahdiyari.info, api.openhive.network full

Consequences:

  1. HiveRpcClient only validates the result shape (r is JsonArray), so a metadata-stripped 200 is recorded as a healthy response.
  2. NodeHealthTracker orders healthy nodes best-first by latency EWMA. These two nodes are among the fastest in the pool, so they win the ranking and serve most account fetches.
  3. ExtractEnabledEngineTokenSymbols returns an empty set, BuildEngineLayer(onlyEnabled: true, allowedSymbols: <empty>) drops every token, and the chain layer drops every wallet.
  4. Balances still look right, because the Hive layer reads account fields rather than metadata. The failure is invisible: no exception, no log line, HTTP 200.

This is the same class of bug as the malformed-200 case already described in CLAUDE.md under "Upstream node failover": a node that answers successfully but uselessly is scored as healthy and stays ranked first.

Reproduction

Against production, for an account with enabled engine tokens:

onlyEnabled: true   -> wallets: points + hive only, engine 0, chain 0
onlyEnabled: false  -> engine layer fully populated

Every symbol in the account's metadata allowlist is present in the unfiltered engine payload, so the data is fetched correctly and then discarded by the filter. ExtractEnabledEngineTokenSymbols itself is correct: fed a real account object it returns the expected symbol set.

Because node ranking shifts with latency, this presents to users as engine balances intermittently disappearing and reappearing rather than as a constant failure.

Proposed fix

  1. Treat a metadata-less account response as a node failure in the typed get_accounts helper, so it fails over instead of being cached as healthy — mirroring the existing result-shape validation. The signal needs to distinguish a pruned node from an account that genuinely has no metadata; a node returning empty metadata for an account that has a non-empty json_metadata/profile elsewhere, or a capability probe recorded per node, both work.
  2. Drop or hard-deprioritize nodes that do not serve account metadata, so the portfolio path never depends on latency ranking for correctness.
  3. Stop degrading silently: when onlyEnabled: true and the metadata could not be read, the request should not render as "user has no tokens". Surface it (error or explicit partial-response marker) so clients can retry rather than paint an empty wallet.

Items 1 and 2 are the fix; item 3 is what turns the next occurrence into something detectable.

Tests

Extend HiveRpcFailoverTests with a stub node that returns a well-formed account carrying empty metadata, and assert the client fails over to a node that serves it.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions