Skip to content

feat(eth-indexer): event-driven AUDIO balance indexer#849

Merged
raymondjacobson merged 2 commits into
mainfrom
api/eth-balance-indexer
May 22, 2026
Merged

feat(eth-indexer): event-driven AUDIO balance indexer#849
raymondjacobson merged 2 commits into
mainfrom
api/eth-balance-indexer

Conversation

@raymondjacobson
Copy link
Copy Markdown
Member

Summary

  • New eth-indexer entrypoint that mirrors solana-indexer's shape: New(cfg) + Start(ctx) error + Close() + GetHealth(), plus a sibling fiber Server on :1325 exposing GET /eth/health.
  • Event-driven instead of polling: opens a persistent WSS subscription via eth_subscribe to the AUDIO Transfer log topic. On each event, joins from/to against users.wallet ∪ chain='eth' associated_wallets, fans out balanceOf + totalStakedFor + getTotalDelegatorStake in parallel for matches, and upserts the sum into the new eth_wallet_balances table.
  • Resumable via eth_indexer_checkpoints — on (re)connect we eth_getLogs the gap (9K-block chunks) and replay.

This is the api-side replacement for the discovery-provider's cache_user_balance job that currently feeds associated_wallets_balance on GET /v1/users/handle/.... Same three contract reads, same result, but live via WS instead of 30s polling.

Why event-driven

For a baseline of ~50K AUDIO transfers/day across ~5M tracked wallets, this approach issues roughly one balance read per actual on-chain transfer — vs. hundreds of thousands of speculative eth_getBalance calls per day for a periodic poll-everyone approach. RPC cost scales with on-chain activity, not user count.

Files

Config

env var required default
ethRpcUrl yes
ethWsUrl no auto-derived from ethRpcUrl (https→wss)
ethAudioContractAddress no 0x18aAA7115705e8be94bfFEbDE57Af9BFc265B998
ethStakingContractAddress no 0xe6D97B2099F142513be7A2a068bE040656Ae4591
ethDelegateManagerContractAddress no 0x4d7968ebfD390D5E7926Cb3587C39eFf2F9FB225

If ethRpcUrl/ethWsUrl are unset the indexer logs a warning and idles until shutdown — safe to deploy without a provider key.

Test plan

  • go build ./... clean
  • go vet ./... clean
  • Smoke test of the 3 contract reads via cmd/eth_smoke against mainnet:
    • rayjacobson primary (0x7d273…b060) → 0/0/0 ✓ (matches discovery-provider balance: "0")
    • Staking contract self (0xe6D9…4591) → 247,024,527 AUDIO ✓
  • End-to-end local run:
    • Seeded a known active AUDIO holder as a tracked associated_wallet
    • Pre-set checkpoint 9000 blocks back to force backfill
    • Indexer found a Transfer involving the seed wallet, called all 3 contracts, upserted 80975640000000000000000 wei
    • Cross-checked the persisted value with cmd/eth_smoke against the same holder — bytes match exactly
    • GET /eth/health returned connected: true, advanced checkpoint, correct tracked/cached counts
  • Deploy plan: set ethRpcUrl (and optionally ethWsUrl) in stage; let it run for ~24h and confirm eth_wallet_balances populates as AUDIO transfers occur

Out of scope / follow-ups

  • This populates eth_wallet_balances but does not yet roll it up into a /v1/users/... API response field. That can be a follow-up PR (or a SQL view) once the data is flowing.
  • No on-demand "force refresh wallet X" endpoint yet — every refresh is event-triggered. We can add a manual hook if support tickets demand it.

🤖 Generated with Claude Code

raymondjacobson and others added 2 commits May 22, 2026 13:27
Adds a new `eth-indexer` entrypoint that mirrors the structure of the
solana-indexer, but is event-driven instead of slot-polling.

Background: the discovery-provider's `cache_user_balance` task feeds the
`associated_wallets_balance` field on `GET /v1/users/...` by polling
`eth_getLogs` on the AUDIO ERC-20 contract every 30s and calling
balanceOf + totalStakedFor + getTotalDelegatorStake only for wallets that
just moved AUDIO. This is the api-side replacement.

The new indexer:
- Opens a persistent WSS connection to the configured ETH provider and
  subscribes via `eth_subscribe` to the AUDIO Transfer log topic. No
  polling baseline; live events stream in as they're mined.
- On every event, parses from/to from the indexed topics, joins them
  against `users.wallet ∪ chain='eth' associated_wallets`, and fans out
  three contract reads (balanceOf + totalStakedFor +
  getTotalDelegatorStake) in parallel for each tracked address.
- Sums the three values and upserts a single row per wallet into a new
  `eth_wallet_balances` table — shape (wallet, balance, blocknumber,
  updated_at, created_at) so per-user totals can be rolled up via the
  same JOIN pattern Solana uses.
- Persists a resume checkpoint to `eth_indexer_checkpoints` after every
  processed block so reconnects backfill via `eth_getLogs` (chunks of
  9000 blocks) for any gap, then resume the live subscription.
- Exposes `GET /eth/health` on :1325 reporting connected, last block
  seen, checkpoint block, tracked vs cached wallet counts, and an
  optional max_event_lag_secs trip wire.

Mainnet contract addresses default to the values in
`packages/sdk/src/sdk/config/production.ts`:
  AUDIO            0x18aAA7115705e8be94bfFEbDE57Af9BFc265B998
  Staking          0xe6D97B2099F142513be7A2a068bE040656Ae4591
  DelegateManager  0x4d7968ebfD390D5E7926Cb3587C39eFf2F9FB225

Config (env vars):
  ethRpcUrl                          - HTTPS endpoint (required)
  ethWsUrl                           - WSS endpoint (auto-derived from
                                       ethRpcUrl when unset)
  ethAudioContractAddress            - overrides
  ethStakingContractAddress          - overrides
  ethDelegateManagerContractAddress  - overrides

If `ethRpcUrl`/`ethWsUrl` are unset the indexer logs a warning and idles
until ctx.Done() so it's safe to deploy without a provider key.

Includes `cmd/eth_smoke` — a one-shot CLI that runs the same three
contract reads for a given holder against ethRpcUrl, useful for ops
debugging.

Local smoke test (this repo):
  1. docker compose up -d db
  2. apply migration 0203
  3. seed users.wallet and an associated_wallets row
  4. set eth_indexer_checkpoints to current-9000
  5. `writeDbUrl=... ethRpcUrl=... go run main.go eth-indexer`
  Backfill walked 9000 blocks, picked up a Transfer involving the
  seeded address, called the 3 contracts, and upserted the correct sum.
  Cross-checked the upserted balance with `go run ./cmd/eth_smoke
  <addr>` — bytes match.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@raymondjacobson raymondjacobson merged commit 8f53b82 into main May 22, 2026
5 checks passed
@raymondjacobson raymondjacobson deleted the api/eth-balance-indexer branch May 22, 2026 21:25
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