feat(graphql): Blend positions, pools, and earn discovery#661
Open
aditya1702 wants to merge 18 commits into
Open
feat(graphql): Blend positions, pools, and earn discovery#661aditya1702 wants to merge 18 commits into
aditya1702 wants to merge 18 commits into
Conversation
63bc4a8 to
16c5afb
Compare
d86bb20 to
a1197eb
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".
16c5afb to
27a7a2e
Compare
a1197eb to
5c7d419
Compare
27a7a2e to
da295e5
Compare
5c7d419 to
731499d
Compare
731499d to
10ccc0a
Compare
da295e5 to
19232c8
Compare
10ccc0a to
9d078d4
Compare
7940d1b to
7e68bdf
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.
7e68bdf to
ca9546f
Compare
9d078d4 to
23486c9
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, pools, earn discovery
Fifth and final stacked PR adding Blend Capital v2 lending support (stacked on #660).
rates.go, pure functions, big.Rat/big.Int): three-slope reactive curve, supply capture, APR→APY, rate projection, underlying conversion, emissions APR, claimable emissions, earned interest, backstop LP conversion, USD scaling. Every scalar/convention verified againstblend-contracts-v2@ ba22b487 andblend-sdk-js@ af62659, with live-mainnet vectors (simulatedget_reserve/get_config/get_reserve_emissions) and contract-test bit-exact fixtures.GetBackstopLPPrices(self-priced-row discrimination),GetLendingClaimTotals(per(poolId, source); backstop rows group under NULL pool),GetTokenMetadataByContractIDs.LENDINGcategory + 15 reasons in the schema;LendingChange implements BaseStateChangewithtokenId/amount/poolId(fromkey_value).Account.blendPositions— single-pass batch assembly, no dataloaders: token amounts at rates projected to now, USD values, APYs, per-reserve claimable emissions + interest earned/paid (cost-basis columns parsed as decimals) with the asset's oraclepriceUsdfor client-side USD conversion of token-denominated fields, backstop shares → LP tokens → USD with queued-for-withdrawal shares valued in the totals and perBlendQ4Wentry (queued shares stay interest-earning, slashable first-loss capital; only emissions accrue on active shares alone).Query.blendPools/blendPool— pool catalog with supplied/borrowed/backstop USD and supplied-USD-weightedinterestApy/netApy; missing prices propagatenull, never error.Query.blendEarnOptions— asset-first grouping; disabled reserves and supply-rejecting pools (status ≥ 4 — Frozen/Setup — or status not yet ingested) excluded; each pool option carriessupplyApy+emissionsSupplyAprso a wallet can show the emissions-inclusive earn rate; pools per asset ordered by supplied USD desc (nil last, address tie-break).blendPools/blendEarnOptions×50,Account.blendPositions×10; derived worst cases: blendPools ≈ 1450, blendPositions ≈ 370 — both ≪ the 6000 prod limit. No existing complexity entry touched; a regression test guardsAccountTransactionEdge.operations/stateChanges(freighter full-detail budget).Deviations register (reviewer sign-off):
ToAPY(apr, periods)+ two constants (plan assumed uniform weekly)eps/indexare uniformly 1e14; the per-reserve part is the CLAIM divisor (10^decimals · 1e7) —ClaimableEmissionstakes an explicit scalaremissionsEarnedBlnd= claimable (uncollected) BLND; lifetime claimed emissions surface asBlendPoolPosition.claimedBlnd(pool-source, BLND) andBlendAccountPositions.backstopClaimedLp(aggregate, Comet LP tokens — the backstop auto-swaps claims into its LP token, and backstop claim events carry no pool addresses)getDBColumnsgained apoolId → key_valuefield mapping — without it the JSONB column was never selected andLendingChange.poolIdalways resolved null (caught by the integration test)blendEarnOptions.tokenDecimalssources fromcontract_tokensmetadata (reserve-config decimals only as fallback) — reserve-order-dependent value caught as a flake in the full-suite runnet_*cost-basis columns may be scale-16 decimal strings (auction valuation ÷1e12) — resolvers parse viaParseDecimalToBigInt(truncating sub-token dust)Manual run procedure + smoke queries per the plan: migrate up → restart ingest →
protocol-setup --protocol-id BLEND→protocol-migrate history/current-state --protocol-id BLEND --start-ledger <v2 deploy ledger>.🤖 Generated with Claude Code
Post-review fixes (verified against
blend-capital/blend-contracts-v2 @ ba22b487,blend-sdk-js, and docs.blend.capital):backstopClaimedBlnd→backstopClaimedLp: backstop claims pay Comet LP tokens (7 decimals), not BLND — the backstop auto-swaps and re-deposits the claimd9c7779fclaimableStreampays the contract's historical-accrual branch: a holder with a balance but noUserEmissionDatarow getsbalance·index/scalar, not 04fa7b2efUtilizationclamped at 100% matchingreserve.rs— unclamped, a bad-debt reserve displays unbounded APYb4a31ee0pool.rs::load_price); shares theMaxPriceAgeconstant with #660's snapshot-side guard33c45f59blendEarnOptionsexcludes supply-rejecting pools: status ≥ 4 (Admin Frozen / Frozen / Setup all refuse deposits on-chain) or status not yet ingested. On-Ice pools (2–3) stay — the contract accepts deposits there24cf1d80lpTokens/usdValueinclude queued-for-withdrawal shares — per Blend's docs, queued shares keep earning pool interest and remain slashable first-loss capital until withdrawn; only emissions stop. EachBlendQ4Wentry gains its ownlpTokens/usdValue;sharesstays active-only6cb742f7findBackstopPricespicks deterministically (lowest oracle key) and error-logs if a second self-priced Comet group ever appears, instead of flapping with Go map-iteration order52b6b030backstopRate,cFactor,lFactor), thestatus0–6 encoding and its supply/borrow permissions, and the 24h price-staleness convention1f6ca7abNote for reviewers cross-checking docs.blend.capital: its "rate modifier has 9 decimals" claim is stale v1 wording — the deployed v2 contract uses
SCALAR_7(interest.rs), which is what the math here implements.