Skip to content

Price/balance fetching, unified money model (decimal/numeric), wallet sync hardening#20

Merged
foxcool merged 14 commits into
mainfrom
integration
Jun 24, 2026
Merged

Price/balance fetching, unified money model (decimal/numeric), wallet sync hardening#20
foxcool merged 14 commits into
mainfrom
integration

Conversation

@foxcool

@foxcool foxcool commented Jun 3, 2026

Copy link
Copy Markdown
Owner

Summary

Builds out external price/balance fetching, unifies the money representation across
the stack, hardens wallet sync, and adds a smoke-test suite. Backend-only — the
frontend is not yet wired (NEXT_PUBLIC_USE_BACKEND=false).

Changes

Prices & balances

  • CoinGecko: ERC-20 lookup by contract address + native-coin symbol map; Binance spot
    prices; base-asset (USD/USDT) resolved/created on demand in FetchExternalPrices.
  • Moralis wallet sync: multi-chain auto-discovery, native + ERC-20 balances, merge by
    symbol. Provider mechanics moved behind a single WalletSyncer.SyncWallet(addr, chains)
    port — the portfolio service keeps only domain logic.
  • Holding.excluded flag (spam/scam tokens excluded from portfolio calculations).
  • Holdings inherit the account's portfolio_id via COALESCE(h.portfolio_id, a.portfolio_id).

Unified money representation

  • Single carrier end to end: Postgres numeric ↔ Go decimal.Decimal ↔ proto string
    (raw-integer model, value = amount / 10^decimals).
  • Migrated Holding.Amount, StoredPrice.Last/Open/High/Low/Close/Volume,
    PortfolioValueResponse.total_value_amount.
  • pgx shopspring-decimal codec registered via postgres.NewPool.
  • Fixes uint256 balance overflow (raw 18-decimal balances exceeded int64/bigint).

Tests & cleanup

  • Smoke-test suite under test/smoke/ (assets, portfolio, prices, wallet sync) against
    the live compose stack; make test-smoke.
  • Removed stale pkg/ folder.

Breaking changes

  • Proto: Holding.amount, Price.last/OHLCV, PortfolioValueResponse.total_value_amount
    are now string (were int64). Acceptable — no live API consumers yet.

Migration / ops

  • Schema: holdings.amount and prices.{last,open,high,low,close,volume}numeric.
    Apply with Atlas declarative (make schema-apply / atlas schema apply) — both are safe
    widening ALTERs (bigintnumeric), no drop/recreate.
  • New dep: github.com/jackc/pgx-shopspring-decimal. Pools must be built via
    postgres.NewPool so the codec is registered.

Testing

  • make test-unit, make test-integration (testcontainers), make test-smoke — all green.
  • Smoke verified live: Binance price fetch + vitalik.eth wallet sync (large uint256 balances)
    through the full decimal/numeric stack.

foxcool and others added 14 commits April 13, 2026 14:11
… value

- entity.NormalizeSymbol: canonical (trim+upper) symbol, shared store/portfolio
- store normalizes symbol on create/update/lookup -> case-insensitive, no dup assets
- resolveAssetID delegates normalization to store (single source of truth)
- SyncAccount merges same-symbol balances across chains by real quantity at max
  decimals (USDC 6dec + 18dec no longer corrupts the total); keyed by canonical symbol
- tests: mixed-decimals merge; symbol-resolution mocks pass input verbatim

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ed forex

FetchExternalPrices created every quote base asset as forex, mislabeling USDT
(a crypto stablecoin). Providers now declare BaseAssetType: CoinGecko USD -> forex,
Binance USDT -> cryptocurrency.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Response quote_asset_id echoes the request value, which may be a ticker symbol
(e.g. "USD") rather than a UUID. Document the field on request and response.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e import

Make the eye.v1 contract importable by sibling modules (greedy-eye-mcp).
Change go_package and buf.gen out dir internal/api -> api, regenerate,
update all import paths. Wire contract unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ListHoldings returned holdings of all users; ListPortfolios and
ListAccounts were already user-scoped. Filter by owning portfolio
(holding's own or inherited from account), falling back to account
owner for holdings outside any portfolio. To be replaced with a
portfolio-visibility predicate when shared ownership lands.
Scam tokens clone real symbols and merged into legitimate holdings:
a fake USDT airdrop (possible_spam=false but verified_contract=false)
summed with real Tether into a 4.1M USDT holding. Filter out tokens
flagged possible_spam or lacking verified_contract before they reach
the symbol-keyed merge.
The chains endpoint defaults to eth only when no chains parameter is
given, hiding L2/sidechain balances (ARB, optimism ETH, DAI were
missing). Pass the supported chain list explicitly and keep only
chains with actual transactions. The endpoint rejects scroll/zksync/
fantom, so those remain uncovered.
CreateRule failed with "user_id is required" because the handler never
populated UserID from the request context, unlike PortfolioService. Inject
the context user on CreateRule and default ListRules to the caller (with an
admin override), so rule creation works and listings are properly scoped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@foxcool foxcool merged commit 1f8eda1 into main Jun 24, 2026
5 checks passed
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