Feature: Ticket 4 v1 unified address balances endpoint - #79
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d283a55609
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| holdings, err := h.unifiedReader.GetAddressTokenPortfolio(ctx, addr) | ||
| if err == nil { | ||
| for _, holding := range holdings { | ||
| assetCode := derefOrDefault(holding.AssetCode, derefOrDefault(holding.Symbol, "")) |
There was a problem hiding this comment.
Keep contract holdings keyed by contract ID
When address_balances_current already returned a custom Soroban token, that row keeps asset_code empty and puts the token symbol in symbol (see the materialized insert in silver-realtime-transformer/go/transformer.go). This fallback copies holding.Symbol into AssetCode, and unifiedBalanceKey includes AssetCode, so the same contract_id is not de-duplicated and deployments with both materialized balances and unifiedReader can return two rows for one custom token.
Useful? React with 👍 / 👎.
| ContractID: holding.ContractID, | ||
| Symbol: holding.Symbol, | ||
| BalanceRaw: strconv.FormatInt(holding.BalanceRaw, 10), | ||
| BalanceDisplay: holding.Balance, |
There was a problem hiding this comment.
Format fallback token balances with their decimals
For Soroban holdings that come from GetAddressTokenPortfolio, holding.Balance was formatted with the hard-coded 7-decimal formatStroopsLocal before registry decimals are applied. When token_decimals is anything other than 7 and the materialized balance is absent or incomplete, this endpoint returns a balance string that disagrees with the decimals field; for example the added test data with raw 5000 and decimals 2 would display as 0.0005000 instead of 50.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
This PR adds a new “unified balances” API endpoint that returns XLM, classic trustlines, and Soroban token holdings for a given Stellar address in a single normalized response, including additional activity metadata.
Changes:
- Added
/api/v1/silver/addresses/{addr}/balancesroute and handler to compose balances from materialized address balances, classic account state, and indexed token transfer history. - Extended
TokenHoldingand DuckDB portfolio query to include last-activity ledger (last_activity_ledger) alongside last-activity timestamp. - Added tests for unified balances composition and address validation.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| obsrvr-lake/stellar-query-api/go/unified_duckdb_reader.go | Extends token portfolio SQL + scan to return last_ledger and expose it via TokenHolding. |
| obsrvr-lake/stellar-query-api/go/silver_reader.go | Adds LastLedger to TokenHolding JSON model for token holdings responses. |
| obsrvr-lake/stellar-query-api/go/routes_silver.go | Registers the new unified balances endpoint and logs it on startup. |
| obsrvr-lake/stellar-query-api/go/handlers_unified_balances.go | Implements unified balances handler + composition logic + address validation helpers. |
| obsrvr-lake/stellar-query-api/go/handlers_unified_balances_test.go | Adds DuckDB-backed tests for unified balances composition and address validation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| decimals := holding.Decimals | ||
| add(UnifiedAddressBalance{ | ||
| AssetType: assetType, | ||
| AssetCode: assetCode, | ||
| AssetIssuer: holding.AssetIssuer, | ||
| ContractID: holding.ContractID, | ||
| Symbol: holding.Symbol, | ||
| BalanceRaw: strconv.FormatInt(holding.BalanceRaw, 10), | ||
| BalanceDisplay: holding.Balance, | ||
| Decimals: &decimals, | ||
| DecimalsSource: "token_registry_or_default", | ||
| BalanceSource: "indexed_transfer_history", | ||
| LastUpdatedLedger: &holding.LastLedger, | ||
| LastUpdatedAt: &holding.LastSeen, | ||
| }) |
| // Compose classic account state when the generic materialized table is absent | ||
| // or incomplete for the address. | ||
| classicBalances, classicErr := h.getClassicAccountBalances(ctx, addr) | ||
| if classicErr == nil && classicBalances != nil { | ||
| decimals := 7 | ||
| for _, b := range classicBalances.Balances { | ||
| add(UnifiedAddressBalance{ | ||
| AssetType: b.AssetType, | ||
| AssetCode: b.AssetCode, | ||
| AssetIssuer: b.AssetIssuer, | ||
| BalanceRaw: strconv.FormatInt(b.BalanceStroops, 10), | ||
| BalanceDisplay: b.Balance, | ||
| Decimals: &decimals, | ||
| DecimalsSource: "stellar_asset", | ||
| BalanceSource: "classic_account_state", | ||
| }) | ||
| } | ||
| } else if classicErr != nil && !strings.Contains(classicErr.Error(), "not found") { | ||
| resp.Partial = true | ||
| resp.Warnings = append(resp.Warnings, "classic balances unavailable: "+classicErr.Error()) | ||
| } |
| Balance string `json:"balance"` | ||
| BalanceRaw int64 `json:"balance_raw"` | ||
| TxCount int64 `json:"tx_count"` | ||
| LastLedger int64 `json:"last_activity_ledger"` |
| if got := byType["contract"].Decimals; got == nil || *got != 2 { | ||
| t.Fatalf("soroban decimals = %v", got) | ||
| } | ||
| } |
This pull request introduces a new unified balances API endpoint that aggregates XLM, classic trustlines, and Soroban token holdings for a given Stellar address. It also adds the necessary backend logic, data model enhancements, and tests to support this endpoint. The implementation ensures that balances are normalized and provides additional metadata such as decimals, last activity, and data sources.
Unified Balances API and Backend Logic:
HandleUnifiedAddressBalancesand supporting types inhandlers_unified_balances.goto provide a single endpoint returning all balance types (XLM, trustlines, Soroban tokens) for a Stellar address. This includes logic to compose results from classic and Soroban sources, normalize fields, and report partial results with warnings as needed./api/v1/silver/addresses/{addr}/balancesin the router and updated logging to reflect its availability.Soroban Token Holdings Data Model and Query Enhancements:
TokenHoldingstruct to includeLastLedger(last activity ledger) and updated the DuckDB SQL queries and row scanning logic inunified_duckdb_reader.goto select, aggregate, and return this field, along withlast_seen(timestamp of last activity). [1] [2] [3] [4] [5] [6]Testing:
handlers_unified_balances_test.goto verify that the unified balances endpoint correctly composes classic and Soroban token balances, validates Stellar addresses, and returns expected results and metadata.