Skip to content

navi: resolve oracle price push from config API instead of hardcoded ids - #92

Merged
jangid merged 4 commits into
mainfrom
chore/navi-upgrade
Jul 29, 2026
Merged

navi: resolve oracle price push from config API instead of hardcoded ids#92
jangid merged 4 commits into
mainfrom
chore/navi-upgrade

Conversation

@Zorag44

@Zorag44 Zorag44 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

NAVI is upgrading lending_core + oracle for Pyth's Sui redeployment, which retires oracle_pro::update_single_price_v2 in favour of _v3 (arg 5 moves from the legacy PriceInfoObject to one from the new Pyth package, 0x55300367...). Rather than hardcode the new ids and hand-cut over at their upgrade window, delegate the price push to @naviprotocol/lending, which resolves the oracle package, entry function, Pyth/Wormhole state and per-feed price info objects from open-api.naviprotocol.io/api/navi/config at runtime.

updateSingleTokenPrice now takes only (tx, feedId) and looks the feed up by NAVI feedId. Matching on coinType would be unsafe: the config returns fully-padded types (0x0000..0002::sui::SUI), so a '0x2::sui::SUI' compare silently misses.

Bump @naviprotocol/lending 2.0.3 -> 2.0.7. 2.0.3 hardcodes update_single_price_v2 and never reads updateFunction/hermesEndpoint, so it would pair the new package id with the retired function name once NAVI flips env=prod. 2.0.7 reads both, so the cutover needs no further code change -- config is cached in-process for 5 min, so a running process picks it up within that window.

Call updatePythPriceFeeds explicitly rather than using updateOraclePricesPTB's updatePythPriceFeeds flag. That flag gates on a stale check reading content.fields.price_info.price_feed.price, but the object nests a fields wrapper at each level, so parsing always fails, the stale list is always empty and no VAA is ever posted. Posting unconditionally preserves the previous behaviour instead of silently depending on NAVI's keepers.

Drop the now-dead hardcoded ids, none of which are re-exported from src/index.ts and so are not public API:

  • NAVI_CONFIG.ORACLE_PRO_PACKAGE_ID (was a direct move-call target)
  • PYTH_STATE_ID / WORMHOLE_STATE_ID
  • pythPriceInfo on all 21 PRICE_FEED entries (feedId is still used)
  • the @pythnetwork/pyth-sui-js direct dependency; it stays in the tree transitively via @alphafi/alphalend-sdk's aggregator deps

Drop src/naviprotocol-lending.d.ts: it worked around NAVI shipping extensionless ESM re-exports that would not resolve under NodeNext. 2.0.7 ships a proper exports map with types, so tsc resolves the real OraclePriceFeed types and the hand-written stub is no longer needed.

Blockchain.pythSuiClient is kept -- it is still used by the rebalanceCap, slushAdmin and alphaVault helpers -- but its comments no longer claim it exists for SuiPythClient.

Verified against mainnet: Lending USDC deposit, Looping SUI-VSUI deposit and Navi USDC withdrawMax all dry-run successfully, targeting update_single_price_v2 at 0x203728f4... as resolved from env=prod, with the Pyth VAA posted through 0x04e20ddf... tsc --noEmit and eslint src/ are clean.

NAVI is upgrading lending_core + oracle for Pyth's Sui redeployment, which
retires oracle_pro::update_single_price_v2 in favour of _v3 (arg 5 moves from
the legacy PriceInfoObject to one from the new Pyth package, 0x55300367...).
Rather than hardcode the new ids and hand-cut over at their upgrade window,
delegate the price push to @naviprotocol/lending, which resolves the oracle
package, entry function, Pyth/Wormhole state and per-feed price info objects
from open-api.naviprotocol.io/api/navi/config at runtime.

updateSingleTokenPrice now takes only (tx, feedId) and looks the feed up by
NAVI feedId. Matching on coinType would be unsafe: the config returns
fully-padded types (0x0000..0002::sui::SUI), so a '0x2::sui::SUI' compare
silently misses.

