feat(graphql): Blend positions, pools, and earn discovery - #661
Open
aditya1702 wants to merge 32 commits into
Open
feat(graphql): Blend positions, pools, and earn discovery#661aditya1702 wants to merge 32 commits into
aditya1702 wants to merge 32 commits into
Conversation
aditya1702
force-pushed
the
blend/pr5-graphql
branch
from
July 9, 2026 00:30
63bc4a8 to
16c5afb
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 16c5afb605
ℹ️ 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".
aditya1702
force-pushed
the
blend/pr5-graphql
branch
from
July 9, 2026 13:57
16c5afb to
27a7a2e
Compare
aditya1702
force-pushed
the
blend/pr5-graphql
branch
from
July 9, 2026 20:12
27a7a2e to
da295e5
Compare
aditya1702
force-pushed
the
blend/pr5-graphql
branch
from
July 9, 2026 20:34
da295e5 to
19232c8
Compare
aditya1702
force-pushed
the
blend/pr5-graphql
branch
2 times, most recently
from
July 10, 2026 11:32
7e68bdf to
ca9546f
Compare
aditya1702
force-pushed
the
blend/pr5-graphql
branch
2 times, most recently
from
July 16, 2026 16:56
0caadc2 to
b67919d
Compare
aditya1702
force-pushed
the
blend/pr5-graphql
branch
from
July 16, 2026 20:07
b67919d to
ddde2c9
Compare
JiahuiWho
reviewed
Jul 20, 2026
JiahuiWho
reviewed
Jul 20, 2026
JiahuiWho
reviewed
Jul 20, 2026
Contributor
|
With the null-propagation and weighting rules, can we add totalUsdValue + netApy to BlendAccountPositions, computed in the resolver from the per-pool values? |
aditya1702
force-pushed
the
blend/pr5-graphql
branch
from
July 21, 2026 18:45
ddde2c9 to
66a77c9
Compare
…data Classification inserts contract_tokens rows with decimals 0 before RPC enrichment; blendEarnOptions reported 0 for such assets. Metadata decimals now apply only once the row is genuinely enriched (name set or nonzero decimals); the reserve config's on-chain copy is the fallback.
reserve.rs's utilization is explicitly capped ("This is capped at 100% to
ensure interest calculations are fair") with branch order: zero liabilities
first, then liabilities >= supply clamps to exactly 1. Unclamped, a bad-debt
reserve (liabilities exceeding supply — a reachable state the contract has a
dedicated test for) pushed BorrowAPR's >95% slope past its [0,1] domain and
displayed an unbounded borrow/supply rate. Zero supply with outstanding debt
now also reads 100% instead of 0.
…stream
distributor.rs's update_user_emissions has a third branch: a user holding
tokens with NO UserEmissionData entry is owed balance*index/scalar the first
time their emissions are touched ("user had tokens before emissions began").
claimableStream returned 0 whenever the account had no blend_emissions row,
understating claimable BLND for every holder of a reserve (or backstop pool)
whose emission stream was configured after they supplied. With no user row
but a configured stream and a positive balance, the full config index now
accrues from a zero starting point — mirroring the contract exactly.
The backstop claim event's amount is execute_claim's return — the Comet LP tokens minted and auto-deposited — never raw BLND (the decoder now records those rows with a NULL token_id and units backstop_lp). The account-wide lifetime total sourced from those rows is therefore LP-denominated: backstopClaimedBlnd is renamed to backstopClaimedLp and documented as Comet LP tokens at 7 decimals. Pre-release schema, no deprecation shim.
The pool contract refuses prices older than 24h (pool.rs::load_price), so a staler blend_oracle_prices row cannot honestly value anything — but the resolvers applied stored prices unconditionally, pricing every USD/APY field off a dead oracle's last snapshot forever. Filter rows past blendrates.MaxPriceAge (exported from the snapshot service so both sides share one constant) when building the price lookups, making a stale price null the same fields a missing one does.
The pool contract refuses supply for status >= 4 (pool.rs::require_action_allowed): 4 Admin Frozen, 5 Frozen, 6 Setup. blendEarnOptions filtered only on reserve.Enabled, so a frozen or still-in-setup pool's enabled reserves were advertised as earn destinations the chain would reject. Gate reserves on the pool's status (< 4, per docs.blend.capital's status lifecycle); a NULL status — config entry not ingested yet — can't be confirmed eligible and is excluded too. On-Ice pools (2-3) stay visible: they accept deposits.
Queued-for-withdrawal shares keep earning pool interest and remain slashable first-loss capital until withdrawn (docs.blend.capital backstop deposit-management) — only emissions stop accruing on them. But BlendBackstopPosition.lpTokens/usdValue were computed from the active share balance alone, so a user's backstop value dropped by the queued amount the moment they queued, understating their at-risk capital for the whole 17-day queue. Value active+queued in the totals and give each BlendQ4W entry its own lpTokens/usdValue through the same shares→LP→USD chain; shares stays active-only and emissions still accrue on active shares only, both now documented in the schema.
…uity findBackstopPrices returned the first complete (self-priced LP, BLND sibling) group Go's map iteration happened to visit, so a second self-priced Comet group in blend_oracle_prices — a misconfigured Comet/oracle address, since Blend v2 has exactly one backstop token at a time — would make backstop valuations flap between requests. Pick the lowest oracle key instead and log the ambiguity as an error rather than failing the whole query over a config mistake.
…eness blend.graphqls handed clients three 7-decimal fixed-point fields (backstopRate, cFactor, lFactor) and the raw 0-6 status int with no scale or encoding documented — docs.blend.capital's vocabulary now backs each description. Also states the 24h price-staleness convention at the top and replaces the earn-option description's stale task references with the actual supply-eligibility contract.
…ion price The wallet's earn list shows the emissions-inclusive yield per token (supply APY + BLND emissions — the number Blend's UI marks with an asterisk), but BlendEarnPoolOption only carried the interest-only supplyApy. Add emissionsSupplyApr, computed by the same emissionsAPRFor path the pool catalog uses, with the earn resolver now batch-loading reserve emissions and the Comet BLND price. The per-token earnings view also needs interestEarned (token units) in USD; BlendReservePosition carried emissionsEarnedUsd but no way to value the interest side. Expose priceUsd — the reserve asset's oracle price — so any token-denominated field converts client-side.
Read claimedBlnd/backstopClaimedLp from the PoolClaimed/BackstopClaimed current-state accumulators (GetByAccount) instead of aggregating LENDING/CLAIM history rows. Remove StateChangeModel.GetLendingClaimTotals and LendingClaimTotal. The CLAIM history rows are still written and feed the LendingChange activity feed.
…variant The blend account-positions, pool-catalog, and earn-options resolvers each issued their mutually-independent reads serially. Group them into errgroup tiers keyed by their shared dependency (account -> pool -> oracle/asset), so each tier's reads run concurrently while the barriers between tiers preserve data dependencies. Correctness is unchanged; this cuts read round-trip latency (the account path drops from ~12 serial round-trips to 3 concurrent waves). Also pin the Comet two-row invariant in findBackstopPrices: the snapshot writer stores exactly the self-priced LP row and its BLND sibling per oracle, so document that and log loudly if a group ever has more than one sibling (which would make BLND-row selection ambiguous).
…laim math Claimable BLND was computed against the stored emission index, which is only as fresh as the stream's last on-chain touch — idle streams under-reported until someone else interacted. ProjectEmissionIndex replicates the contract distributors' update_emission_data step (blend-contracts-v2 @ ba22b487, pool and backstop distributor.rs; mirrored by blend-sdk-js Emissions.accrue): index + floor(min(now,expiration)- last_time)*eps*scalar/supply with the contract's exact guards. Pool streams project over the side's raw b/dToken supply at 10^decimals; the backstop projects over unqueued shares (shares - q4w) at 1e7. Contract-test vectors (test_update_emission_data_past_exp/_rounds_down) pinned bit-exact in rates_test.go.
emissionsAPRFor call sites passed the stored BRate/DRate while every sibling token/USD figure on the same types is valued at the projected PB/PD, so emissions APRs were overstated after idle accrual. All surviving call sites (position reserve, pool catalog) now pass PB/PD.
…d-sdk-js BlendPoolPosition.netApy divided the weighted APY spread by the net position value (supplied - borrowed), which diverges from blend-sdk-js's PositionsEstimate — the convention the Blend UI displays — and explodes as a leveraged position's net value approaches 0. Now (Σ suppliedUsd·supplyApy − Σ borrowedUsd·borrowApy) / Σ suppliedUsd, with the SDK's supply-less rule: exactly 0 (debt is forgiven as bad debt).
…eams Blend runs two independent emission streams per reserve; the single emissionsApr picked the bToken side whenever supply/collateral existed, silently dropping the borrow-stream APR for both-sides positions. BlendReservePosition now mirrors BlendReserve with emissionsSupplyApr + emissionsBorrowApr, each the stream's pool-wide APR at projected rates.
Earn discovery is a downstream composition concern: blendPools already carries every field the asset-first view derived (status gating, reserve enabled, supplyApy, emissionsSupplyApr, suppliedUsd ordering, token metadata), so consumers assemble it themselves. Drops the query, its BlendEarnOption/BlendEarnPoolOption types, resolver, complexity entry, and the now-unused ReserveModel.GetAll reader.
…te accrual Adversarial audit vs blend-contracts-v2 @ ba22b487 found two edge-state deviations in ProjectRates: - util==0: the contract bumps last_time and skips the rate update when nobody borrows; the projection grew dRate by irMod*rBase. Masked today (every pD consumer multiplies a zero dToken amount) but a landmine. - bad debt (liabilities >= supply): the contract grows bRate by accrued*(1-bstop)/totalSupply — the UNCLAMPED liabilities/supply ratio — while the reduction reused the clamped utilization, so suppliers' bRate growth was understated (~0.5% over 30 idle days). ProjectRates now takes bSupply/dSupply, derives the raw ratio itself, and mirrors the contract's on-load guards (zero bSupply / zero liabilities). New vectors: util==0, zero bSupply, bad-debt raw ratio.
…ry/Account BlendPool.status becomes a BlendPoolStatus enum (ADMIN_ACTIVE..SETUP, matching the BlendAuctionType precedent); null until ingested and for any unrecognized on-chain value. The blend entry points move off 'extend type' blocks into the base Query (queries.graphqls) and Account (account.graphqls) definitions so each type reads complete in one place; blend.graphqls keeps all Blend object/enum types.
Nested blend lists were priced once under a single outer multiplier, so a full blendPools selection cost ~29 units per pool instead of pools x reserves. Every list level now carries its own bound: catalog x50, per-account pools/backstop/activeAuctions x10 (documented assumption), reserves and auction bid/lot x30 (MAX_RESERVES), q4w x20 (MAX_Q4W_SIZE); Account.blendPositions drops back to default pricing so cardinality is not double-charged. Full-selection worst cases are now blendPools=26150 and blendPositions=7572 - above the current 6000 deployment limit by design; admitting full selections needs a deployment-side limit raise. Existing non-blend entries (freighter budget) untouched.
…ct guard The distributor advances the stream index before any user write and traps on a negative delta via require_nonnegative; off-chain a negative delta can only mean an ingestion gap between the stream row and the user row. ClaimableEmissions now returns the accrued balance alone in that state instead of a negative claimable.
GetBackstopLPPrices inferred the protocol-wide BLND/LP price group from a self-priced-row join, but a self-priced row does not uniquely identify the configured Comet: pools are permissionless, so any pool whose oracle is also one of its reserve assets produces the same shape, and a stale formerly-configured Comet's rows linger until they age out. BLEND_BACKSTOP_LP_CONTRACT_ID - the same pin the snapshot writer targets - now flows into serve (cmd/serve.go -> Configs -> resolver) and scopes the query to exactly that oracle; findBackstopPrices reduces to picking the self-priced LP row and its BLND sibling within the pinned group. An unset pin returns no rows, so backstop USD fields resolve to null. Deploy note: the api deployment needs BLEND_BACKSTOP_LP_CONTRACT_ID in its env (ingest already has it).
aditya1702
force-pushed
the
blend/pr5-graphql
branch
from
July 30, 2026 21:39
eacc9ad to
db5c332
Compare
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.
Blend GraphQL: positions and pools
Final stacked Blend v2 PR (on #660). Adds the GraphQL read surface for Blend lending: account positions, a pool catalog, and the Blend state-change types.
What this adds
Account.blendPositions— the account's lending/collateral/borrowing per pool and reserve, backstop deposits with queued withdrawals, claimable BLND emissions, lifetime claimed totals, interest earned/paid to date, and any open Dutch auctions on the account.Query.blendPools/blendPool(address)— pool catalog: per-pool status, admin, reward-zone flag, supplied/borrowed/backstop USD, weighted APYs, and every reserve's rates, sizes, and prices.BlendSupplyChange,BlendCollateralChange,BlendDebtChange(BORROW/REPAY/FLASH_LOAN/BAD_DEBT/BURN, with per-reason amount denomination documented on the type),BlendAuctionChange(FILL, withauctionType,fillPercent,counterparty, and first-classlot/bidamount lists),BlendEmissionsClaimChange,BlendBackstopEmissionsClaimChange,BlendBackstopChange, andBlendBackstopQueueChange. Every non-null field is always meaningful for its type; unmatched (category, reason) rows surface as errors, not null nodes.internal/services/blend/rates.goas pure functions, verified againstblend-contracts-v2 @ ba22b487andblend-sdk-js @ af62659(details below).Conventions
Float, null when the value is uncomputable (missing or stale price)Stringraw integers at the token's native decimalsBlendPool.statusBlendPoolStatusenum; null until ingestedbackstopRate,cFactor,lFactorare 7-decimal fixed point(Σ supplied·supplyApy − Σ borrowed·borrowApy) / Σ supplied, 0 when nothing supplied — the SDK/UI conventionMath audit
Every formula was diffed against the contract source (authority) and the JS SDK (cross-check), adversarially — including edge states. Where the SDK and contract disagree, the contract wins.
Reserve::load, including the zero-utilization short-circuit and the unclamped bad-debt bRate accruallast_time → nowexactly like the distributors (reserve: over token supply; backstop: over unqueued shares); contract test vectors pinned bit-exact10^reserve_decimals · 1e7per the contract — the SDK's uniform1e14is only right for 7-decimal tokensusdc×5/totalSharesfor the 80/20 poolNotes for reviewers
BLEND_BACKSTOP_LP_CONTRACT_IDoracle group — a permissionless pool can't spoof the protocol-wide prices with a self-priced row.claimedBlnd/backstopClaimedLpread current-state accumulator tables, not history sums, so retention can't undercount lifetime totals.blendPools= 26,150,blendPositions= 7,572. Admitting full selections needs a deployment-sideGRAPHQL_COMPLEXITY_LIMITraise (recommendation: 30,000). Existing non-Blend entries are untouched, so the freighter full-detail budget is unchanged.SCALAR_7), which is what this implements.Deploy
BLEND_BACKSTOP_LP_CONTRACT_IDin its env (ingest already has it).GRAPHQL_COMPLEXITY_LIMITraise as above.protocol-setup --protocol-id BLEND→protocol-migrate history/current-state --protocol-id BLEND --start-ledger <v2 deploy ledger>.