feat(balances): add liquidity-pool share balances#650
Merged
Conversation
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
Contributor
There was a problem hiding this comment.
Pull request overview
Adds end-to-end support for liquidity-pool share balances (pool_share trustlines + LiquidityPoolEntry reserves) across ingestion, storage, GraphQL, and SDK types, enabling the balances API to expose pool shares and joined reserves.
Changes:
- Introduces new PostgreSQL tables/migrations for
liquidity_pools(reserves) andliquidity_pool_balances(per-account shares) plus data models + mocks. - Extends live ingestion + checkpoint population to extract and persist pool-share and pool-reserve ledger changes via new indexer processors and buffer plumbing.
- Adds GraphQL schema/types + resolver pagination integration, and updates SDK/client-side balance unmarshalling for
LiquidityPoolBalance.
Reviewed changes
Copilot reviewed 35 out of 37 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/wbclient/types/types.go | Adds SDK TokenTypeLiquidityPool and LiquidityPoolBalance/reserve types + unmarshal support. |
| pkg/wbclient/types/types_test.go | Adds SDK JSON unmarshal test coverage for liquidity-pool balances. |
| internal/services/token_ingestion.go | Persists liquidity pool reserve + share changes during live ingestion. |
| internal/services/token_ingestion_test.go | Updates ingestion service tests for new ProcessTokenChanges signature. |
| internal/services/mocks.go | Updates TokenIngestionService mock to include LP change args. |
| internal/services/ingest_test.go | Updates ingest-service tests to account for LP change args. |
| internal/services/ingest_live.go | Passes LP buffer deltas into token ingestion persistence. |
| internal/services/checkpoint.go | Routes pool_share trustlines + LiquidityPoolEntry rows into new tables during checkpoint population. |
| internal/services/checkpoint_test.go | Adds/updates checkpoint tests to verify LP routing and BatchCopy calls. |
| internal/serve/serve.go | Wires LiquidityPoolBalanceModel into GraphQL BalanceReader construction. |
| internal/serve/graphql/schema/enums.graphqls | Extends GraphQL TokenType enum with LIQUIDITY_POOL. |
| internal/serve/graphql/schema/balances.graphqls | Adds LiquidityPoolBalance + LiquidityPoolReserve GraphQL types. |
| internal/serve/graphql/resolvers/resolver.go | Extends BalanceReader interface to support LP balance reads. |
| internal/serve/graphql/resolvers/balance_reader.go | Implements GetLiquidityPoolBalances adapter over the LP balance model. |
| internal/serve/graphql/resolvers/account_liquidity_pool_balances_test.go | Adds tests for DB→GraphQL LP balance mapping and cross-source pagination. |
| internal/serve/graphql/resolvers/account_balances.go | Adds LP as a new balance source + cursor validation + node retrieval. |
| internal/serve/graphql/resolvers/account_balances_utils.go | Adds buildLiquidityPoolBalanceFromDB helper. |
| internal/serve/graphql/resolvers/account_balances_test.go | Updates balance-source tests and resolver wiring for LP model. |
| internal/serve/graphql/resolvers/account_allowances_test.go | Updates BalanceReader construction in allowance tests for new LP arg. |
| internal/serve/graphql/generated/models_gen.go | Regenerates gqlgen models for LiquidityPoolBalance/Reserve + enum value. |
| internal/serve/graphql/generated/generated.go | Regenerates gqlgen execution code/complexity for new GraphQL types. |
| internal/loadtest/runner.go | Wires LP models into the loadtest ingestion service config. |
| internal/ingest/ingest.go | Wires LP models into ingestion + checkpoint service configs. |
| internal/indexer/types/types.go | Adds indexer change types for LP shares and pool reserves. |
| internal/indexer/processors/liquidity_pools.go | Adds processors to extract pool_share trustline shares + LiquidityPoolEntry reserves. |
| internal/indexer/processors/liquidity_pools_test.go | Adds unit tests for LP share/reserve extraction. |
| internal/indexer/mocks.go | Adds mock processors for LP shares and pools. |
| internal/indexer/indexer.go | Runs LP processors per operation and pushes changes into the buffer. |
| internal/indexer/indexer_test.go | Updates indexer tests for new LP processors. |
| internal/indexer/indexer_buffer.go | Adds buffer maps + tombstones + merge/clear support for LP changes. |
| internal/db/migrations/2026-07-02.0-liquidity_pools.sql | Creates liquidity_pools table with ingestion-tuned storage parameters. |
| internal/db/migrations/2026-07-02.1-liquidity_pool_balances.sql | Creates liquidity_pool_balances table with ingestion-tuned storage parameters. |
| internal/data/models.go | Registers LiquidityPool/LiquidityPoolBalance models in the shared Models container. |
| internal/data/mocks.go | Adds mocks for LiquidityPoolModel and LiquidityPoolBalanceModel. |
| internal/data/liquidity_pools.go | Adds data model for batch upsert/copy of liquidity pool reserves. |
| internal/data/liquidity_pool_balances.go | Adds data model for keyset-paginated LP balance reads (JOIN reserves) + batch upsert/copy. |
| internal/data/liquidity_pool_balances_test.go | Adds model-level tests for LP reserves/shares upsert/copy and pagination/join behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The balances query fragment omitted the LiquidityPoolBalance case, so GetAccountBalances/GetAllAccountBalances never requested liquidityPoolId, reserves, or lastModifiedLedger for pool-share balances — clients decoded a LiquidityPoolBalance with those fields empty/zero. Add the missing inline fragment so pool-share balances are returned complete.
…er live ingestion Add pool-share balance coverage to the account-balances integration suite, covering both hydration paths: - A dedicated account (CheckpointLPAccountKP) issues its own credit asset, establishes a constant-product pool, and deposits equal legs BEFORE the checkpoint snapshot (in setupTestAccounts, before the ingest container starts). Phase 1 asserts its pool-share balance, proving checkpoint BatchCopy hydration of liquidity_pool_balances JOINed with liquidity_pools. - A second dedicated account (LiveLPAccountKP) deposits without withdrawing via a new submitted use case (prepareLiveLiquidityPoolDepositOps) AFTER the snapshot. Phase 3 asserts its pool-share balance, proving the live delta-ingestion upsert path. Both assert shares, pool id, and both reserve legs via a shared assertLiquidityPoolBalance helper. The existing liquidityPoolOps fixture is untouched (it fully withdraws, leaving no balance to assert).
minimum_balance previously bundled selling liabilities into the reserve (reserve + sellingLiabilities), which is not what "minimum balance" means in Stellar. Redefine it as the pure base reserve requirement, matching stellar-core's getMinBalance: (2 + numSubEntries + numSponsoring - numSponsored) * baseReserve Selling liabilities are still exposed separately, so spendable balance is now derived downstream as balance - minimum_balance - selling_liabilities (mirrors how trustlines already work). This makes native consistent with trustlines and makes minimum_balance match core/Horizon for cross-checks. Pre-release: no migration needed (column unchanged, value re-ingested from scratch); no consumer pinned to the old semantics. The freighter-backend-v2 mapper subtracts selling liabilities to keep `available` unchanged.
The wbclient balances query fragment omitted numSubentries, so the SDK never fetched it and NativeBalance.NumSubentries always unmarshalled to 0 for every SDK consumer (e.g. freighter-backend-v2's subentry_count). The GraphQL schema and resolver already exposed the field; only the query fragment was missing it. Add a reflection-based regression test asserting every concrete balance variant's inline fragment requests each of that struct's JSON fields, so a future field added to a balance type without a matching fragment entry fails a unit test rather than silently zeroing out at the SDK boundary.
The base-reserve minimum-balance calc was duplicated in accounts.go and checkpoint.go. Route both through processors.MinimumBalance(xdr.AccountEntry) so the computation lives in one place.
The prior payload used a 65-char (odd) hex string, which is not a valid 32-byte pool id. Use a 64-hex value for realistic test data.
aristidesstaffieri
approved these changes
Jul 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Exposes an account's liquidity-pool share balances through a new
LiquidityPoolBalanceGraphQL/SDK type. Pool shares were previously dropped at ingestion (thepool_sharetrustline was skipped andLiquidityPoolEntryledger entries were not routed anywhere). This is part of the Freighter v1→v2 balances-API migration — closing theliquidity_pool_id+reservesdata gap the v1 client reads.The new balance carries the account's shares (as
balance), the pool id (astokenId/liquidityPoolId), and the pool'sreserves(constituent assets + amounts), joined at query time from two ingested sources:pool_sharetrustline (TrustLineEntry.Asset.LiquidityPoolId= pool id,.Balance= shares) →liquidity_pool_balances.LiquidityPoolEntryledger entry (constant-product body:Params.AssetA/AssetB,ReserveA/ReserveB) →liquidity_pools.Changes
Storage (new migrations)
liquidity_pools(pool_id TEXT PK, asset_a, amount_a, asset_b, amount_b, last_modified_ledger)— assets stored canonically (native/CODE:ISSUER). Nototal_shares(unused).liquidity_pool_balances(account_id BYTEA, pool_id TEXT, shares, last_modified_ledger, PK(account_id, pool_id)).trustline_balances' fillfactor/autovacuum tuning for upsert-heavy ingestion.Data models —
data/liquidity_pools.go+data/liquidity_pool_balances.go(GetByAccountkeyset-paginated bypool_id, JOINingliquidity_poolsfor reserves;BatchUpsert;BatchCopy; interfaces + mocks).Ingestion
LiquidityPoolSharesProcessor(pool-share trustline → shares) andLiquidityPoolsProcessor(LiquidityPoolEntry→ reserves), wired into the indexer alongside the trustline/account/SAC processors, with buffer dedup (tombstone-aware, mirrors the existing change types).token_ingestion.go) and checkpoint population (checkpoint.go) both handle the two new sources.GraphQL / resolver
TokenType.LIQUIDITY_POOL;LiquidityPoolReserve+LiquidityPoolBalance implements Balance.lpbalance source appended for G-addresses;getLiquidityPoolBalanceNodespages by pool id and participates in the multi-source cursor scheme.SDK —
wbclient/types:LiquidityPoolBalance(+ nested reserve type),UnmarshalBalancecase,TokenTypeLiquidityPool.Design decisions
TrustlinesProcessor: the genericLedgerChangeProcessor[T]contract emits a single type per processor, so each new output type (LiquidityPoolShareChange,LiquidityPoolChange) gets its own processor — matching how accounts/trustlines/SAC each own one output type.TrustlinesProcessorstill skips pool-shares (they are not asset trustlines).PoolIDToString(hex of the 32-byte hash), consistent with Horizon; reserve assets viaxdr.Asset.StringCanonical().Tests
GetByAccountJOIN + pagination,BatchUpsert,BatchCopy.buildLiquidityPoolBalanceFromDB;getLiquidityPoolBalanceNodespagination across the SEP-41 → LP source boundary (cursor round-trips) and within the LP keyset.__typenameunmarshal.make checkclean; fullmake unit-test(race) green.Part of the Freighter v2 balances migration; independent of the subentry/SAC PR. Do not merge without review.
Closes #652
Also in this branch:
minimum_balanceis now the pure base reserveRedefines
minimum_balanceto be the pure base reserve requirement — matching stellar-core'sgetMinBalanceand Horizon — instead of bundling selling liabilities:Selling liabilities remain exposed separately, so spendable balance is derived downstream as
balance - minimum_balance - selling_liabilities(mirrors how trustlines already work). This makesnative consistent with trustlines and makes
minimum_balancematch core/Horizon for cross-checks.Pre-release, so no migration (column unchanged; value re-ingested from scratch) and no consumer is
pinned to the old semantics.
freighter-backend-v2's balances mapper subtracts selling liabilities,so the response's
availablevalue is unchanged.