Bump @naviprotocol/lending 2.0.3 -> 2.0.7. 2.0.3 hardcodes
update_single_price_v2 and never reads updateFunction/hermesEndpoint, so it
would pair the new package id with the retired function name once NAVI flips
env=prod. 2.0.7 reads both, so the cutover needs no further code change --
config is cached in-process for 5 min, so a running process picks it up
within that window.

Call updatePythPriceFeeds explicitly rather than using
updateOraclePricesPTB's updatePythPriceFeeds flag. That flag gates on a stale
check reading content.fields.price_info.price_feed.price, but the object nests
a `fields` wrapper at each level, so parsing always fails, the stale list is
always empty and no VAA is ever posted. Posting unconditionally preserves the
previous behaviour instead of silently depending on NAVI's keepers.

Drop the now-dead hardcoded ids, none of which are re-exported from
src/index.ts and so are not public API:
  - NAVI_CONFIG.ORACLE_PRO_PACKAGE_ID (was a direct move-call target)
  - PYTH_STATE_ID / WORMHOLE_STATE_ID
  - pythPriceInfo on all 21 PRICE_FEED entries (feedId is still used)
  - the @pythnetwork/pyth-sui-js direct dependency; it stays in the tree
    transitively via @alphafi/alphalend-sdk's aggregator deps

Drop src/naviprotocol-lending.d.ts: it worked around NAVI shipping
extensionless ESM re-exports that would not resolve under NodeNext. 2.0.7
ships a proper exports map with types, so tsc resolves the real
OraclePriceFeed types and the hand-written stub is no longer needed.

Blockchain.pythSuiClient is kept -- it is still used by the rebalanceCap,
slushAdmin and alphaVault helpers -- but its comments no longer claim it
exists for SuiPythClient.

Verified against mainnet: Lending USDC deposit, Looping SUI-VSUI deposit and
Navi USDC withdrawMax all dry-run successfully, targeting
update_single_price_v2 at 0x203728f4... as resolved from env=prod, with the
Pyth VAA posted through 0x04e20ddf... tsc --noEmit and eslint src/ are clean.
@Zorag44
Zorag44 requested a review from jangid as a code owner July 28, 2026 14:05
@Zorag44
Zorag44 requested review from 11felix and rg-alpha July 28, 2026 14:07
11felix
11felix previously approved these changes Jul 29, 2026
@jangid

jangid commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Updated for 15f0b5a. Both 🟡 findings from the 117c086 pass are fixed and independently verified — details below. CI green (lint/test/build), secrets-in-diff scan clean, no *.move/Move.toml. Approving; note the push dismissed the earlier approval, so standing is 1/2.

✅ 1. Pyth reads routed through our client — verified

src/strategies/lending.ts:903 and src/strategies/looping.ts:634 now build const naviOptions = { client: this.context.blockchain.suiGrpcClient } and pass it to both updatePythPriceFeeds and updateOraclePricesPTB.

Worth recording why this needed checking beyond "it compiles": NAVI's provider type is Partial<Pick<NaviSuiClient, 'getObject' | 'getDynamicFieldObject'>> & { core?: unknown } — every field optional, so any object type-checks and tsc offers no guarantee here. At runtime SuiPythClient.getCoreProvider() returns the core client only if provider.core has getObject, getDynamicObjectField and getDynamicField as functions, otherwise it falls through to provider.getObject and throws Sui Pyth provider does not support getObject. Checked against the installed @mysten/sui@2.22.1: SuiGrpcClient sets this.core = new GrpcCoreClient(...) (dist/grpc/client.mjs:42), and GrpcCoreClient extends CoreClient (dist/grpc/core.mjs:20), which defines all three (dist/client/core.mjs:24,34,73). So the core path is taken, not the throwing fallback. ✅

✅ 2. Feed coverage + optional-field guard — verified against live config

The guard is in at lending.ts:897-899 / looping.ts:628-630, and it matches NAVI's own !!r.pythPriceFeedId && !!r.pythPriceInfoObject filter.

