Commit 8f53b82
feat(eth-indexer): event-driven AUDIO balance indexer (#849)
## 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
-
[eth/indexer/eth_indexer.go](https://github.com/AudiusProject/api/blob/api/eth-balance-indexer/eth/indexer/eth_indexer.go)
— `EthIndexer` struct with WS subscription loop, backfill, fan-out
balance reads, upsert, checkpoint
-
[eth/indexer/server.go](https://github.com/AudiusProject/api/blob/api/eth-balance-indexer/eth/indexer/server.go)
— fiber app on `:1325`, `GET /eth/health`
-
[ddl/migrations/0203_eth_wallet_balances.sql](https://github.com/AudiusProject/api/blob/api/eth-balance-indexer/ddl/migrations/0203_eth_wallet_balances.sql)
— `eth_wallet_balances` + `eth_indexer_checkpoints`
-
[config/config.go](https://github.com/AudiusProject/api/blob/api/eth-balance-indexer/config/config.go)
— env wiring + mainnet defaults
-
[main.go](https://github.com/AudiusProject/api/blob/api/eth-balance-indexer/main.go)
— `case "eth-indexer":` parallels `case "solana-indexer":`
-
[cmd/eth_smoke/main.go](https://github.com/AudiusProject/api/blob/api/eth-balance-indexer/cmd/eth_smoke/main.go)
— one-shot CLI that runs the same three contract reads for a given
holder, for ops debugging
## 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
- [x] `go build ./...` clean
- [x] `go vet ./...` clean
- [x] 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 ✓
- [x] 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](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>1 parent 94100cf commit 8f53b82
5 files changed
Lines changed: 735 additions & 9 deletions
File tree
- config
- ddl/migrations
- eth/indexer
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
37 | 37 | | |
38 | 38 | | |
39 | 39 | | |
40 | | - | |
41 | | - | |
42 | | - | |
43 | | - | |
44 | | - | |
45 | | - | |
46 | | - | |
47 | | - | |
48 | | - | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
49 | 65 | | |
50 | 66 | | |
51 | 67 | | |
| |||
81 | 97 | | |
82 | 98 | | |
83 | 99 | | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
84 | 105 | | |
85 | 106 | | |
86 | 107 | | |
| |||
100 | 121 | | |
101 | 122 | | |
102 | 123 | | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
103 | 145 | | |
104 | 146 | | |
105 | 147 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
0 commit comments