Skip to content

feat(balances): add liquidity-pool share balances#650

Merged
aditya1702 merged 16 commits into
mainfrom
feat/balances-lp-shares
Jul 6, 2026
Merged

feat(balances): add liquidity-pool share balances#650
aditya1702 merged 16 commits into
mainfrom
feat/balances-lp-shares

Conversation

@aditya1702

@aditya1702 aditya1702 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Exposes an account's liquidity-pool share balances through a new LiquidityPoolBalance GraphQL/SDK type. Pool shares were previously dropped at ingestion (the pool_share trustline was skipped and LiquidityPoolEntry ledger entries were not routed anywhere). This is part of the Freighter v1→v2 balances-API migration — closing the liquidity_pool_id + reserves data gap the v1 client reads.

The new balance carries the account's shares (as balance), the pool id (as tokenId / liquidityPoolId), and the pool's reserves (constituent assets + amounts), joined at query time from two ingested sources:

  • Per-account shares — the pool_share trustline (TrustLineEntry.Asset.LiquidityPoolId = pool id, .Balance = shares) → liquidity_pool_balances.
  • Pool reserves — the LiquidityPoolEntry ledger 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). No total_shares (unused).
  • liquidity_pool_balances(account_id BYTEA, pool_id TEXT, shares, last_modified_ledger, PK(account_id, pool_id)).
  • Both mirror trustline_balances' fillfactor/autovacuum tuning for upsert-heavy ingestion.

Data modelsdata/liquidity_pools.go + data/liquidity_pool_balances.go (GetByAccount keyset-paginated by pool_id, JOINing liquidity_pools for reserves; BatchUpsert; BatchCopy; interfaces + mocks).

Ingestion

  • New LiquidityPoolSharesProcessor (pool-share trustline → shares) and LiquidityPoolsProcessor (LiquidityPoolEntry → reserves), wired into the indexer alongside the trustline/account/SAC processors, with buffer dedup (tombstone-aware, mirrors the existing change types).
  • Live persist (token_ingestion.go) and checkpoint population (checkpoint.go) both handle the two new sources.

GraphQL / resolver

  • TokenType.LIQUIDITY_POOL; LiquidityPoolReserve + LiquidityPoolBalance implements Balance.
  • New lp balance source appended for G-addresses; getLiquidityPoolBalanceNodes pages by pool id and participates in the multi-source cursor scheme.

SDKwbclient/types: LiquidityPoolBalance (+ nested reserve type), UnmarshalBalance case, TokenTypeLiquidityPool.

Design decisions

  • Two dedicated processors rather than folding pool-share extraction into TrustlinesProcessor: the generic LedgerChangeProcessor[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. TrustlinesProcessor still skips pool-shares (they are not asset trustlines).
  • Pool id rendered via the existing PoolIDToString (hex of the 32-byte hash), consistent with Horizon; reserve assets via xdr.Asset.StringCanonical().
  • LP cursor keys by the pool-id hex string (validated as a 32-byte hex hash) rather than a UUID like the other contract/asset-backed sources.

Tests

  • Processor extraction (ADD/UPDATE/REMOVE) for both pool-shares and pool reserves.
  • Data models: GetByAccount JOIN + pagination, BatchUpsert, BatchCopy.
  • buildLiquidityPoolBalanceFromDB; getLiquidityPoolBalanceNodes pagination across the SEP-41 → LP source boundary (cursor round-trips) and within the LP keyset.
  • Checkpoint routing of pool-shares and LP entries; SDK __typename unmarshal.
  • make check clean; full make 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_balance is now the pure base reserve

Redefines minimum_balance to be the pure base reserve requirement — matching stellar-core's
getMinBalance and Horizon — instead of bundling selling liabilities:

minimum_balance = (2 + numSubEntries + numSponsoring - numSponsored) * baseReserve   # excludes liabilities

Selling liabilities remain exposed separately, so spendable balance is 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, 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 available value is unchanged.

Copilot AI review requested due to automatic review settings July 2, 2026 16:38
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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) and liquidity_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.

Comment thread pkg/wbclient/types/types_test.go Outdated
Comment thread pkg/wbclient/types/types_test.go Outdated
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.
Comment thread internal/services/checkpoint.go Outdated
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.
@aditya1702 aditya1702 merged commit b59b28d into main Jul 6, 2026
9 checks passed
@aditya1702 aditya1702 deleted the feat/balances-lp-shares branch July 6, 2026 18:44
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.

Add liquidity-pool share balances

3 participants