Skip to content

feat(account): add GET /account/balances with CHEQ/USD conversion - #814

Open
fraseragain wants to merge 4 commits into
developfrom
feat/account-balances-usd
Open

fraseragain wants to merge 4 commits into
developfrom
feat/account-balances-usd

Conversation

@fraseragain

@fraseragain fraseragain commented Sep 8, 2026

Copy link
Copy Markdown

What

Adds GET /account/balances — the on-chain balance of the authenticated customer's mainnet and testnet payment accounts, in ncheq, CHEQ and USD.

GET /account/balances            // auth: read:account (same as /account, /account/analytics)
{
  "mainnet": {
    "address": "cheqd1...",
    "denom": "ncheq",
    "balance": { "ncheq": "1500000000000", "cheq": 1500 },
    "usd": 3.03
  },
  "testnet": { "address": "cheqd1...", "denom": "ncheq", "balance": { "ncheq": "0", "cheq": 0 }, "usd": 0 },
  "rate": { "cheqUsd": 0.00202, "source": "coingecko", "asOf": "2026-09-08T13:00:00.000Z" }
}

Why

GET /account returns only the payment-account addresses. checkBalance from @cheqd/sdk was used internally (faucet top-up) but never surfaced, and there was no CHEQ→USD lookup anywhere. The Studio portal needs both to show customers their account funding status. Studio owns the price-feed dependency and caching (the same way it wraps Stripe); the portal just displays.

How

  • src/helpers/price.ts — new PriceHelper.getCheqUsdRate(). Fetches the CHEQ/USD spot rate from CoinGecko (coin id cheqd-network, matching cheqd/market-monitoring), caches it in-process for CHEQ_USD_RATE_CACHE_TTL seconds (default 300), 5s request timeout. Never throws — any failure logs and returns null.
  • src/helpers/denom.ts — new. Extracts ncheqToCheq / cheqToNcheq / toSafeFaucetAmount out of account.ts so the pure conversion logic has one home and is unit-testable in isolation.
  • account.tsgetBalances() controller method (inline @openapi, [Account] tag). getTestnetBalanceNcheq generalised to getBalanceNcheq(address, rpcUrl) with an 8s timeout guard (StargateClient exposes no cancellation).
  • Route in app.ts, read:account rule in account-auth.ts, AccountBalancesResponse / AccountNetworkBalance schemas in swagger-api-types.ts (+ regenerated swagger-api.json).
  • Config: COINGECKO_API_URL / COINGECKO_TOKEN_ID / COINGECKO_API_KEY / CHEQ_USD_RATE_CACHE_TTL (constants use the existing parseNumberEnv; documented in README.md + example.env).

Edge cases handled

  • Customer has no payment account on a network → that key is null.
  • A network's RPC query fails → that network's balance is null (the address is still returned); the other network is unaffected.
  • CoinGecko unavailable → rate is null and every usd is null; balances still returned; response is still 200.
  • Balances beyond Number.MAX_SAFE_INTEGER ncheq → conversion via Number(bigint) / 1e9, no throw (covered by a unit test).

Deliberate deviations from existing conventions

  1. Price cache lives inside PriceHelper (its own node-cache), not LocalStoreLocalStore is coupled to PaymentAccountEntity and sits under database/; a market-rate cache doesn't belong there.
  2. Per-network failure isolation + checkBalance timeout — the existing getTestnetBalanceNcheq lets RPC errors propagate. /account/balances isolates them so one RPC outage doesn't blank the other network or the addresses.
  3. Named swagger schema (AccountBalancesResponse) rather than fully inline like the newest /account/faucet block — the response is structured enough that a named schema (like AccountAnalyticsResponse) reads better.

Testing

  • npm run build (swagger + tsc) — clean.
  • npm run test:unit — 137/137 pass (6 new in tests/unit/account/balances.test.ts).
  • prettier --check 'src/**/*.ts' 'tests/**/*.ts' — clean.
  • New Playwright specs under tests/e2e/parallel/account/ (authenticated shape check + testnet address parity with /account, and an unauthenticated 401) — these need a deployed environment, so they run in CI, not locally.
  • Manually exercised end-to-end against a local Postgres-backed instance: returned the testnet address (matching /account), a live balance, and a live CoinGecko rate; with CoinGecko pointed at a dead URL, rate/usd were null, balances still returned, 200, no crash.

Follow-up

Per-account transaction history is tracked in the payment table (PaymentEntity) but has no read endpoint — raised as #815.

🤖 Generated with Claude Code

dubiouspip and others added 3 commits September 8, 2026 16:51
Returns the on-chain balance of the authenticated customer's mainnet and
testnet payment accounts in ncheq, CHEQ and USD. The CHEQ/USD rate is
sourced from CoinGecko (coin id `cheqd-network`) via a new PriceHelper and
cached in-process for CHEQ_USD_RATE_CACHE_TTL seconds.

The endpoint degrades gracefully: if the rate lookup or a network's RPC
endpoint is unavailable it still returns 200, with the affected
usd/rate/balance fields set to null. Per-network balance queries are
isolated so one RPC outage doesn't blank the other network or the
addresses, and checkBalance is bounded by an 8s timeout.

Also extracts the ncheq<->CHEQ helpers (cheqToNcheq, ncheqToCheq,
toSafeFaucetAmount) out of account.ts into helpers/denom.ts, and
generalises getTestnetBalanceNcheq to getBalanceNcheq(address, rpcUrl).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- unit: ncheqToCheq edge cases (zero, sub-CHEQ, > Number.MAX_SAFE_INTEGER)
  and the cheq * rate USD calculation
- e2e: authenticated shape check + testnet address parity with GET /account,
  and an unauthenticated 401 check

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants