feat(blend): periodic SEP-40 oracle price snapshot with Comet BLND/LP leg - #660
feat(blend): periodic SEP-40 oracle price snapshot with Comet BLND/LP leg#660aditya1702 wants to merge 11 commits into
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d86bb208a6
ℹ️ 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".
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: a1197eb096
ℹ️ 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".
a1197eb to
5c7d419
Compare
5c7d419 to
731499d
Compare
10ccc0a to
9d078d4
Compare
9d078d4 to
23486c9
Compare
23486c9 to
6cdb564
Compare
6cdb564 to
97ab9f5
Compare
adec3b6 to
ef562f1
Compare
There was a problem hiding this comment.
Pull request overview
Adds periodic Blend v2 oracle and Comet LP price snapshots for USD valuation.
Changes:
- Implements SEP-40 and Comet price fetching and persistence.
- Adds scheduling, configuration, and advisory-lock gating.
- Adds Prometheus metrics and database-backed tests.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
internal/services/ingest.go |
Registers post-lock tasks. |
internal/services/ingest_live.go |
Starts tasks after lock acquisition. |
internal/services/blend/scval.go |
Adds SEP-40 XDR helpers. |
internal/services/blend/scval_test.go |
Tests price encoding and decoding. |
internal/services/blend/prices.go |
Implements snapshot scheduling and persistence. |
internal/services/blend/prices_test.go |
Tests snapshot behavior. |
internal/services/blend/comet.go |
Derives BLND and LP prices. |
internal/services/blend/comet_test.go |
Tests Comet fetching and valuation. |
internal/metrics/metrics.go |
Registers Blend metrics. |
internal/metrics/metrics_test.go |
Verifies metric initialization. |
internal/metrics/blend.go |
Defines snapshot metrics. |
internal/metrics/blend_test.go |
Tests metric collection. |
internal/ingest/ingest.go |
Wires the snapshot service. |
internal/data/blend/oracle_prices.go |
Adds price storage operations. |
internal/data/blend/oracle_prices_test.go |
Tests price SQL operations. |
internal/data/blend/models.go |
Registers the price model. |
docker-compose.yaml |
Documents environment settings. |
cmd/utils/global_options.go |
Defines Blend CLI options. |
cmd/ingest.go |
Exposes options through ingest. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
03c1c7b to
38ebf96
Compare
…e live advisory lock The blend price snapshot task started in setupDeps, so an instance that never wins the advisory lock (rolling deploy, standby pod) still ran snapshot passes, duplicating oracle RPC load and writes. Long-lived background tasks now register as PostLockTasks and launch inside startLiveIngestion after the lock is held.
The pool contract itself (pool.rs::load_price) rejects prices older than 24h or <= 0, so persisting them only lets the API price positions the pool would refuse to. Skip both at snapshot time, counted as new "stale"/"invalid" fetch outcomes, and add a wallet_blend_price_oldest_age_seconds gauge covering every decoded price including skipped-as-stale ones, so a dead oracle surfaces as unbounded gauge growth.
PricesTracked is now set after BatchUpsert - 0 on a failed upsert - so a DB outage no longer shows healthy row counts. Cover OldestPriceAge in the metrics tests (registration, record, lint) and align the FetchesTotal and snapshotOracle godocs with actual counting behavior (Comet success counts per derived row; invalid <=0 prices are excluded from the age gauge).
Copilot review fixes: - fetchCometState reads the pool's numeric state twice and requires both readings to agree: each getter is its own latest-ledger simulation, so a Comet transaction landing mid-read could otherwise mix pre- and post-trade values into a state that never existed on-chain. Any mutation changes at least one balance or the LP supply, so agreement is a complete witness. - cometValuation rejects a positive price that rounds to zero at 7 decimals, matching the snapshot invariant that non-positive prices are never persisted. - blend-price-interval rejects negative durations at startup instead of silently disabling the snapshot task (only 0 disables).
…r simulations
Everything the Comet valuation needs lives in two persistent ContractData
entries (DataKey::AllRecordData's Map<Address, Record{balance, weight,
scalar, index}> and DataKey::TotalShares), so fetchCometState now reads them
plus the contract instance in a single getLedgerEntries call — atomic by
construction, replacing eleven sequential per-getter simulations and the
double-read torn-state guard they required. The instance entry's WASM hash
is asserted against the pinned Comet hash before any storage is decoded
(the contract has no upgrade entrypoint, so a mismatch means the configured
address is not the pinned pool), and the per-token scalar fields must match
(the valuation's raw-balance ratio requires both legs to share decimals).
Key encodings (unit enum variants as single-element ScvVec[Symbol]), Record
field shapes, both legs' 7-decimal scalars, and the instance hash were all
verified against the live mainnet pool via getLedgerEntries.
PriceSnapshotConfig gains an RPC field, required when BackstopLPContractID
is set; the SEP-40 oracle leg still simulates decimals/lastprice (those are
computed values, not storage).
38ebf96 to
84df451
Compare
Blend oracle price snapshot task
Fourth of 5 stacked PRs adding Blend Capital v2 lending support (stacked on #659).
Adds a periodic price snapshot task under
ingest(live mode only) that keepsblend_oracle_pricesfresh for PR5's USD valuation. Each pass: discover targets → fetch prices → oneBatchUpsert.SEP-40 leg — for every
(oracle, asset)pair derived fromblend_pools × blend_reserves:decimals()once per oracle,lastprice(Asset::Stellar(asset))per target, via RPC simulation.Option::None, prices ≤ 0 (invalid), oracle timestamps older than 24h (stale) — the same pricespool.rs::load_priceitself rejects. Write-side half of the staleness guard; feat(graphql): Blend positions, pools, and earn discovery #661 adds the read side.errors.Join) — one bad oracle never aborts the pass.Comet leg (optional) — BLND spot + backstop LP price derived from the Comet BLND:USDC pool, since no oracle prices the LP token:
AllRecordData,TotalShares) in onegetLedgerEntriescall — atomic, no getter simulations.8abc2891…(contract has no upgrade entrypoint) and both legs'scalarfields match (equal decimals).blndPrice = (usdcBal/usdcWeight)/(blndBal/blndWeight)(Balancer spot, fee-less);lpPrice= pool NAV / LP supply; USDC ≡ $1. Prices rounding to"0"at 7 decimals are rejected.oracle_contract_id = <Comet address>; LP row self-priced (asset == oracle); BLND row keyed by the real BLND token address.Scheduling & metrics
success|error|none|stale|invalid); prices-tracked gauge (rows actually written — 0 on upsert failure); oldest-price-age gauge (includes skipped-as-stale prices, so a dead oracle that still serves old timestamps grows it unboundedly).Config
BLEND_PRICE_INTERVAL60s0disables; negative rejected at startupBLEND_BACKSTOP_LP_CONTRACT_IDVerification — all confirmed against live mainnet: SEP-40
Asset::Stellarencoding (spec + Reflector interface +lastpricesimulation round-trip), Comet entry keys/shapes/scalars/WASM hash (getLedgerEntriesdump), and the spot-price formula vs comet-contracts-v1 source (c_math.rs::calc_spot_price,c_consts.rs,init.rs).🤖 Generated with Claude Code