Skip to content

Feature: Ticket 4 v1 unified address balances endpoint - #79

Merged
tmosleyIII merged 1 commit into
mainfrom
feature/tmosley/unified-addresses-endpoint
Jun 22, 2026
Merged

Feature: Ticket 4 v1 unified address balances endpoint#79
tmosleyIII merged 1 commit into
mainfrom
feature/tmosley/unified-addresses-endpoint

Conversation

@tmosleyIII

Copy link
Copy Markdown
Contributor

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:

  • Added a new handler HandleUnifiedAddressBalances and supporting types in handlers_unified_balances.go to 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.
  • Registered the new endpoint /api/v1/silver/addresses/{addr}/balances in the router and updated logging to reflect its availability.

Soroban Token Holdings Data Model and Query Enhancements:

  • Extended the TokenHolding struct to include LastLedger (last activity ledger) and updated the DuckDB SQL queries and row scanning logic in unified_duckdb_reader.go to select, aggregate, and return this field, along with last_seen (timestamp of last activity). [1] [2] [3] [4] [5] [6]

Testing:

  • Added comprehensive tests in handlers_unified_balances_test.go to verify that the unified balances endpoint correctly composes classic and Soroban token balances, validates Stellar addresses, and returns expected results and metadata.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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, ""))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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}/balances route and handler to compose balances from materialized address balances, classic account state, and indexed token transfer history.
  • Extended TokenHolding and 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.

Comment on lines +156 to +170
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,
})
Comment on lines +124 to +144
// 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"`
Comment on lines +75 to +78
if got := byType["contract"].Decimals; got == nil || *got != 2 {
t.Fatalf("soroban decimals = %v", got)
}
}
@tmosleyIII
tmosleyIII merged commit 791b5f9 into main Jun 22, 2026
1 check passed
@tmosleyIII
tmosleyIII deleted the feature/tmosley/unified-addresses-endpoint branch June 22, 2026 11:21
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