On the coverage question I raised: I fetched open-api.naviprotocol.io/api/navi/config?env=prod and diffed all of NAVI_CONFIG.PRICE_FEED against oracle.feeds programmatically. All 20 remaining feedIds are present, byte-identical, each with a non-empty pythPriceFeedId and pythPriceInfoObject — SUI, wUSDC, USDC, USDT, WETH, CETUS, NAVX, WBTC, AUSD, ETH, USDY, NS, DEEP, BLUE, BUCK, suiUSDT, stSUI, wBTC, vSUI, HASUI. The single miss is DMC (0x8abfa63a…), which is absent from NAVI's config and which this push removes; no DMC reference remains anywhere in src/ or scripts/, so nothing dereferences the deleted entry. Removal confirmed correct. ✅

Note for later: this coverage now depends on a remote document, so a feed NAVI retires becomes a runtime throw for that pool rather than a build break. The new error messages make that diagnosable, which is the right trade — but it's worth knowing that adding a pool means checking oracle.feeds first.

🟢 3. Still open — duplicate VAA post per transaction (unchanged, optional)

Looping calls updateSingleTokenPrice once per asset, and the live config confirms the overlap I suspected: SUI, vSUI, stSUI and HASUI all map to Pyth feed 0x23d7315113… with the same price info object 0x89b2add829…. So a SUI-vSUI transaction fetches that VAA from Hermes twice, posts it twice, pays the update fee twice, and appends two storage::version_verification calls. Pre-existing behaviour, not a regression. Resolving both feeds up front and issuing one updatePythPriceFeeds(tx, uniqueFeedIds) + updateOraclePricesPTB(tx, feeds) pair would drop a round-trip and a fee from every looping tx.

🟢 4. Still open — unused import

getSuiClient is still imported at scripts/testRun.ts:20 and never used; it's also the function this PR deprecates. lint only globs src/**/*.ts so CI stays green either way.

🟢 5. Still open — PR description

The body still describes the 117c086 state: it says pythPriceInfo was dropped from "all 21 PRICE_FEED entries" (now 20, DMC removed), and doesn't mention overrideNaviLendingCoreLinkage / NAVI_STORAGE_ID, the ACCOUNT_ADDRESSES.WUSDCwUSDC fix, the 0x${ALPHA_COIN_TYPE} fix, the GraphQL rewrite of scripts/utils.ts, or the client plumbing. Worth a refresh before merge since it's the standing record of why the linkage override exists.

Also unchanged from the last pass: the mainnet dry-run evidence in the body predates 15f0b5a, which altered the client used for the Pyth object reads. Given the verification above I don't think a re-run gates merge, but a single re-dry-run would make the body's claim true of the code being merged.


Approving on the strength of findings 1 and 2 being verified fixed. Items 3–5 are advisory and don't gate merge. Approvals: 1/2 — the push dismissed the earlier approval, so this needs one more before merge.

updateSingleTokenPrice passed no options to updatePythPriceFeeds /
updateOraclePricesPTB, so NAVI's module-level default client served the
SuiPythClient object reads on every deposit and withdraw — the public
mainnet fullnode, the traffic #91 moved off public endpoints. Pass
context.blockchain.suiGrpcClient to both.

Guard pythPriceFeedId / pythPriceInfoObject, which NAVI treats as
optional: the latter reaches tx.object() as arg 5 of
oracle_pro::update_single_price_v3, so a supra/switchboard-only feed
would abort on tx.object('') instead of naming the feed.

Drop the DMC PRICE_FEED entry — the only one of our 21 absent from
NAVI's live oracle.feeds, with no Navi pool and no reference in src/
outside the map. The other 20 match byte-identically and all carry
Pyth data.

@jangid jangid 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.

operate.md: clean at 15f0b5a — CI green. Both prior blocking findings verified fixed: Pyth reads now go through our client (checked that SuiGrpcClient.core actually satisfies NAVI's getCoreProvider probe at runtime, since the provider type is fully Partial and gives no compile-time guarantee), and all 20 remaining feedIds verified byte-identical against the live env=prod oracle.feeds with populated Pyth fields — DMC correctly removed as the only absentee. Non-blocking notes in the summary comment: duplicate VAA post per looping tx, an unused import in scripts/testRun.ts, and a stale PR description. Approvals: 1/2 — the last push dismissed the earlier approval, so this needs one more before merge.

@jangid
jangid merged commit 62d087c into main Jul 29, 2026
3 checks passed
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.

3 participants