Skip to content

Feat/token metadata caching - #46

Merged
Miracle656 merged 2 commits into
Miracle656:mainfrom
K1NGD4VID:feat/token-metadata-caching
Sep 1, 2026
Merged

Feat/token metadata caching#46
Miracle656 merged 2 commits into
Miracle656:mainfrom
K1NGD4VID:feat/token-metadata-caching

Conversation

@K1NGD4VID

Copy link
Copy Markdown
Contributor

Summary

Related issue

Type of change

  • Bug fix
  • New feature
  • Refactor
  • Docs
  • Tests
  • CI / tooling

Checklist

  • I have read CONTRIBUTING.md
  • npx tsc --noEmit passes
  • npm run build passes
  • I added / updated tests where relevant
  • I updated docs where relevant

@drips-wave

drips-wave Bot commented Apr 25, 2026

Copy link
Copy Markdown

@K1NGD4VID Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@Miracle656 Miracle656 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Token metadata caching is the right approach for #37. Three things to address:

1. Remove scratch/generate_fixtures.js
Same as #45 — this debug script shouldn't be committed. git rm scratch/generate_fixtures.js.

2. Stacked on #45
Your diff includes all of #45's changes (decoder rename, decoder tests, fixtures). If #45 merges first your diff will shrink automatically to just the caching additions. Please either: (a) wait for #45 to merge and rebase, or (b) confirm you want this PR to supersede #45 and we'll close that one.

3. prisma/schema.prisma changes
You're adding to the Prisma schema here and also in #48. Make sure only one PR owns schema changes to avoid conflicts.

@Miracle656 Miracle656 mentioned this pull request Apr 26, 2026
11 tasks
@K1NGD4VID
K1NGD4VID force-pushed the feat/token-metadata-caching branch from b0a2f1c to 7555083 Compare April 26, 2026 08:36
@K1NGD4VID

Copy link
Copy Markdown
Contributor Author

I've addressed the feedback for both PR #37 (feat/token-metadata-caching) and PR #45 (test/xdr-decoder-unit-tests).

Here’s the summary of the changes:

Removed scratch/generate_fixtures.js: I've removed this script from the feat/token-metadata-caching branch. I also ensured it was cleaned up on the test/xdr-decoder-unit-tests branch as previously requested.
Stacked on #45: I've rebased feat/token-metadata-caching onto test/xdr-decoder-unit-tests. This ensures that once #45 is merged into main, the diff for the caching PR will automatically shrink to only show the metadata caching logic.
Resolved Prisma Schema Conflict: To address the concern about multiple PRs owning schema changes, I've rebased feat/accounts-balance-endpoint (#48) onto feat/token-metadata-caching (#37). This makes #37 the "owner" of the TokenMetadata model in the schema, while #48 now simply inherits it as part of its base.
Updated Branch State:

feat/token-metadata-caching: Rebased on #45, script removed, force-pushed.
feat/accounts-balance-endpoint: Rebased on #37, force-pushed.
Everything is now cleanly organized and ready for the next round of reviews!

@K1NGD4VID
K1NGD4VID requested a review from Miracle656 April 26, 2026 08:45

@Miracle656 Miracle656 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Good progress — scratch/generate_fixtures.js is gone. The token metadata cache (memory → DB → RPC) is the right architecture.\n\nOne thing to fix before merging:\n\nRebase on main — PR #45 (XDR decoder) merged today, so your branch still carries all of #45's changes in the diff. After rebasing, the diff will shrink to just the caching-related additions (tokenCache.ts, tokenCache.test.ts, schema changes, etc.) making it much easier to review in isolation.\n\n\ngit fetch origin\ngit rebase origin/main\ngit push --force-with-lease\n\n\nOnce rebased I'll approve immediately.

@Miracle656 Miracle656 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

