@suilend/sdk 4.0.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
1
1
  import { SuiClientTypes } from "@mysten/sui/client";
2
2
  import { SuiGrpcClient } from "@mysten/sui/grpc";
3
+ import { SuiPriceServiceConnection } from "@pythnetwork/pyth-sui-js";
3
4
  import BigNumber from "bignumber.js";
4
5
  import { Reserve } from "../_generated/suilend/reserve/structs";
5
6
  import { SuilendClient } from "../client";
@@ -9,7 +10,87 @@ export declare const RESERVES_CUSTOM_ORDER: Record<string, string[]>;
9
10
  export declare const NORMALIZED_MAYA_COINTYPE: string;
10
11
  export declare const NORMALIZED_mPOINTS_COINTYPE: string;
11
12
  export declare const NORMALIZED_TREATS_COINTYPE: string;
12
- export declare const initializeSuilend: (suiGrpcClient: SuiGrpcClient, suilendClient: SuilendClient, lendingMarketMetadata?: LendingMarketMetadata, fallbackPythEndpoint?: string) => Promise<{
13
+ /**
14
+ * Price reserves for `initializeSuilend`, honouring the opt-in tolerance flag.
15
+ *
16
+ * Extracted so both branches are directly testable — the difference between
17
+ * them is a throw, which is the whole reason the flag exists and is exactly
18
+ * what a regression here would silently change.
19
+ */
20
+ export declare const refreshReservesForInitialize: (reserves: Reserve<string>[], pythConnection: SuiPriceServiceConnection, tolerateMissingPriceFeeds?: boolean) => Promise<{
21
+ reserves: Reserve<string>[];
22
+ unpricedCoinTypes: string[];
23
+ }>;
24
+ /**
25
+ * Price the temporary-Pyth-feed reserves from the cached-price service, in
26
+ * place, and report which ones ended up with a FABRICATED price.
27
+ *
28
+ * These reserves never reach the Pyth refresh at all, so nothing else can
29
+ * report them. When the cached lookup fails the price falls back to a
30
+ * hardcoded 0.0001 — a placeholder chosen to be non-zero, not a market price —
31
+ * and previously that was indistinguishable from a real quote: the reserve
32
+ * carried a plausible-looking number and appeared in no gap report.
33
+ *
34
+ * Extracted for the same reason as `refreshReservesForInitialize`:
35
+ * `initializeSuilend` needs a gRPC client and a SuilendClient, so logic left
36
+ * inline in it cannot be tested, and "is this price real" is not something to
37
+ * leave unverified.
38
+ */
39
+ export declare const priceTemporaryPythFeedReserves: (reserves: Reserve<string>[], fetchPrice?: (coinType: string) => Promise<number | undefined>) => Promise<string[]>;
40
+ export declare const initializeSuilend: (suiGrpcClient: SuiGrpcClient, suilendClient: SuilendClient, lendingMarketMetadata?: LendingMarketMetadata, fallbackPythEndpoint?: string, options?: {
41
+ /**
42
+ * Price connection to use instead of the one this function would build.
43
+ *
44
+ * Without this, the connection is constructed here and never exposed, so a
45
+ * caller cannot authenticate it, point it at a different Hermes deployment,
46
+ * or give it failover — it is unreachable from outside.
47
+ *
48
+ * SUPERSEDES `fallbackPythEndpoint`, which is ignored when this is set.
49
+ * That parameter is a hint for building a connection, and there is nothing
50
+ * left to build; it engages only when a `/live` probe fails, which tests
51
+ * whether Hermes is UP, not whether it still serves your feeds (a 401 or a
52
+ * per-feed 404 never trips it). Supply failover on the connection you
53
+ * inject — a multi-endpoint connection covers strictly more than the probe
54
+ * did.
55
+ *
56
+ * YOU ALSO OWN THE TIMEOUT. The connection this function builds for itself
57
+ * uses 30s; pyth-sui-js defaults to 5s, so an injected connection
58
+ * constructed with default options gets a 6x tighter deadline than
59
+ * initializeSuilend used to guarantee. Nothing here can impose a timeout on
60
+ * an object it did not construct — set it when you build the connection.
61
+ *
62
+ * Must be a `SuiPriceServiceConnection` (a subclass is fine, and is how you
63
+ * add `getLatestPriceFeedsPartial`). Not the structural
64
+ * `PartialPriceFeedSource` shape: with `tolerateMissingPriceFeeds` off this
65
+ * connection goes to `refreshReservePrice`, which needs the class's own
66
+ * `getLatestPriceFeeds`. The lower-level `refreshReservePriceTolerant` does
67
+ * accept the structural shape, since it only ever needs the partial fetch.
68
+ */
69
+ pythConnection?: SuiPriceServiceConnection;
70
+ /**
71
+ * Let a reserve whose feed no endpoint can serve keep the price already in
72
+ * its on-chain state, instead of failing the whole market. Off by default.
73
+ *
74
+ * OPT-IN because it trades a loud failure for a quiet one, and only the
75
+ * caller knows which it wants. A UI is usually better off failing visibly
76
+ * than rendering a stale price with no indication; a liquidator watching 45
77
+ * reserves is better off pricing the 41 it can than going blind over the 4
78
+ * it cannot.
79
+ *
80
+ * REQUIRES `pythConnection` to implement `getLatestPriceFeedsPartial` —
81
+ * throws otherwise, rather than degrading to the identical hard failure
82
+ * this flag exists to avoid. The connection this function builds for itself
83
+ * is all-or-nothing, so the flag cannot be used alone.
84
+ *
85
+ * Callers that enable this MUST read `unpricedCoinTypes` and decide per
86
+ * reserve what a kept price is allowed to mean — nothing here makes that
87
+ * judgement for them. An unpriced reserve stays in `refreshedRawReserves`
88
+ * carrying its on-chain price, which may be zero, so folding that array
89
+ * into collateral or borrow-limit math without excluding those coin types
90
+ * prices them at literal zero.
91
+ */
92
+ tolerateMissingPriceFeeds?: boolean;
93
+ }) => Promise<{
13
94
  lendingMarket: {
14
95
  version: bigint;
15
96
  reserves: {
@@ -148,6 +229,7 @@ export declare const initializeSuilend: (suiGrpcClient: SuiGrpcClient, suilendCl
148
229
  };
149
230
  coinMetadataMap: Record<string, SuiClientTypes.CoinMetadata>;
150
231
  refreshedRawReserves: Reserve<string>[];
232
+ unpricedCoinTypes: string[];
151
233
  reserveMap: Record<string, {
152
234
  config: {
153
235
  $typeName: string;
package/lib/initialize.js CHANGED
@@ -1,12 +1,11 @@
1
1
  import { normalizeStructTag } from "@mysten/sui/utils";
2
- import { SuiPriceServiceConnection } from "@pythnetwork/pyth-sui-js";
3
2
  import BigNumber from "bignumber.js";
4
3
  import { NORMALIZED_ALKIMI_COINTYPE, NORMALIZED_AUSD_COINTYPE, NORMALIZED_BLUE_COINTYPE, NORMALIZED_BUCK_COINTYPE, NORMALIZED_DEEP_COINTYPE, NORMALIZED_DMC_COINTYPE, NORMALIZED_FUD_COINTYPE, NORMALIZED_HAEDAL_COINTYPE, NORMALIZED_HIPPO_COINTYPE, NORMALIZED_IKA_COINTYPE, NORMALIZED_KOBAN_COINTYPE, NORMALIZED_LBTC_COINTYPE, NORMALIZED_NS_COINTYPE, NORMALIZED_SEND_COINTYPE, NORMALIZED_SEND_POINTS_S1_COINTYPE, NORMALIZED_SEND_POINTS_S2_COINTYPE, NORMALIZED_SOL_COINTYPE, NORMALIZED_SUI_COINTYPE, NORMALIZED_UP_COINTYPE, NORMALIZED_USDB_COINTYPE, NORMALIZED_USDC_COINTYPE, NORMALIZED_USDsui_COINTYPE, NORMALIZED_WAL_COINTYPE, NORMALIZED_WBTC_COINTYPE, NORMALIZED_WETH_COINTYPE, NORMALIZED_XAUm_COINTYPE, NORMALIZED_eEARN_COINTYPE, NORMALIZED_eTHIRD_COINTYPE, NORMALIZED_flSUI_COINTYPE, NORMALIZED_fpSUI_COINTYPE, NORMALIZED_fudSUI_COINTYPE, NORMALIZED_iSUI_COINTYPE, NORMALIZED_jugSUI_COINTYPE, NORMALIZED_kSUI_COINTYPE, NORMALIZED_mSUI_COINTYPE, NORMALIZED_mUSD_COINTYPE, NORMALIZED_oshiSUI_COINTYPE, NORMALIZED_sSUI_COINTYPE, NORMALIZED_sdeUSD_COINTYPE, NORMALIZED_stratSUI_COINTYPE, NORMALIZED_suiETH_COINTYPE, NORMALIZED_suiUSDT_COINTYPE, NORMALIZED_suiUSDe_COINTYPE, NORMALIZED_suiWBTC_COINTYPE, NORMALIZED_trevinSUI_COINTYPE, NORMALIZED_upSUI_COINTYPE, NORMALIZED_wUSDC_COINTYPE, NORMALIZED_wUSDT_COINTYPE, NORMALIZED_xBTC_COINTYPE, NORMALIZED_yapSUI_COINTYPE, TEMPORARY_PYTH_PRICE_FEED_COINTYPES, getAllOwnedObjects, getCoinMetadataMap, getPrice, isSendPoints, isSteammPoints, } from "@suilend/sui-core";
5
4
  import { LENDING_MARKET_ID, SuilendClient } from "../client.js";
6
5
  import { parseLendingMarket, parseObligation } from "../parsers/index.js";
7
6
  import * as simulate from "../utils/simulate.js";
8
7
  import { WAD } from "./constants.js";
9
- import { getWorkingPythEndpoint } from "./pyth.js";
8
+ import { resolvePythConnection } from "./pyth.js";
10
9
  import { STRATEGY_TYPE_INFO_MAP, STRATEGY_WRAPPER_PACKAGE_ID_V1, } from "./strategyOwnerCap.js";
11
10
  export const RESERVES_CUSTOM_ORDER = {
12
11
  [LENDING_MARKET_ID]: [
@@ -85,7 +84,79 @@ const TREATS_COINTYPE = "0x0dadb7fa2771c2952f96161fc1f0c105d1f22d53926b9ff2498a8
85
84
  export const NORMALIZED_MAYA_COINTYPE = normalizeStructTag(MAYA_COINTYPE);
86
85
  export const NORMALIZED_mPOINTS_COINTYPE = normalizeStructTag(mPOINTS_COINTYPE);
87
86
  export const NORMALIZED_TREATS_COINTYPE = normalizeStructTag(TREATS_COINTYPE);
88
- export const initializeSuilend = async (suiGrpcClient, suilendClient, lendingMarketMetadata, fallbackPythEndpoint) => {
87
+ /**
88
+ * Price reserves for `initializeSuilend`, honouring the opt-in tolerance flag.
89
+ *
90
+ * Extracted so both branches are directly testable — the difference between
91
+ * them is a throw, which is the whole reason the flag exists and is exactly
92
+ * what a regression here would silently change.
93
+ */
94
+ export const refreshReservesForInitialize = async (reserves, pythConnection, tolerateMissingPriceFeeds) => {
95
+ if (tolerateMissingPriceFeeds) {
96
+ // Tolerance is only expressible over a connection that can report WHICH
97
+ // feeds it could not serve. A stock SuiPriceServiceConnection cannot: the
98
+ // method does not exist in pyth-sui-js, so the tolerant path would fall
99
+ // back to the all-or-nothing fetch, and Hermes v2 404s the whole batch over
100
+ // one unknown id — the caller would get the same hard failure as before the
101
+ // flag existed, having explicitly asked not to.
102
+ //
103
+ // Rejected here rather than left to degrade quietly, because the pairing is
104
+ // otherwise invisible: nothing in the flag's name or type says it needs a
105
+ // connection the SDK cannot build for itself. Checked on CAPABILITY, not on
106
+ // whether a connection was injected — injecting a stock one is equally
107
+ // useless. Direct callers of refreshReservePriceTolerant keep the strict
108
+ // fallback, which is correct for an endpoint that omits unknown ids.
109
+ if (!simulate.canReportMissingPriceFeeds(pythConnection)) {
110
+ throw new Error(`tolerateMissingPriceFeeds requires a connection that can report per-feed gaps. The connection in use is all-or-nothing, so an endpoint that rejects the whole batch over one unknown feed would fail initialization regardless of this flag — ${simulate.PARTIAL_FETCH_REQUIRED_HINT}. Note that patching SuiPriceServiceConnection.prototype does NOT satisfy this: it can reroute getLatestPriceFeeds but cannot add a per-feed gap report.`);
111
+ }
112
+ return simulate.refreshReservePriceTolerant(reserves, pythConnection);
113
+ }
114
+ // Default path, behaviourally identical to what this function did before the
115
+ // flag existed: throws if any requested feed is absent from the response.
116
+ return {
117
+ reserves: await simulate.refreshReservePrice(reserves, pythConnection),
118
+ unpricedCoinTypes: [],
119
+ };
120
+ };
121
+ /**
122
+ * Price the temporary-Pyth-feed reserves from the cached-price service, in
123
+ * place, and report which ones ended up with a FABRICATED price.
124
+ *
125
+ * These reserves never reach the Pyth refresh at all, so nothing else can
126
+ * report them. When the cached lookup fails the price falls back to a
127
+ * hardcoded 0.0001 — a placeholder chosen to be non-zero, not a market price —
128
+ * and previously that was indistinguishable from a real quote: the reserve
129
+ * carried a plausible-looking number and appeared in no gap report.
130
+ *
131
+ * Extracted for the same reason as `refreshReservesForInitialize`:
132
+ * `initializeSuilend` needs a gRPC client and a SuilendClient, so logic left
133
+ * inline in it cannot be tested, and "is this price real" is not something to
134
+ * leave unverified.
135
+ */
136
+ export const priceTemporaryPythFeedReserves = async (reserves, fetchPrice = getPrice) => {
137
+ const fabricatedPriceCoinTypes = [];
138
+ await Promise.all(reserves.map(async (reserve) => {
139
+ const coinType = normalizeStructTag(reserve.coinType.name);
140
+ let cachedUsdPrice;
141
+ try {
142
+ cachedUsdPrice = await fetchPrice(coinType);
143
+ }
144
+ catch (err) {
145
+ console.error(err);
146
+ }
147
+ if (cachedUsdPrice === undefined) {
148
+ cachedUsdPrice = 0.0001; // Non-zero price override if no price
149
+ fabricatedPriceCoinTypes.push(coinType);
150
+ }
151
+ const parsedCachedUsdPrice = BigInt(+new BigNumber(cachedUsdPrice)
152
+ .times(WAD)
153
+ .integerValue(BigNumber.ROUND_DOWN));
154
+ reserve.price.value = parsedCachedUsdPrice;
155
+ reserve.smoothedPrice.value = parsedCachedUsdPrice;
156
+ }));
157
+ return fabricatedPriceCoinTypes;
158
+ };
159
+ export const initializeSuilend = async (suiGrpcClient, suilendClient, lendingMarketMetadata, fallbackPythEndpoint, options) => {
89
160
  const nowMs = Date.now();
90
161
  const nowS = Math.floor(nowMs / 1000);
91
162
  const interestCompoundedRawReserves = suilendClient.lendingMarket.reserves.map((r) => simulate.compoundReserveInterest(r, nowS));
@@ -100,35 +171,35 @@ export const initializeSuilend = async (suiGrpcClient, suilendClient, lendingMar
100
171
  reservesWithoutTemporaryPythPriceFeeds.push(reserve);
101
172
  }
102
173
  }
103
- // Get a working Pyth endpoint (try primary, fallback to fallbackPythEndpoint if provided)
104
- const pythEndpoint = await getWorkingPythEndpoint(fallbackPythEndpoint);
105
- const pythConnection = new SuiPriceServiceConnection(pythEndpoint, {
106
- timeout: 30 * 1000,
107
- });
108
- const [refreshedReservesWithoutTemporaryPythPriceFeeds] = await Promise.all([
109
- simulate.refreshReservePrice(reservesWithoutTemporaryPythPriceFeeds, pythConnection),
110
- Promise.all(reservesWithTemporaryPythPriceFeeds.map((reserve) => (async () => {
111
- let cachedUsdPrice;
112
- try {
113
- cachedUsdPrice = await getPrice(normalizeStructTag(reserve.coinType.name));
114
- }
115
- catch (err) {
116
- console.error(err);
117
- }
118
- if (cachedUsdPrice === undefined)
119
- cachedUsdPrice = 0.0001; // Non-zero price override if no price
120
- const parsedCachedUsdPrice = BigInt(+new BigNumber(cachedUsdPrice)
121
- .times(WAD)
122
- .integerValue(BigNumber.ROUND_DOWN));
123
- reserve.price.value = parsedCachedUsdPrice;
124
- reserve.smoothedPrice.value = parsedCachedUsdPrice;
125
- })())),
174
+ const pythConnection = await resolvePythConnection(options?.pythConnection, fallbackPythEndpoint);
175
+ const [refreshedWithoutTemporary, fabricatedPriceCoinTypes] = await Promise.all([
176
+ refreshReservesForInitialize(reservesWithoutTemporaryPythPriceFeeds, pythConnection, options?.tolerateMissingPriceFeeds),
177
+ priceTemporaryPythFeedReserves(reservesWithTemporaryPythPriceFeeds),
126
178
  ]);
127
179
  // Recombine reserves back into a single array
128
180
  const refreshedRawReserves = [
129
- ...refreshedReservesWithoutTemporaryPythPriceFeeds,
181
+ ...refreshedWithoutTemporary.reserves,
130
182
  ...reservesWithTemporaryPythPriceFeeds,
131
183
  ];
184
+ // Reserves NOT carrying a real refreshed price, from either cause: no endpoint
185
+ // served the Pyth feed (needs `tolerateMissingPriceFeeds`, else the refresh
186
+ // threw), or a temporary-feed reserve fell back to a fabricated placeholder
187
+ // (independent of that flag). Surfaced rather than logged-and-forgotten: only
188
+ // the caller knows whether a given reserve can safely hold such a price.
189
+ //
190
+ // Normalized, matching `reserveMap` / `reserveCoinTypes` keys. Entries in
191
+ // `refreshedRawReserves` carry the RAW on-chain `coinType.name`, which never
192
+ // compares equal to these (no `0x` prefix), so filtering that array means
193
+ // normalizing first:
194
+ //
195
+ // const unpriced = new Set(unpricedCoinTypes);
196
+ // refreshedRawReserves.filter(
197
+ // (r) => !unpriced.has(normalizeStructTag(r.coinType.name)),
198
+ // );
199
+ const unpricedCoinTypes = [
200
+ ...refreshedWithoutTemporary.unpricedCoinTypes,
201
+ ...fabricatedPriceCoinTypes,
202
+ ];
132
203
  const miscCoinTypes = [
133
204
  NORMALIZED_SEND_POINTS_S1_COINTYPE,
134
205
  NORMALIZED_SEND_POINTS_S2_COINTYPE,
@@ -193,6 +264,7 @@ export const initializeSuilend = async (suiGrpcClient, suilendClient, lendingMar
193
264
  lendingMarket,
194
265
  coinMetadataMap,
195
266
  refreshedRawReserves,
267
+ unpricedCoinTypes,
196
268
  reserveMap,
197
269
  reserveCoinTypes: uniqueReserveCoinTypes,
198
270
  reserveCoinMetadataMap,
package/lib/pyth.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { SuiPriceServiceConnection } from "@pythnetwork/pyth-sui-js";
1
2
  export declare const PRIMARY_PYTH_ENDPOINT = "https://hermes.pyth.network";
2
3
  /**
3
4
  * Tests if the primary Pyth connection endpoint is working by checking the /live endpoint
@@ -10,3 +11,18 @@ export declare const testPrimaryPythConnection: () => Promise<boolean>;
10
11
  * @returns The endpoint URL that is working, or the primary endpoint if fallback fails
11
12
  */
12
13
  export declare const getWorkingPythEndpoint: (fallbackPythEndpoint?: string) => Promise<string>;
14
+ /**
15
+ * Pick the price connection `initializeSuilend` will use.
16
+ *
17
+ * Lives here rather than in initialize.ts for two reasons: it belongs beside
18
+ * getWorkingPythEndpoint, the thing it decides whether to call; and this module
19
+ * imports nothing, so the seam is testable without resolving
20
+ * `@suilend/sui-core` (whose in-repo `exports` points at a .js file that does
21
+ * not exist, making initialize.ts unimportable from source).
22
+ *
23
+ * Skipping the probe for an injected connection is deliberate, not an
24
+ * optimisation. getWorkingPythEndpoint fetches hermes.pyth.network/live, which
25
+ * answers for whether Hermes is up — not for whether it still serves the
26
+ * caller's feeds, and not for whether the caller is even pointed at Hermes.
27
+ */
28
+ export declare const resolvePythConnection: (pythConnectionOverride?: SuiPriceServiceConnection, fallbackPythEndpoint?: string) => Promise<SuiPriceServiceConnection>;
package/lib/pyth.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { SuiPriceServiceConnection } from "@pythnetwork/pyth-sui-js";
1
2
  export const PRIMARY_PYTH_ENDPOINT = "https://hermes.pyth.network";
2
3
  /**
3
4
  * Tests if the primary Pyth connection endpoint is working by checking the /live endpoint
@@ -37,3 +38,26 @@ export const getWorkingPythEndpoint = async (fallbackPythEndpoint) => {
37
38
  return PRIMARY_PYTH_ENDPOINT;
38
39
  }
39
40
  };
41
+ /**
42
+ * Pick the price connection `initializeSuilend` will use.
43
+ *
44
+ * Lives here rather than in initialize.ts for two reasons: it belongs beside
45
+ * getWorkingPythEndpoint, the thing it decides whether to call; and this module
46
+ * imports nothing, so the seam is testable without resolving
47
+ * `@suilend/sui-core` (whose in-repo `exports` points at a .js file that does
48
+ * not exist, making initialize.ts unimportable from source).
49
+ *
50
+ * Skipping the probe for an injected connection is deliberate, not an
51
+ * optimisation. getWorkingPythEndpoint fetches hermes.pyth.network/live, which
52
+ * answers for whether Hermes is up — not for whether it still serves the
53
+ * caller's feeds, and not for whether the caller is even pointed at Hermes.
54
+ */
55
+ export const resolvePythConnection = async (pythConnectionOverride, fallbackPythEndpoint) => {
56
+ if (pythConnectionOverride)
57
+ return pythConnectionOverride;
58
+ // Get a working Pyth endpoint (try primary, fallback to fallbackPythEndpoint if provided)
59
+ const pythEndpoint = await getWorkingPythEndpoint(fallbackPythEndpoint);
60
+ return new SuiPriceServiceConnection(pythEndpoint, {
61
+ timeout: 30 * 1000,
62
+ });
63
+ };
package/package.json CHANGED
@@ -1 +1 @@
1
- {"name":"@suilend/sdk","version":"4.0.0","private":false,"description":"A TypeScript SDK for interacting with the Suilend program","author":"Suilend","license":"MIT","main":"./index.js","exports":{".":"./index.js","./mmt":"./mmt.js","./strategies":"./strategies.js","./client":"./client.js","./utils":"./utils/index.js","./utils/simulate":"./utils/simulate.js","./utils/obligation":"./utils/obligation.js","./utils/events":"./utils/events.js","./margin":"./margin/index.js","./lib/transactions":"./lib/transactions.js","./lib/strategyOwnerCap":"./lib/strategyOwnerCap.js","./lib/constants":"./lib/constants.js","./lib/types":"./lib/types.js","./lib":"./lib/index.js","./lib/pyth":"./lib/pyth.js","./lib/liquidityMining":"./lib/liquidityMining.js","./lib/initialize":"./lib/initialize.js","./lib/pythAdapter":"./lib/pythAdapter.js","./swap":"./swap/index.js","./swap/transaction":"./swap/transaction.js","./swap/quote":"./swap/quote.js","./parsers/reserve":"./parsers/reserve.js","./parsers/rateLimiter":"./parsers/rateLimiter.js","./parsers":"./parsers/index.js","./parsers/apiReserveAssetDataEvent":"./parsers/apiReserveAssetDataEvent.js","./parsers/obligation":"./parsers/obligation.js","./parsers/lendingMarket":"./parsers/lendingMarket.js","./api":"./api/index.js","./api/events":"./api/events.js","./margin/utils":"./margin/utils/index.js","./margin/margin/market":"./margin/margin/market.js","./margin/margin/version":"./margin/margin/version.js","./margin/margin/position":"./margin/margin/position.js","./margin/margin/router":"./margin/margin/router.js","./margin/margin/admin_cap":"./margin/margin/admin_cap.js","./margin/margin/permissions":"./margin/margin/permissions.js","./_generated/suilend":"./_generated/suilend/index.js","./_generated/_framework/reified":"./_generated/_framework/reified.js","./_generated/_framework/util":"./_generated/_framework/util.js","./_generated/_framework/vector":"./_generated/_framework/vector.js","./_generated/suilend/lending-market-registry/functions":"./_generated/suilend/lending-market-registry/functions.js","./_generated/suilend/obligation/structs":"./_generated/suilend/obligation/structs.js","./_generated/suilend/reserve-config/structs":"./_generated/suilend/reserve-config/structs.js","./_generated/suilend/reserve-config/functions":"./_generated/suilend/reserve-config/functions.js","./_generated/suilend/rate-limiter/structs":"./_generated/suilend/rate-limiter/structs.js","./_generated/suilend/rate-limiter/functions":"./_generated/suilend/rate-limiter/functions.js","./_generated/suilend/cell/structs":"./_generated/suilend/cell/structs.js","./_generated/suilend/liquidity-mining/structs":"./_generated/suilend/liquidity-mining/structs.js","./_generated/suilend/lending-market/structs":"./_generated/suilend/lending-market/structs.js","./_generated/suilend/lending-market/functions":"./_generated/suilend/lending-market/functions.js","./_generated/suilend/decimal/structs":"./_generated/suilend/decimal/structs.js","./_generated/suilend/reserve/structs":"./_generated/suilend/reserve/structs.js","./margin/margin/deps/suilend/lending_market":"./margin/margin/deps/suilend/lending_market.js","./margin/margin/deps/sui/vec_set":"./margin/margin/deps/sui/vec_set.js","./margin/margin/deps/std/type_name":"./margin/margin/deps/std/type_name.js","./_generated/_dependencies/source/0x1":"./_generated/_dependencies/source/0x1/index.js","./_generated/_dependencies/source/0x2":"./_generated/_dependencies/source/0x2/index.js","./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e":"./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/index.js","./_generated/_dependencies/source/0x1/option/structs":"./_generated/_dependencies/source/0x1/option/structs.js","./_generated/_dependencies/source/0x1/ascii/structs":"./_generated/_dependencies/source/0x1/ascii/structs.js","./_generated/_dependencies/source/0x1/type-name/structs":"./_generated/_dependencies/source/0x1/type-name/structs.js","./_generated/_dependencies/source/0x2/balance/structs":"./_generated/_dependencies/source/0x2/balance/structs.js","./_generated/_dependencies/source/0x2/object/structs":"./_generated/_dependencies/source/0x2/object/structs.js","./_generated/_dependencies/source/0x2/object-table/structs":"./_generated/_dependencies/source/0x2/object-table/structs.js","./_generated/_dependencies/source/0x2/bag/structs":"./_generated/_dependencies/source/0x2/bag/structs.js","./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/price-identifier/structs":"./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/price-identifier/structs.js","./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/price-info/structs":"./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/price-info/structs.js","./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/price/structs":"./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/price/structs.js","./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/price-feed/structs":"./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/price-feed/structs.js","./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/i64/structs":"./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/i64/structs.js"},"types":"./index.d.ts","scripts":{"build":"rm -rf ./dist && bun tsc && node ./fix-esm-imports.js","typecheck":"tsc --noEmit","lint:ci":"tsc --noEmit","prettier":"prettier --write src/","release":"bun run build && bun ./release.js && cd ./dist && npm publish --access public"},"repository":{"type":"git","url":"git+https://github.com/fireflyprotocol/lending-mono.git","directory":"ts/sdks/sdk"},"dependencies":{"@bluefin-exchange/bluefin7k-aggregator-sdk":"^7.3.0","@cetusprotocol/aggregator-sdk":"^1.5.7","@flowx-finance/sdk":"^2.1.0","@pythnetwork/pyth-sui-js":"2.2.0","@suilend/springsui-sdk":"^4.0.0","bignumber.js":"^9.1.2","bn.js":"^5.2.2","crypto-js":"^4.2.0","lodash":"^4.17.21","p-limit":"3.1.0","uuid":"^11.0.3"},"devDependencies":{"@types/bn.js":"^5.2.0","@types/lodash":"^4.17.20","ts-node":"^10.9.2","typescript":"^5.6.3","@tsconfig/recommended":"^1.0.8","@types/node":"^22.9.0"},"peerDependencies":{"@mysten/bcs":"^2.0.5","@mysten/sui":"2.17.0","@suilend/sui-core":"^0.1.0"},"type":"module"}
1
+ {"name":"@suilend/sdk","version":"5.0.0","private":false,"description":"A TypeScript SDK for interacting with the Suilend program","author":"Suilend","license":"MIT","main":"./index.js","exports":{".":"./index.js","./mmt":"./mmt.js","./strategies":"./strategies.js","./client":"./client.js","./utils":"./utils/index.js","./utils/simulate":"./utils/simulate.js","./utils/obligation":"./utils/obligation.js","./utils/events":"./utils/events.js","./margin":"./margin/index.js","./lib/transactions":"./lib/transactions.js","./lib/strategyOwnerCap":"./lib/strategyOwnerCap.js","./lib/constants":"./lib/constants.js","./lib/types":"./lib/types.js","./lib":"./lib/index.js","./lib/pyth":"./lib/pyth.js","./lib/liquidityMining":"./lib/liquidityMining.js","./lib/initialize":"./lib/initialize.js","./lib/pythAdapter":"./lib/pythAdapter.js","./swap":"./swap/index.js","./swap/transaction":"./swap/transaction.js","./swap/quote":"./swap/quote.js","./parsers/reserve":"./parsers/reserve.js","./parsers/rateLimiter":"./parsers/rateLimiter.js","./parsers":"./parsers/index.js","./parsers/apiReserveAssetDataEvent":"./parsers/apiReserveAssetDataEvent.js","./parsers/obligation":"./parsers/obligation.js","./parsers/lendingMarket":"./parsers/lendingMarket.js","./api":"./api/index.js","./api/events":"./api/events.js","./margin/utils":"./margin/utils/index.js","./margin/margin/market":"./margin/margin/market.js","./margin/margin/version":"./margin/margin/version.js","./margin/margin/position":"./margin/margin/position.js","./margin/margin/router":"./margin/margin/router.js","./margin/margin/admin_cap":"./margin/margin/admin_cap.js","./margin/margin/permissions":"./margin/margin/permissions.js","./_generated/suilend":"./_generated/suilend/index.js","./_generated/_framework/reified":"./_generated/_framework/reified.js","./_generated/_framework/util":"./_generated/_framework/util.js","./_generated/_framework/vector":"./_generated/_framework/vector.js","./_generated/suilend/lending-market-registry/functions":"./_generated/suilend/lending-market-registry/functions.js","./_generated/suilend/obligation/structs":"./_generated/suilend/obligation/structs.js","./_generated/suilend/reserve-config/structs":"./_generated/suilend/reserve-config/structs.js","./_generated/suilend/reserve-config/functions":"./_generated/suilend/reserve-config/functions.js","./_generated/suilend/rate-limiter/structs":"./_generated/suilend/rate-limiter/structs.js","./_generated/suilend/rate-limiter/functions":"./_generated/suilend/rate-limiter/functions.js","./_generated/suilend/cell/structs":"./_generated/suilend/cell/structs.js","./_generated/suilend/liquidity-mining/structs":"./_generated/suilend/liquidity-mining/structs.js","./_generated/suilend/lending-market/structs":"./_generated/suilend/lending-market/structs.js","./_generated/suilend/lending-market/functions":"./_generated/suilend/lending-market/functions.js","./_generated/suilend/decimal/structs":"./_generated/suilend/decimal/structs.js","./_generated/suilend/reserve/structs":"./_generated/suilend/reserve/structs.js","./margin/margin/deps/suilend/lending_market":"./margin/margin/deps/suilend/lending_market.js","./margin/margin/deps/sui/vec_set":"./margin/margin/deps/sui/vec_set.js","./margin/margin/deps/std/type_name":"./margin/margin/deps/std/type_name.js","./_generated/_dependencies/source/0x1":"./_generated/_dependencies/source/0x1/index.js","./_generated/_dependencies/source/0x2":"./_generated/_dependencies/source/0x2/index.js","./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e":"./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/index.js","./_generated/_dependencies/source/0x1/option/structs":"./_generated/_dependencies/source/0x1/option/structs.js","./_generated/_dependencies/source/0x1/ascii/structs":"./_generated/_dependencies/source/0x1/ascii/structs.js","./_generated/_dependencies/source/0x1/type-name/structs":"./_generated/_dependencies/source/0x1/type-name/structs.js","./_generated/_dependencies/source/0x2/balance/structs":"./_generated/_dependencies/source/0x2/balance/structs.js","./_generated/_dependencies/source/0x2/object/structs":"./_generated/_dependencies/source/0x2/object/structs.js","./_generated/_dependencies/source/0x2/object-table/structs":"./_generated/_dependencies/source/0x2/object-table/structs.js","./_generated/_dependencies/source/0x2/bag/structs":"./_generated/_dependencies/source/0x2/bag/structs.js","./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/price-identifier/structs":"./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/price-identifier/structs.js","./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/price-info/structs":"./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/price-info/structs.js","./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/price/structs":"./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/price/structs.js","./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/price-feed/structs":"./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/price-feed/structs.js","./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/i64/structs":"./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/i64/structs.js"},"types":"./index.d.ts","scripts":{"build":"rm -rf ./dist && bun tsc && node ./fix-esm-imports.js","typecheck":"tsc --noEmit && tsc --noEmit -p tsconfig.test.json","test":"bun test tests/","lint:ci":"bun run typecheck","prettier":"prettier --write src/ tests/","release":"bun run build && bun ./release.js && cd ./dist && npm publish --access public"},"repository":{"type":"git","url":"git+https://github.com/fireflyprotocol/lending-mono.git","directory":"ts/sdks/sdk"},"dependencies":{"@bluefin-exchange/bluefin7k-aggregator-sdk":"^7.3.0","@cetusprotocol/aggregator-sdk":"^1.5.7","@flowx-finance/sdk":"^2.1.0","@pythnetwork/pyth-sui-js":"2.2.0","@suilend/springsui-sdk":"^4.0.0","bignumber.js":"^9.1.2","bn.js":"^5.2.2","crypto-js":"^4.2.0","lodash":"^4.17.21","p-limit":"3.1.0","uuid":"^11.0.3"},"devDependencies":{"@types/bn.js":"^5.2.0","@types/lodash":"^4.17.20","ts-node":"^10.9.2","typescript":"^6.0.3","@tsconfig/recommended":"^1.0.8","@types/node":"^22.9.0"},"peerDependencies":{"@mysten/bcs":"^2.0.5","@mysten/sui":"2.17.0","@suilend/sui-core":"^1.0.0"},"type":"module"}
@@ -1,4 +1,4 @@
1
- import { SuiPriceServiceConnection } from "@pythnetwork/pyth-sui-js";
1
+ import { PriceFeed, SuiPriceServiceConnection } from "@pythnetwork/pyth-sui-js";
2
2
  import BigNumber from "bignumber.js";
3
3
  import { Decimal } from "../_generated/suilend/decimal/structs";
4
4
  import { PoolRewardManager, UserRewardManager } from "../_generated/suilend/liquidity-mining/structs";
@@ -22,6 +22,73 @@ export declare const calculateDepositAprPercent: (reserve: Reserve<string>) => B
22
22
  export declare const compoundReserveInterest: (reserve: Reserve<string>, nowS: number) => Reserve<string>;
23
23
  export declare const updatePoolRewardsManager: (manager: PoolRewardManager, nowMs: number) => PoolRewardManager;
24
24
  export declare const refreshReservePrice: (reserves: Reserve<string>[], pythConnection: SuiPriceServiceConnection) => Promise<Reserve<string>[]>;
25
+ /**
26
+ * A connection that can report which of the requested feeds it could not serve,
27
+ * instead of failing the whole batch. Structural on purpose: any object with
28
+ * this method qualifies, so a caller can inject its own client without this
29
+ * package depending on it.
30
+ */
31
+ export interface PartialPriceFeedSource {
32
+ getLatestPriceFeedsPartial(ids: string[]): Promise<{
33
+ feeds: PriceFeed[];
34
+ missing: string[];
35
+ }>;
36
+ }
37
+ /**
38
+ * Whether this connection can report WHICH requested feeds it could not serve.
39
+ *
40
+ * Exported because it is a precondition callers need to check, not just an
41
+ * internal branch: `tolerateMissingPriceFeeds` is meaningless without it (see
42
+ * `refreshReservesForInitialize`, which refuses the combination).
43
+ */
44
+ export declare function canReportMissingPriceFeeds(connection: SuiPriceServiceConnection | PartialPriceFeedSource): connection is PartialPriceFeedSource;
45
+ /**
46
+ * The remedy both gap-tolerance failure paths point at.
47
+ *
48
+ * One constant because the two paths fail at different moments for the same
49
+ * underlying reason — `refreshReservesForInitialize` refuses up front on
50
+ * capability, `refreshReservePriceTolerant` only once an endpoint actually
51
+ * rejects a batch — and separately worded advice for one cause is how the two
52
+ * drift apart.
53
+ */
54
+ export declare const PARTIAL_FETCH_REQUIRED_HINT = "inject a connection implementing getLatestPriceFeedsPartial (e.g. a failover connection that reports per-feed gaps)";
55
+ /**
56
+ * Like `refreshReservePrice`, but a reserve whose feed no endpoint can serve
57
+ * keeps the price already in its on-chain state instead of failing every other
58
+ * reserve with it.
59
+ *
60
+ * `refreshReservePrice` is all-or-nothing in both directions, and both are
61
+ * hazardous once an endpoint stops carrying every feed: a missing feed throws
62
+ * (so one retired market blinds pricing for the whole lending market), while an
63
+ * undefined response silently returns EVERY reserve at its stale price with no
64
+ * signal at all. Hermes v2 rejects an entire batch when any requested id is
65
+ * unknown, which makes both reachable in practice.
66
+ *
67
+ * This reports `unpricedCoinTypes` rather than deciding what an unpriceable
68
+ * reserve means — that judgement needs reserve config the caller has and this
69
+ * function should not second-guess. Nothing is ever fabricated here.
70
+ *
71
+ * TWO THINGS A CALLER MUST KNOW:
72
+ *
73
+ * 1. An unpriced reserve is returned in `reserves` carrying whatever price its
74
+ * on-chain state holds — which may be ZERO for a reserve that was never
75
+ * priced on-chain. Folding `reserves` into collateral or borrow-limit math
76
+ * without first excluding `unpricedCoinTypes` therefore prices that reserve
77
+ * at literal zero. `unpricedCoinTypes` is the only thing distinguishing a
78
+ * real price from a kept one; it is not optional to read.
79
+ * 2. Tolerance requires a connection that can REPORT a gap
80
+ * (`getLatestPriceFeedsPartial`). Handed a stock `SuiPriceServiceConnection`,
81
+ * this falls back to the all-or-nothing fetch, which Hermes v2 answers with
82
+ * a 404 for the whole batch when any id is unknown — so there is nothing
83
+ * left to be tolerant with. That fetch is kept because it is correct for an
84
+ * endpoint that OMITS unknown ids rather than rejecting the batch, but the
85
+ * failure is re-thrown with attribution rather than surfacing as a bare HTTP
86
+ * error. `refreshReservesForInitialize` rejects the combination outright.
87
+ */
88
+ export declare const refreshReservePriceTolerant: (reserves: Reserve<string>[], pythConnection: SuiPriceServiceConnection | PartialPriceFeedSource) => Promise<{
89
+ reserves: Reserve<string>[];
90
+ unpricedCoinTypes: string[];
91
+ }>;
25
92
  export declare const updateUserRewardManager: (poolManager: PoolRewardManager, userRewardManager: UserRewardManager, nowMs: number) => UserRewardManager;
26
93
  export declare const refreshObligation: (unrefreshedObligation: Obligation<string>, refreshedReserves: Reserve<string>[]) => Obligation<string>;
27
94
  export declare const numberToDecimal: (value: number) => Decimal;
package/utils/simulate.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { bcs } from "@mysten/sui/bcs";
2
- import { toHex } from "@mysten/sui/utils";
2
+ import { normalizeStructTag, toHex } from "@mysten/sui/utils";
3
3
  import BigNumber from "bignumber.js";
4
4
  import { v4 as uuidv4 } from "uuid";
5
5
  import { Decimal } from "../_generated/suilend/decimal/structs.js";
@@ -118,25 +118,151 @@ export const updatePoolRewardsManager = (manager, nowMs) => {
118
118
  updatedManager.lastUpdateTimeMs = BigInt(nowMs);
119
119
  return updatedManager;
120
120
  };
121
+ /** The Pyth feed id a reserve is priced from, in the response's `id` encoding. */
122
+ const reserveFeedId = (reserve) => toHex(new Uint8Array(reserve.priceIdentifier.bytes));
123
+ /**
124
+ * Index feeds by id for O(1) lookup, keeping the FIRST entry per id.
125
+ *
126
+ * First-wins matches the `priceFeeds.find(...)` this replaced, so a duplicated
127
+ * id in a response resolves to the same feed it always did. Pyth does not
128
+ * duplicate ids today; the point is that switching to a Map is not allowed to
129
+ * quietly change which one is picked if it ever does.
130
+ */
131
+ const indexFeedsById = (priceFeeds) => {
132
+ const byId = new Map();
133
+ for (const feed of priceFeeds)
134
+ if (!byId.has(feed.id))
135
+ byId.set(feed.id, feed);
136
+ return byId;
137
+ };
138
+ /**
139
+ * Apply a feed's price to a COPY of `reserve`.
140
+ *
141
+ * Shared by both refresh functions so the price-mapping math has exactly one
142
+ * definition. It was duplicated verbatim, which meant a change to how a feed
143
+ * becomes a price had to be made twice or the two silently diverged.
144
+ *
145
+ * The two functions still own their own control flow — what a MISSING feed
146
+ * means is precisely where they are supposed to differ (see
147
+ * `refreshReservePriceTolerant`).
148
+ */
149
+ const withFeedPrice = (reserve, priceFeed) => {
150
+ const newReserve = { ...reserve };
151
+ newReserve.price = stringToDecimal(priceFeed.getPriceUnchecked().getPriceAsNumberUnchecked().toString());
152
+ newReserve.smoothedPrice = stringToDecimal(priceFeed.getEmaPriceUnchecked().getPriceAsNumberUnchecked().toString());
153
+ newReserve.priceLastUpdateTimestampS = BigInt(priceFeed.getPriceUnchecked().publishTime);
154
+ return newReserve;
155
+ };
121
156
  export const refreshReservePrice = async (reserves, pythConnection) => {
122
- const priceIdentifiers = Array.from(new Set(reserves.map((r) => toHex(new Uint8Array(r.priceIdentifier.bytes)))));
157
+ const priceIdentifiers = Array.from(new Set(reserves.map(reserveFeedId)));
123
158
  const priceFeeds = await pythConnection.getLatestPriceFeeds(priceIdentifiers);
159
+ // Deliberately NOT a throw, and deliberately not expressed over the tolerant
160
+ // function below: consumers outside this repo depend on `undefined` meaning
161
+ // "keep every reserve's existing price". Hazardous (see the tolerant
162
+ // function's comment), but changing it here would turn a quiet failure loud
163
+ // under callers who never asked for that.
124
164
  if (!priceFeeds)
125
165
  return reserves;
166
+ const byId = indexFeedsById(priceFeeds);
126
167
  const updatedReserves = [];
127
168
  for (let i = 0; i < reserves.length; i++) {
128
169
  const reserve = reserves[i];
129
- const priceFeed = priceFeeds.find((p) => p.id === toHex(new Uint8Array(reserve.priceIdentifier.bytes)));
170
+ const priceFeed = byId.get(reserveFeedId(reserve));
130
171
  if (!priceFeed)
131
172
  throw new Error(`Price feed not found for reserve ${reserve.coinType.name}`);
132
- const newReserve = { ...reserve };
133
- newReserve.price = stringToDecimal(priceFeed.getPriceUnchecked().getPriceAsNumberUnchecked().toString());
134
- newReserve.smoothedPrice = stringToDecimal(priceFeed.getEmaPriceUnchecked().getPriceAsNumberUnchecked().toString());
135
- newReserve.priceLastUpdateTimestampS = BigInt(priceFeed.getPriceUnchecked().publishTime);
136
- updatedReserves.push(newReserve);
173
+ updatedReserves.push(withFeedPrice(reserve, priceFeed));
137
174
  }
138
175
  return updatedReserves;
139
176
  };
177
+ /**
178
+ * Whether this connection can report WHICH requested feeds it could not serve.
179
+ *
180
+ * Exported because it is a precondition callers need to check, not just an
181
+ * internal branch: `tolerateMissingPriceFeeds` is meaningless without it (see
182
+ * `refreshReservesForInitialize`, which refuses the combination).
183
+ */
184
+ export function canReportMissingPriceFeeds(connection) {
185
+ return (typeof connection.getLatestPriceFeedsPartial ===
186
+ "function");
187
+ }
188
+ /**
189
+ * The remedy both gap-tolerance failure paths point at.
190
+ *
191
+ * One constant because the two paths fail at different moments for the same
192
+ * underlying reason — `refreshReservesForInitialize` refuses up front on
193
+ * capability, `refreshReservePriceTolerant` only once an endpoint actually
194
+ * rejects a batch — and separately worded advice for one cause is how the two
195
+ * drift apart.
196
+ */
197
+ export const PARTIAL_FETCH_REQUIRED_HINT = "inject a connection implementing getLatestPriceFeedsPartial (e.g. a failover connection that reports per-feed gaps)";
198
+ /**
199
+ * Like `refreshReservePrice`, but a reserve whose feed no endpoint can serve
200
+ * keeps the price already in its on-chain state instead of failing every other
201
+ * reserve with it.
202
+ *
203
+ * `refreshReservePrice` is all-or-nothing in both directions, and both are
204
+ * hazardous once an endpoint stops carrying every feed: a missing feed throws
205
+ * (so one retired market blinds pricing for the whole lending market), while an
206
+ * undefined response silently returns EVERY reserve at its stale price with no
207
+ * signal at all. Hermes v2 rejects an entire batch when any requested id is
208
+ * unknown, which makes both reachable in practice.
209
+ *
210
+ * This reports `unpricedCoinTypes` rather than deciding what an unpriceable
211
+ * reserve means — that judgement needs reserve config the caller has and this
212
+ * function should not second-guess. Nothing is ever fabricated here.
213
+ *
214
+ * TWO THINGS A CALLER MUST KNOW:
215
+ *
216
+ * 1. An unpriced reserve is returned in `reserves` carrying whatever price its
217
+ * on-chain state holds — which may be ZERO for a reserve that was never
218
+ * priced on-chain. Folding `reserves` into collateral or borrow-limit math
219
+ * without first excluding `unpricedCoinTypes` therefore prices that reserve
220
+ * at literal zero. `unpricedCoinTypes` is the only thing distinguishing a
221
+ * real price from a kept one; it is not optional to read.
222
+ * 2. Tolerance requires a connection that can REPORT a gap
223
+ * (`getLatestPriceFeedsPartial`). Handed a stock `SuiPriceServiceConnection`,
224
+ * this falls back to the all-or-nothing fetch, which Hermes v2 answers with
225
+ * a 404 for the whole batch when any id is unknown — so there is nothing
226
+ * left to be tolerant with. That fetch is kept because it is correct for an
227
+ * endpoint that OMITS unknown ids rather than rejecting the batch, but the
228
+ * failure is re-thrown with attribution rather than surfacing as a bare HTTP
229
+ * error. `refreshReservesForInitialize` rejects the combination outright.
230
+ */
231
+ export const refreshReservePriceTolerant = async (reserves, pythConnection) => {
232
+ const priceIdentifiers = Array.from(new Set(reserves.map(reserveFeedId)));
233
+ let priceFeeds;
234
+ if (canReportMissingPriceFeeds(pythConnection)) {
235
+ priceFeeds = (await pythConnection.getLatestPriceFeedsPartial(priceIdentifiers)).feeds;
236
+ }
237
+ else {
238
+ try {
239
+ priceFeeds = await pythConnection.getLatestPriceFeeds(priceIdentifiers);
240
+ }
241
+ catch (error) {
242
+ // Not swallowed — a transport failure must stay a failure, or every
243
+ // reserve silently reports as unpriced and the caller cannot tell a dead
244
+ // endpoint from a retired feed. Re-thrown only to name the cause, which a
245
+ // bare "HTTP error! status: 404" does not.
246
+ throw new Error(`Tolerant price refresh failed on an all-or-nothing connection: ${error instanceof Error ? error.message : String(error)}. This connection cannot report which feeds are missing, so an endpoint that rejects the whole batch over one unknown id (Hermes v2) defeats tolerance entirely — ${PARTIAL_FETCH_REQUIRED_HINT}.`, { cause: error });
247
+ }
248
+ }
249
+ const byId = indexFeedsById(priceFeeds ?? []);
250
+ const unpricedCoinTypes = [];
251
+ const updatedReserves = reserves.map((reserve) => {
252
+ const priceFeed = byId.get(reserveFeedId(reserve));
253
+ if (!priceFeed) {
254
+ // Normalized to match every other coin type this SDK returns
255
+ // (`reserveMap`, `reserveCoinTypes`, and the parsers are all
256
+ // normalizeStructTag'd). The raw on-chain form never compares equal to
257
+ // one of those keys — it lacks the `0x` prefix — so reporting it raw made
258
+ // the cross-reference this field exists for silently never match.
259
+ unpricedCoinTypes.push(normalizeStructTag(reserve.coinType.name));
260
+ return reserve;
261
+ }
262
+ return withFeedPrice(reserve, priceFeed);
263
+ });
264
+ return { reserves: updatedReserves, unpricedCoinTypes };
265
+ };
140
266
  export const updateUserRewardManager = (poolManager, userRewardManager, nowMs) => {
141
267
  const updatedUserRewardManager = { ...userRewardManager };
142
268
  for (let i = 0; i < poolManager.poolRewards.length; i++) {