import { HermesClient, type HermesClientConfig } from "@pythnetwork/hermes-client"; import type { PartialPriceFeedSource, PriceFeedSource } from "../utils/simulate"; /** * Re-exported so a caller can configure the connection this module builds * without taking its own dependency on `@pythnetwork/hermes-client` — the point * of `PriceFeedSource` is that consumers need not know which client is behind * it. */ export type { HermesClientConfig }; export declare const PRIMARY_PYTH_ENDPOINT = "https://hermes.pyth.network"; /** * Tests if the primary Pyth connection endpoint is working by checking the /live endpoint * @returns true if the connection is working, false otherwise */ export declare const testPrimaryPythConnection: () => Promise; /** * Gets a working Pyth connection endpoint, trying primary first, then fallback if provided * @param fallbackPythEndpoint Optional fallback endpoint * @returns The endpoint URL that is working, or the primary endpoint if fallback fails */ export declare const getWorkingPythEndpoint: (fallbackPythEndpoint?: string) => Promise; /** * The only part of `HermesClient` this module reads prices through. * * Structural for the same reason `PriceFeedSource` is: a real `HermesClient` * satisfies it, and so does a stub — which is what makes the wire-to-`FeedPrice` * mapping below testable without standing up an endpoint. */ export type HermesPriceReader = Pick; /** * A `PriceFeedSource` (and `PartialPriceFeedSource`) backed by Hermes. * * `getLatestPriceFeedsPartial` is served by Hermes' own * `ignoreInvalidPriceIds`, which returns the feeds it can and omits the rest — * so gaps are reported by the endpoint rather than inferred from a thrown * batch. `missing` is the requested-minus-returned difference, which is the * contract `refreshReservePriceTolerant` relies on to populate * `unpricedCoinTypes`. * * `parsed: true` is required on both calls: without it Hermes returns only the * binary blob and there is no price to read. * * `encoding: "base64"` is the cheap half of a payload regression. v2 read prices * from `/api/latest_price_feeds`, which omitted the update blob entirely; this * endpoint always returns `binary` and offers no way to decline it, so every * price read now downloads a VAA nothing here looks at. base64 is ~1/3 smaller * than the default hex for the same bytes, on a request that runs per lending * market on every refresh. * * An EMPTY id list short-circuits and never reaches the network, matching what * v2's `getLatestPriceFeeds` did (`if (priceIds.length === 0) return []`). This * is not a micro-optimisation: `/v2/updates/price/latest` rejects a request with * no `ids[]` outright — `400 Failed to deserialize query string. Error: missing * field 'ids'` — so without the guard an empty batch is a hard failure instead of * an empty answer. `initializeSuilend` reaches it routinely, because it splits a * market's reserves into temporary-feed and non-temporary buckets and either * bucket can come out empty. */ export declare const createHermesPriceFeedSource: (hermesClient: HermesPriceReader) => PriceFeedSource & PartialPriceFeedSource; /** * The deadline the connection this module builds gets, set explicitly because * hermes-client defaults to 5s. * * One batch here is every reserve's feed for a lending market, and * `/v2/updates/price/latest` returns a binary blob alongside the parsed prices * whether or not the caller wants one — 5s is not a deadline that request * reliably meets. 30s is what this module guaranteed before the client changed. */ export declare const DEFAULT_HERMES_TIMEOUT_MS: number; /** * The config the SDK's own Hermes connection is built with. * * Split out from `resolvePythConnection` so the DEFAULT is assertable without a * 30-second test: hermes-client keeps `timeout` private, so once a client is * constructed there is nothing left to observe, and a test written against the * built connection can only prove that a config the CALLER passed came through * — never that the default did. * * Exported for the same reason it is useful internally: a caller assembling its * own client (failover, throttling) can start from the SDK's defaults instead of * silently inheriting hermes-client's. */ export declare const hermesConnectionConfig: (hermesConfig?: HermesClientConfig) => HermesClientConfig; /** * Public Hermes has required a Pyth API key since 2026-08-26 16:00 UTC, and * nothing in this module can supply one — so an unconfigured caller gets a 401 * on every price read. Said once, at construction, because the alternative is * discovering it per-request from inside whatever swallows the rejection. * * Two ways to be authenticated, and both are honoured: `accessToken`, which * hermes-client turns into a Bearer header itself, or an `Authorization` * header passed as a plain object. A header in either other `HeadersInit` form * gets its own warning, because it is silently dropped in transit rather than * rejected — the caller believes they configured auth and did not. * * Exported for tests only. */ export declare const warnIfPublicHermesIsUnauthenticated: (endpoint: string, hermesConfig?: HermesClientConfig) => void; /** Tests only: forget that the unauthenticated warning was already emitted. */ export declare const resetPublicHermesAuthWarning: () => void; /** * Pick the price source `initializeSuilend` will use. * * Lives here rather than in initialize.ts because it belongs beside * getWorkingPythEndpoint, the thing it decides whether to call. * * The `/live` probe now runs ONLY when a `fallbackPythEndpoint` was supplied, * which is the only case where its answer can change anything: with no * fallback, `getWorkingPythEndpoint` returns the primary whether the probe * passes or fails, so it was a request and up to 5s spent on a decision already * made. Post-cutover it was worse than wasted — it reports Hermes healthy * (`/live` needs no key) for an endpoint that 401s every real read, so it * certified the broken path. Skipping it for an injected source is the same * argument taken further: that probe answers for whether Hermes is up, not for * whether it still serves the caller's feeds, nor whether the caller is even * pointed at Hermes. * * `hermesConfig` exists so the commonest reason to reach for an injected source * — an API key, which public Hermes now requires — does not cost the caller a * hand-rolled `PriceFeedSource`. Pass it as `accessToken`, which hermes-client * turns into a Bearer header on every request; a plain-object * `headers: { Authorization: ... }` also works, but the other two `HeadersInit` * forms do not survive its object-spread merge. It configures only the * connection built here; an injected source is returned untouched, config or * not, because there is nothing left to build. * * Declared as `PriceFeedSource` because that is all an OVERRIDE is known to be. * The source built here is also a `PartialPriceFeedSource`, which is a behaviour * change worth stating: `tolerateMissingPriceFeeds` used to throw on the default * connection, because a stock `SuiPriceServiceConnection` could not report which * feeds it failed to serve. Hermes' `ignoreInvalidPriceIds` can, so the flag now * works without an injected connection. */ export declare const resolvePythConnection: (pythConnectionOverride?: PriceFeedSource, fallbackPythEndpoint?: string, hermesConfig?: HermesClientConfig) => Promise; /** * Where `SuilendClient` gets the signed price update it puts ON-CHAIN. * * Not the same thing as `PriceFeedSource`, and the distinction is the whole * point of this type existing. `PriceFeedSource` answers "what is this feed * worth" and is read off-chain; the answer is a number and any Hermes can * produce it. This answers "give me a VAA the Pyth deployment our contracts * link will verify" — a signed blob that a *specific* deployment's guardian set * has to have signed. Injecting one has never affected the other, which is how * an app could believe it had moved to Pyth Pro while every transaction it built * still carried a public-Hermes VAA. * * One method, because that is all `SuilendClient` ever calls on the field. */ export interface PriceUpdateDataSource { getPriceFeedsUpdateData(priceIds: string[]): Promise; } /** * Register the process-wide source for on-chain price updates. Call at startup; * clients constructed BEFORE the call still pick it up, because the SDK default * resolves late (see `deferredPriceUpdateDataSource`). * * Process-wide rather than a constructor argument on purpose: consumers build a * `SuilendClient` in many places (perpetual-ui alone has 17 `initialize` call * sites across four apps), and a per-site argument means the site somebody adds * next silently reverts to the broken path. One registration covers every * instance, including ones constructed by code the caller does not own. * * Resolution order, most specific first: * 1. `client.pythConnection = …` — a per-instance assignment always wins, and * is how the indexer injects its authenticated failover connection. * 2. whatever was registered here. * 3. the SDK default. * * That order is load-bearing for the planned change to have the SDK resolve its * own default endpoint: adding a real default at (3) cannot fight a consumer * that registered at (2), so registrations made now become redundant rather * than wrong. */ export declare function setPriceUpdateDataSource(source: PriceUpdateDataSource): void; /** * Test-only. The registration is process-wide by design, so without this one * test's source would leak into the next. */ export declare function clearPriceUpdateDataSource(): void; /** * The source a newly-constructed `SuilendClient` should start with: ALWAYS the * late-resolving wrapper, never the registered source directly. * * Returning the registered source when one happened to exist made resolution * depend on construction order after all — a client built before registration * followed the registry, one built after snapshotted it. So a re-registration (a * provider re-running on hot reload, a refreshed credential behind the source) * left every already-built client calling the source it replaced, which is the * same silent-divergence failure this seam exists to prevent, reached through * timing instead of a missed call site. * * Per-instance assignment still wins, because the field stays public and * reassignable; this only decides what a client starts with. */ export declare function resolvePriceUpdateDataSource(): PriceUpdateDataSource;