This PR has fallen behind main and now shows merge conflicts (and it's been a few weeks since the last update). The feature itself is welcome — could you rebase on the latest main and resolve the conflicts? Once it's mergeable I'll review and merge. Thanks!

@Miracle656

Copy link
Copy Markdown
Owner

Friendly nudge — this is CONFLICTING, and its token-metadata caching overlaps the tokenCache work in your #48. Please consolidate the two into a single focused PR (or scope each clearly), rebase on main, and address the review.

If you're still up for it, please rebase on the latest main and address the review by 2026-07-01 — otherwise I'll close this to keep the queue tidy (you can always reopen). Thanks for contributing! 🙏

@K1NGD4VID

Copy link
Copy Markdown
Contributor Author

This PR has CHANGES_REQUESTED review status and only the GitGuardian security check has run — no CI workflow (typecheck/build/tests) results are present. Please address the review feedback and ensure CI is triggered.

@Miracle656

Copy link
Copy Markdown
Owner

This is conflicting with main and needs a rebase — src/api.ts in particular has changed with the routes that landed since this was opened. Please rebase onto current main and resolve, then I'll review the token-metadata caching. Thanks!

The cache was complete but unreachable: initTokenCache and getTokenMetadata
were imported into src/indexer.ts and never called, so TokenMetadata stayed
empty and GET /tokens always returned []. Same shape of failure as Miracle656#137 —
nothing errors, the feature simply never runs.

Wiring:
- initTokenCache(net) when a loop starts, so the first batch does not pay an
  RPC round-trip per contract already in the database.
- pollOnce resolves metadata for each distinct contract in the batch. Only a
  cache miss reaches RPC, and a miss happens once per contract for the life of
  the database, so this is one extra call the first time a token is seen and
  free after. Best-effort: a token whose metadata cannot be read is still
  worth indexing transfers for.
- GET /tokens is scoped to the selected network and names it in the response.

Network keying, which this branch predates:
- TokenMetadata was keyed `contractId @id`. A contract id is only unique
  within a chain, so a testnet token at the same address as a mainnet one
  would share the row — serving the wrong symbol and, far worse, the wrong
  `decimals`, which silently rescales every amount rendered from that token by
  orders of magnitude. Now @@id([network, contractId]), with the in-memory
  cache keyed `network:contractId` to match.
- fetchTokenMetadata took no network and used getRpc() plus a passphrase read
  from STELLAR_NETWORK. With one loop per network (Miracle656#161) that simulates a
  mainnet contract call against testnet, returning nothing or a different
  token. It now takes the network and derives both from it.
- Added the migration this PR never had (20260901140000), ordered after
  add_network for the same reason as the others.

Tests: added _resetTokenCache so cases stop leaking module-level state into
each other — the original beforeEach had a comment noting it could not clear
the map — plus coverage that the same contract id on two networks resolves to
two different tokens, that the RPC is asked for the network the answer is
cached under, and that a failed DB seed still serves from RPC.

Merge: three import conflicts in api.ts, indexer.ts and rpc.ts, all additive
unions with main.

tsc clean; full suite 396 passed.

@Miracle656 Miracle656 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Approved and merging — I wired it in and rebased it onto main (4d34d3a), since the wave has closed.

The cache design is right: memory → database → RPC, with the database tier there so a restart or a second process doesn't re-pay RPC for tokens already resolved. That middle tier is the one people skip, and it's the one that matters when you're running more than one instance.

It was unreachable, though. initTokenCache and getTokenMetadata were imported into src/indexer.ts and never called, so TokenMetadata stayed empty and GET /tokens always returned []. Same shape as the tombstone detector in #155 — nothing errors, no test goes red, the feature simply never runs.

Now wired:

  • initTokenCache(net) at loop start, so the first batch doesn't pay a round-trip per contract already in the database.
  • pollOnce resolves metadata for each distinct contract in the batch. Only a miss reaches RPC, and a miss happens once per contract for the life of the database — so this is one extra call the first time a token is seen and free thereafter. Best-effort: a token whose metadata can't be read is still worth indexing transfers for.
  • GET /tokens is scoped to the selected network and names it in the response.

The network keying is the substantive change, and this is the one I'd flag hardest. The model was contractId @id. A contract id is only unique within a chain, so a testnet token at the same address as a mainnet one shares the row. That gets you the wrong symbol — which someone would notice — and the wrong decimals, which nobody would: it silently rescales every amount rendered from that token by orders of magnitude, and the number still looks like a plausible number. Now @@id([network, contractId]), with the in-memory map keyed network:contractId to match.

Same class of bug in fetchTokenMetadata: no network parameter, getRpc() for the endpoint, and a passphrase read straight from STELLAR_NETWORK. With one loop per network (#161) that simulates a mainnet contract call against testnet and gets back nothing, or a different token. It takes the network now and derives both from it.

I also added the migration this PR never had, ordered after add_network.

On the tests — the original beforeEach carried this comment:

// Clear the internal Map by some means?
// Since it's a module-level constant, I might need to reset it.

That instinct was correct and worth acting on: without a reset, a "cache hit" assertion can be satisfied by a value some earlier test left behind, and the test passes for the wrong reason. There's a _resetTokenCache export now. Added coverage that the same contract id on two networks resolves to two different tokens, that the RPC is asked for the network the answer gets cached under, and that a failed DB seed still serves from RPC — a cold cache is recoverable, refusing to start is not.

Verified: tsc --noEmit clean, full suite 396 passed.

Good idea, and it now actually runs.

@Miracle656
Miracle656 merged commit 0ed5ce7 into Miracle656:main Sep 1, 2026
3 of 4 checks passed
Miracle656 added a commit to K1NGD4VID/wraith that referenced this pull request Sep 1, 2026
Derived per-token balances for an address, summing what it received and
subtracting what it sent across the indexed history.

Rebased onto main, which has moved a long way since this branch: the token
cache landed via Miracle656#46 and the metrics module via Miracle656#175, so those parts of this
PR are dropped as duplicates and what remains is the balance endpoint itself.

Three fixes to the query on the way in:

- The table reference was unqualified, `FROM "TokenTransfer"`. Every other raw
  query in db.ts uses `"wraith"."TokenTransfer"`, because the models declare
  @@Schema("wraith") — unqualified it resolves only if search_path happens to
  include the schema, so it would work locally and fail on a deployment that
  sets search_path differently.

- No network predicate. Summing both chains' transfers for one address gives a
  number that corresponds to no balance anywhere. Now takes the network and
  filters on it, with the route reading it from the selector so an unknown
  network 400s instead of silently answering for the default.

- The metrics timer was started and stopped around the query but not in a
  finally, so a throw leaked it. Uses observeDbQuery, which times failures
  too — a query that takes eight seconds and then fails is the one worth
  seeing.

Mounted on the existing accounts router rather than a second one, so it sits
beside /summary and /transfers and inherits the network middleware.

The response keeps this PR's honesty about what the number is — a sum over the
indexed window, not an on-chain read — and returns both the raw stroop amount
and the display string, so a consumer doing arithmetic does not have to parse
the decimal back and guess the scale.

tsc clean; full suite 402 passed.
Miracle656 added a commit that referenced this pull request Sep 1, 2026
* feat: implement tiered token metadata caching with Prisma persistence and RPC fallback

* feat: implement Prometheus metrics collection with registry and endpoint testing

* feat: implement accounts balance route with ledger-derived token balances

* chore: add vitest as devDependency for test:integration

* fix: exclude broken upstream tests from jest (opa, integration)

* Add GET /accounts/:address/balance, network-scoped and schema-qualified

Derived per-token balances for an address, summing what it received and
subtracting what it sent across the indexed history.

Rebased onto main, which has moved a long way since this branch: the token
cache landed via #46 and the metrics module via #175, so those parts of this
PR are dropped as duplicates and what remains is the balance endpoint itself.

Three fixes to the query on the way in:

- The table reference was unqualified, `FROM "TokenTransfer"`. Every other raw
  query in db.ts uses `"wraith"."TokenTransfer"`, because the models declare
  @@Schema("wraith") — unqualified it resolves only if search_path happens to
  include the schema, so it would work locally and fail on a deployment that
  sets search_path differently.

- No network predicate. Summing both chains' transfers for one address gives a
  number that corresponds to no balance anywhere. Now takes the network and
  filters on it, with the route reading it from the selector so an unknown
  network 400s instead of silently answering for the default.

- The metrics timer was started and stopped around the query but not in a
  finally, so a throw leaked it. Uses observeDbQuery, which times failures
  too — a query that takes eight seconds and then fails is the one worth
  seeing.

Mounted on the existing accounts router rather than a second one, so it sits
beside /summary and /transfers and inherits the network middleware.

The response keeps this PR's honesty about what the number is — a sum over the
indexed window, not an on-chain read — and returns both the raw stroop amount
and the display string, so a consumer doing arithmetic does not have to parse
the decimal back and guess the scale.

tsc clean; full suite 402 passed.

---------

Co-authored-by: Miracle656 <iupacnumen2020@gmail.com>
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.

2 participants