@suilend/sdk 5.1.0 → 6.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.
package/lib/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from "./constants";
2
2
  export * from "./initialize";
3
3
  export * from "./liquidityMining";
4
+ export * from "./pyth";
4
5
  export * from "./transactions";
5
6
  export * from "./types";
package/lib/index.js CHANGED
@@ -1,5 +1,12 @@
1
1
  export * from "./constants.js";
2
2
  export * from "./initialize.js";
3
3
  export * from "./liquidityMining.js";
4
+ // Exported because `createHermesPriceFeedSource` is the migration path off
5
+ // `SuiPriceServiceConnection`: the price-reading half of that class is gone in
6
+ // pyth-sui-js v4, so every caller that passed one to `refreshReservePrice` needs
7
+ // this factory. The `PriceFeedSource` types it produces already reach the root
8
+ // barrel via `utils/simulate`; without this the only thing that can build one
9
+ // was reachable only by deep-importing `@suilend/sdk/lib/pyth`.
10
+ export * from "./pyth.js";
4
11
  export * from "./transactions.js";
5
12
  export * from "./types.js";
@@ -1,10 +1,11 @@
1
1
  import { SuiClientTypes } from "@mysten/sui/client";
2
2
  import { SuiGrpcClient } from "@mysten/sui/grpc";
3
- import { SuiPriceServiceConnection } from "@pythnetwork/pyth-sui-js";
4
3
  import BigNumber from "bignumber.js";
5
4
  import { Reserve } from "../_generated/suilend/reserve/structs";
6
5
  import { SuilendClient } from "../client";
7
6
  import { ParsedReserve } from "../parsers";
7
+ import * as simulate from "../utils/simulate";
8
+ import { type HermesClientConfig } from "./pyth";
8
9
  import { LendingMarketMetadata, StrategyOwnerCap } from "./types";
9
10
  export declare const RESERVES_CUSTOM_ORDER: Record<string, string[]>;
10
11
  export declare const NORMALIZED_MAYA_COINTYPE: string;
@@ -17,7 +18,7 @@ export declare const NORMALIZED_TREATS_COINTYPE: string;
17
18
  * them is a throw, which is the whole reason the flag exists and is exactly
18
19
  * what a regression here would silently change.
19
20
  */
20
- export declare const refreshReservesForInitialize: (reserves: Reserve<string>[], pythConnection: SuiPriceServiceConnection, tolerateMissingPriceFeeds?: boolean) => Promise<{
21
+ export declare const refreshReservesForInitialize: (reserves: Reserve<string>[], pythConnection: simulate.PriceFeedSource, tolerateMissingPriceFeeds?: boolean) => Promise<{
21
22
  reserves: Reserve<string>[];
22
23
  unpricedCoinTypes: string[];
23
24
  }>;
@@ -54,19 +55,38 @@ export declare const initializeSuilend: (suiGrpcClient: SuiGrpcClient, suilendCl
54
55
  * did.
55
56
  *
56
57
  * 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.
58
+ * uses `DEFAULT_HERMES_TIMEOUT_MS` (30s); hermes-client defaults to 5s, so
59
+ * an injected connection constructed with default options gets a 6x tighter
60
+ * deadline than initializeSuilend guarantees. Nothing here can impose a
61
+ * timeout on an object it did not construct — set it when you build the
62
+ * connection. If a timeout (or an access token) is the ONLY reason you were
63
+ * going to inject, use `pythConnectionConfig` instead and let this function
64
+ * keep building the connection.
61
65
  *
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
+ * Any `PriceFeedSource` `createHermesPriceFeedSource` builds the default
67
+ * one, and an injected source is also how you add
68
+ * `getLatestPriceFeedsPartial`. Must satisfy `PriceFeedSource`, not the
69
+ * `PartialPriceFeedSource` shape alone: with `tolerateMissingPriceFeeds` off
70
+ * this connection goes to `refreshReservePrice`, which needs
66
71
  * `getLatestPriceFeeds`. The lower-level `refreshReservePriceTolerant` does
67
- * accept the structural shape, since it only ever needs the partial fetch.
72
+ * accept the partial-only shape, since it never needs the other method.
68
73
  */
69
- pythConnection?: SuiPriceServiceConnection;
74
+ pythConnection?: simulate.PriceFeedSource;
75
+ /**
76
+ * Config for the connection this function builds — the seam for an access
77
+ * token, which public Hermes is expected to require.
78
+ *
79
+ * The narrow alternative to `pythConnection`, and the one to reach for
80
+ * first: authenticating or re-timing the default connection should not cost
81
+ * a caller a hand-rolled `PriceFeedSource`, and hand-rolling one is how the
82
+ * feed-id normalisation and the requested-minus-returned `missing` contract
83
+ * get reimplemented slightly wrong.
84
+ *
85
+ * IGNORED when `pythConnection` is set, on the same grounds as
86
+ * `fallbackPythEndpoint`: it configures a connection this function no longer
87
+ * builds.
88
+ */
89
+ pythConnectionConfig?: HermesClientConfig;
70
90
  /**
71
91
  * Let a reserve whose feed no endpoint can serve keep the price already in
72
92
  * its on-chain state, instead of failing the whole market. Off by default.
@@ -77,10 +97,18 @@ export declare const initializeSuilend: (suiGrpcClient: SuiGrpcClient, suilendCl
77
97
  * reserves is better off pricing the 41 it can than going blind over the 4
78
98
  * it cannot.
79
99
  *
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.
100
+ * USABLE ON ITS OWN as of the move to hermes-client. The connection this
101
+ * function builds now reports per-feed gaps Hermes' `ignoreInvalidPriceIds`
102
+ * returns the feeds it can and omits the rest — so this flag no longer needs
103
+ * a `pythConnection` alongside it. Under pyth-sui-js v2 it did: the stock
104
+ * `SuiPriceServiceConnection` was all-or-nothing and the flag threw unless
105
+ * you injected a subclass.
106
+ *
107
+ * Still REQUIRES gap reporting from whatever connection is in use, so an
108
+ * INJECTED `pythConnection` must implement `getLatestPriceFeedsPartial` —
109
+ * throws otherwise, rather than degrading to the identical hard failure this
110
+ * flag exists to avoid. `createHermesPriceFeedSource` gives you one; the
111
+ * check is on capability, not on origin.
84
112
  *
85
113
  * Callers that enable this MUST read `unpricedCoinTypes` and decide per
86
114
  * reserve what a kept price is allowed to mean — nothing here makes that
package/lib/initialize.js CHANGED
@@ -94,20 +94,22 @@ export const NORMALIZED_TREATS_COINTYPE = normalizeStructTag(TREATS_COINTYPE);
94
94
  export const refreshReservesForInitialize = async (reserves, pythConnection, tolerateMissingPriceFeeds) => {
95
95
  if (tolerateMissingPriceFeeds) {
96
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
97
+ // feeds it could not serve. A plain all-or-nothing source cannot the
98
+ // method is not part of `PriceFeedSource` so the tolerant path would fall
99
99
  // back to the all-or-nothing fetch, and Hermes v2 404s the whole batch over
100
100
  // one unknown id — the caller would get the same hard failure as before the
101
101
  // flag existed, having explicitly asked not to.
102
102
  //
103
103
  // Rejected here rather than left to degrade quietly, because the pairing is
104
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
105
+ // capability only some connections have. Checked on CAPABILITY, not on
106
+ // whether a connection was injected — the source this SDK builds now DOES
107
+ // report gaps (Hermes' ignoreInvalidPriceIds), so origin says nothing, while
108
+ // an injected all-or-nothing client is still useless no matter who built it.
109
+ // Direct callers of refreshReservePriceTolerant keep the strict
108
110
  // fallback, which is correct for an endpoint that omits unknown ids.
109
111
  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.`);
112
+ 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 a client prototype does NOT satisfy this: it can reroute getLatestPriceFeeds but cannot add a per-feed gap report.`);
111
113
  }
112
114
  return simulate.refreshReservePriceTolerant(reserves, pythConnection);
113
115
  }
@@ -171,7 +173,7 @@ export const initializeSuilend = async (suiGrpcClient, suilendClient, lendingMar
171
173
  reservesWithoutTemporaryPythPriceFeeds.push(reserve);
172
174
  }
173
175
  }
174
- const pythConnection = await resolvePythConnection(options?.pythConnection, fallbackPythEndpoint);
176
+ const pythConnection = await resolvePythConnection(options?.pythConnection, fallbackPythEndpoint, options?.pythConnectionConfig);
175
177
  const [refreshedWithoutTemporary, fabricatedPriceCoinTypes] = await Promise.all([
176
178
  refreshReservesForInitialize(reservesWithoutTemporaryPythPriceFeeds, pythConnection, options?.tolerateMissingPriceFeeds),
177
179
  priceTemporaryPythFeedReserves(reservesWithTemporaryPythPriceFeeds),
package/lib/pyth.d.ts CHANGED
@@ -1,4 +1,12 @@
1
- import { SuiPriceServiceConnection } from "@pythnetwork/pyth-sui-js";
1
+ import { HermesClient, type HermesClientConfig } from "@pythnetwork/hermes-client";
2
+ import type { PartialPriceFeedSource, PriceFeedSource } from "../utils/simulate";
3
+ /**
4
+ * Re-exported so a caller can configure the connection this module builds
5
+ * without taking its own dependency on `@pythnetwork/hermes-client` — the point
6
+ * of `PriceFeedSource` is that consumers need not know which client is behind
7
+ * it.
8
+ */
9
+ export type { HermesClientConfig };
2
10
  export declare const PRIMARY_PYTH_ENDPOINT = "https://hermes.pyth.network";
3
11
  /**
4
12
  * Tests if the primary Pyth connection endpoint is working by checking the /live endpoint
@@ -12,17 +20,80 @@ export declare const testPrimaryPythConnection: () => Promise<boolean>;
12
20
  */
13
21
  export declare const getWorkingPythEndpoint: (fallbackPythEndpoint?: string) => Promise<string>;
14
22
  /**
15
- * Pick the price connection `initializeSuilend` will use.
23
+ * The only part of `HermesClient` this module reads prices through.
24
+ *
25
+ * Structural for the same reason `PriceFeedSource` is: a real `HermesClient`
26
+ * satisfies it, and so does a stub — which is what makes the wire-to-`FeedPrice`
27
+ * mapping below testable without standing up an endpoint.
28
+ */
29
+ export type HermesPriceReader = Pick<HermesClient, "getLatestPriceUpdates">;
30
+ /**
31
+ * A `PriceFeedSource` (and `PartialPriceFeedSource`) backed by Hermes.
16
32
  *
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).
33
+ * `getLatestPriceFeedsPartial` is served by Hermes' own
34
+ * `ignoreInvalidPriceIds`, which returns the feeds it can and omits the rest
35
+ * so gaps are reported by the endpoint rather than inferred from a thrown
36
+ * batch. `missing` is the requested-minus-returned difference, which is the
37
+ * contract `refreshReservePriceTolerant` relies on to populate
38
+ * `unpricedCoinTypes`.
22
39
  *
23
- * Skipping the probe for an injected connection is deliberate, not an
40
+ * `parsed: true` is required on both calls: without it Hermes returns only the
41
+ * binary blob and there is no price to read.
42
+ *
43
+ * `encoding: "base64"` is the cheap half of a payload regression. v2 read prices
44
+ * from `/api/latest_price_feeds`, which omitted the update blob entirely; this
45
+ * endpoint always returns `binary` and offers no way to decline it, so every
46
+ * price read now downloads a VAA nothing here looks at. base64 is ~1/3 smaller
47
+ * than the default hex for the same bytes, on a request that runs per lending
48
+ * market on every refresh.
49
+ */
50
+ export declare const createHermesPriceFeedSource: (hermesClient: HermesPriceReader) => PriceFeedSource & PartialPriceFeedSource;
51
+ /**
52
+ * The deadline the connection this module builds gets, set explicitly because
53
+ * hermes-client defaults to 5s.
54
+ *
55
+ * One batch here is every reserve's feed for a lending market, and
56
+ * `/v2/updates/price/latest` returns a binary blob alongside the parsed prices
57
+ * whether or not the caller wants one — 5s is not a deadline that request
58
+ * reliably meets. 30s is what this module guaranteed before the client changed.
59
+ */
60
+ export declare const DEFAULT_HERMES_TIMEOUT_MS: number;
61
+ /**
62
+ * The config the SDK's own Hermes connection is built with.
63
+ *
64
+ * Split out from `resolvePythConnection` so the DEFAULT is assertable without a
65
+ * 30-second test: hermes-client keeps `timeout` private, so once a client is
66
+ * constructed there is nothing left to observe, and a test written against the
67
+ * built connection can only prove that a config the CALLER passed came through
68
+ * — never that the default did.
69
+ *
70
+ * Exported for the same reason it is useful internally: a caller assembling its
71
+ * own client (failover, throttling) can start from the SDK's defaults instead of
72
+ * silently inheriting hermes-client's.
73
+ */
74
+ export declare const hermesConnectionConfig: (hermesConfig?: HermesClientConfig) => HermesClientConfig;
75
+ /**
76
+ * Pick the price source `initializeSuilend` will use.
77
+ *
78
+ * Lives here rather than in initialize.ts because it belongs beside
79
+ * getWorkingPythEndpoint, the thing it decides whether to call.
80
+ *
81
+ * Skipping the probe for an injected source is deliberate, not an
24
82
  * optimisation. getWorkingPythEndpoint fetches hermes.pyth.network/live, which
25
83
  * answers for whether Hermes is up — not for whether it still serves the
26
84
  * caller's feeds, and not for whether the caller is even pointed at Hermes.
85
+ *
86
+ * `hermesConfig` exists so the commonest reason to reach for an injected source
87
+ * — an access token, since public Hermes is expected to require one — does not
88
+ * cost the caller a hand-rolled `PriceFeedSource`. It configures only the
89
+ * connection built here; an injected source is returned untouched, config or
90
+ * not, because there is nothing left to build.
91
+ *
92
+ * Declared as `PriceFeedSource` because that is all an OVERRIDE is known to be.
93
+ * The source built here is also a `PartialPriceFeedSource`, which is a behaviour
94
+ * change worth stating: `tolerateMissingPriceFeeds` used to throw on the default
95
+ * connection, because a stock `SuiPriceServiceConnection` could not report which
96
+ * feeds it failed to serve. Hermes' `ignoreInvalidPriceIds` can, so the flag now
97
+ * works without an injected connection.
27
98
  */
28
- export declare const resolvePythConnection: (pythConnectionOverride?: SuiPriceServiceConnection, fallbackPythEndpoint?: string) => Promise<SuiPriceServiceConnection>;
99
+ export declare const resolvePythConnection: (pythConnectionOverride?: PriceFeedSource, fallbackPythEndpoint?: string, hermesConfig?: HermesClientConfig) => Promise<PriceFeedSource>;
package/lib/pyth.js CHANGED
@@ -1,20 +1,34 @@
1
- import { SuiPriceServiceConnection } from "@pythnetwork/pyth-sui-js";
1
+ import { HermesClient, } from "@pythnetwork/hermes-client";
2
+ import { normalizeFeedId } from "../utils/feedId.js";
2
3
  export const PRIMARY_PYTH_ENDPOINT = "https://hermes.pyth.network";
4
+ // Concurrent callers (e.g. one initializeSuilend per lending market) share a
5
+ // single in-flight /live probe. The entry is dropped once it settles, so
6
+ // sequential callers still probe fresh and failover behavior is unchanged.
7
+ let _testPrimaryPythConnectionPromise = null;
3
8
  /**
4
9
  * Tests if the primary Pyth connection endpoint is working by checking the /live endpoint
5
10
  * @returns true if the connection is working, false otherwise
6
11
  */
7
- export const testPrimaryPythConnection = async () => {
8
- try {
9
- const res = await fetch(`${PRIMARY_PYTH_ENDPOINT}/live`, {
10
- signal: AbortSignal.timeout(5 * 1000), // 5 second timeout
11
- });
12
- return res.ok;
13
- }
14
- catch (err) {
15
- console.warn(`[testPrimaryPythConnection] Pyth connection test failed for ${PRIMARY_PYTH_ENDPOINT}:`, err);
16
- return false;
12
+ export const testPrimaryPythConnection = () => {
13
+ if (_testPrimaryPythConnectionPromise) {
14
+ return _testPrimaryPythConnectionPromise;
17
15
  }
16
+ const promise = (async () => {
17
+ try {
18
+ const res = await fetch(`${PRIMARY_PYTH_ENDPOINT}/live`, {
19
+ signal: AbortSignal.timeout(5 * 1000), // 5 second timeout
20
+ });
21
+ return res.ok;
22
+ }
23
+ catch (err) {
24
+ console.warn(`[testPrimaryPythConnection] Pyth connection test failed for ${PRIMARY_PYTH_ENDPOINT}:`, err);
25
+ return false;
26
+ }
27
+ })().finally(() => {
28
+ _testPrimaryPythConnectionPromise = null;
29
+ });
30
+ _testPrimaryPythConnectionPromise = promise;
31
+ return promise;
18
32
  };
19
33
  /**
20
34
  * Gets a working Pyth connection endpoint, trying primary first, then fallback if provided
@@ -39,25 +53,119 @@ export const getWorkingPythEndpoint = async (fallbackPythEndpoint) => {
39
53
  }
40
54
  };
41
55
  /**
42
- * Pick the price connection `initializeSuilend` will use.
56
+ * Hermes' wire price -> a number.
57
+ *
58
+ * `price` is an integer STRING and `expo` a negative exponent, so the value is
59
+ * `price * 10 ** expo`. This is what v2's `getPriceAsNumberUnchecked()` did
60
+ * internally; doing it here keeps the conversion in one place now that the
61
+ * client no longer offers it.
62
+ */
63
+ const toPriceNumber = (price) => Number(price.price) * 10 ** price.expo;
64
+ /**
65
+ * A `PriceFeedSource` (and `PartialPriceFeedSource`) backed by Hermes.
43
66
  *
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).
67
+ * `getLatestPriceFeedsPartial` is served by Hermes' own
68
+ * `ignoreInvalidPriceIds`, which returns the feeds it can and omits the rest
69
+ * so gaps are reported by the endpoint rather than inferred from a thrown
70
+ * batch. `missing` is the requested-minus-returned difference, which is the
71
+ * contract `refreshReservePriceTolerant` relies on to populate
72
+ * `unpricedCoinTypes`.
73
+ *
74
+ * `parsed: true` is required on both calls: without it Hermes returns only the
75
+ * binary blob and there is no price to read.
76
+ *
77
+ * `encoding: "base64"` is the cheap half of a payload regression. v2 read prices
78
+ * from `/api/latest_price_feeds`, which omitted the update blob entirely; this
79
+ * endpoint always returns `binary` and offers no way to decline it, so every
80
+ * price read now downloads a VAA nothing here looks at. base64 is ~1/3 smaller
81
+ * than the default hex for the same bytes, on a request that runs per lending
82
+ * market on every refresh.
83
+ */
84
+ export const createHermesPriceFeedSource = (hermesClient) => {
85
+ const fetchFeeds = async (ids, ignoreInvalidPriceIds) => {
86
+ const update = await hermesClient.getLatestPriceUpdates(ids, {
87
+ parsed: true,
88
+ encoding: "base64",
89
+ ignoreInvalidPriceIds,
90
+ });
91
+ return (update.parsed ?? []).map((parsed) => ({
92
+ id: parsed.id,
93
+ price: toPriceNumber(parsed.price),
94
+ emaPrice: toPriceNumber(parsed.ema_price),
95
+ publishTimeS: parsed.price.publish_time,
96
+ }));
97
+ };
98
+ return {
99
+ // All-or-nothing, matching the previous client: an unknown id fails the
100
+ // whole batch rather than being silently dropped, so a caller that did not
101
+ // ask for tolerance cannot end up pricing off a partial response.
102
+ getLatestPriceFeeds: (ids) => fetchFeeds(ids, false),
103
+ getLatestPriceFeedsPartial: async (ids) => {
104
+ const feeds = await fetchFeeds(ids, true);
105
+ const served = new Set(feeds.map((feed) => normalizeFeedId(feed.id)));
106
+ const missing = ids.filter((id) => !served.has(normalizeFeedId(id)));
107
+ return { feeds, missing };
108
+ },
109
+ };
110
+ };
111
+ /**
112
+ * The deadline the connection this module builds gets, set explicitly because
113
+ * hermes-client defaults to 5s.
49
114
  *
50
- * Skipping the probe for an injected connection is deliberate, not an
115
+ * One batch here is every reserve's feed for a lending market, and
116
+ * `/v2/updates/price/latest` returns a binary blob alongside the parsed prices
117
+ * whether or not the caller wants one — 5s is not a deadline that request
118
+ * reliably meets. 30s is what this module guaranteed before the client changed.
119
+ */
120
+ export const DEFAULT_HERMES_TIMEOUT_MS = 30 * 1000;
121
+ /**
122
+ * The config the SDK's own Hermes connection is built with.
123
+ *
124
+ * Split out from `resolvePythConnection` so the DEFAULT is assertable without a
125
+ * 30-second test: hermes-client keeps `timeout` private, so once a client is
126
+ * constructed there is nothing left to observe, and a test written against the
127
+ * built connection can only prove that a config the CALLER passed came through
128
+ * — never that the default did.
129
+ *
130
+ * Exported for the same reason it is useful internally: a caller assembling its
131
+ * own client (failover, throttling) can start from the SDK's defaults instead of
132
+ * silently inheriting hermes-client's.
133
+ */
134
+ export const hermesConnectionConfig = (hermesConfig) => ({
135
+ ...hermesConfig,
136
+ // `??`, not a spread default: `{ timeout: someOptionalVar }` is a normal thing
137
+ // for a caller to pass, and letting an explicit `undefined` through would
138
+ // quietly hand back hermes-client's 5s.
139
+ timeout: hermesConfig?.timeout ?? DEFAULT_HERMES_TIMEOUT_MS,
140
+ });
141
+ /**
142
+ * Pick the price source `initializeSuilend` will use.
143
+ *
144
+ * Lives here rather than in initialize.ts because it belongs beside
145
+ * getWorkingPythEndpoint, the thing it decides whether to call.
146
+ *
147
+ * Skipping the probe for an injected source is deliberate, not an
51
148
  * optimisation. getWorkingPythEndpoint fetches hermes.pyth.network/live, which
52
149
  * answers for whether Hermes is up — not for whether it still serves the
53
150
  * caller's feeds, and not for whether the caller is even pointed at Hermes.
151
+ *
152
+ * `hermesConfig` exists so the commonest reason to reach for an injected source
153
+ * — an access token, since public Hermes is expected to require one — does not
154
+ * cost the caller a hand-rolled `PriceFeedSource`. It configures only the
155
+ * connection built here; an injected source is returned untouched, config or
156
+ * not, because there is nothing left to build.
157
+ *
158
+ * Declared as `PriceFeedSource` because that is all an OVERRIDE is known to be.
159
+ * The source built here is also a `PartialPriceFeedSource`, which is a behaviour
160
+ * change worth stating: `tolerateMissingPriceFeeds` used to throw on the default
161
+ * connection, because a stock `SuiPriceServiceConnection` could not report which
162
+ * feeds it failed to serve. Hermes' `ignoreInvalidPriceIds` can, so the flag now
163
+ * works without an injected connection.
54
164
  */
55
- export const resolvePythConnection = async (pythConnectionOverride, fallbackPythEndpoint) => {
165
+ export const resolvePythConnection = async (pythConnectionOverride, fallbackPythEndpoint, hermesConfig) => {
56
166
  if (pythConnectionOverride)
57
167
  return pythConnectionOverride;
58
168
  // Get a working Pyth endpoint (try primary, fallback to fallbackPythEndpoint if provided)
59
169
  const pythEndpoint = await getWorkingPythEndpoint(fallbackPythEndpoint);
60
- return new SuiPriceServiceConnection(pythEndpoint, {
61
- timeout: 30 * 1000,
62
- });
170
+ return createHermesPriceFeedSource(new HermesClient(pythEndpoint, hermesConnectionConfig(hermesConfig)));
63
171
  };
package/package.json CHANGED
@@ -1 +1 @@
1
- {"name":"@suilend/sdk","version":"5.1.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
+ {"name":"@suilend/sdk","version":"6.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/feedId":"./utils/feedId.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/hermes-client":"3.1.0","@pythnetwork/pyth-sui-js":"4.0.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"}
package/strategies.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { AggregatorClient as CetusSdk } from "@cetusprotocol/aggregator-sdk";
1
+ import type { AggregatorClient as CetusSdk } from "@cetusprotocol/aggregator-sdk";
2
2
  import { SuiClientTypes } from "@mysten/sui/client";
3
3
  import { SuiGrpcClient } from "@mysten/sui/grpc";
4
4
  import { Transaction, TransactionArgument, TransactionObjectInput } from "@mysten/sui/transactions";
package/swap/quote.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { QuoteResponse as Bluefin7kQuote } from "@bluefin-exchange/bluefin7k-aggregator-sdk";
2
- import { RouterDataV3 as CetusQuote, AggregatorClient as CetusSdk } from "@cetusprotocol/aggregator-sdk";
3
- import { AggregatorQuoter as FlowXAggregatorQuoter, GetRoutesResult as FlowXGetRoutesResult } from "@flowx-finance/sdk";
1
+ import type { QuoteResponse as Bluefin7kQuote } from "@bluefin-exchange/bluefin7k-aggregator-sdk";
2
+ import type { RouterDataV3 as CetusQuote, AggregatorClient as CetusSdk } from "@cetusprotocol/aggregator-sdk";
3
+ import type { AggregatorQuoter as FlowXAggregatorQuoter, GetRoutesResult as FlowXGetRoutesResult } from "@flowx-finance/sdk";
4
4
  import BigNumber from "bignumber.js";
5
5
  import { Token } from "@suilend/sui-core";
6
6
  export declare enum QuoteProvider {
package/swap/quote.js CHANGED
@@ -1,4 +1,3 @@
1
- import { getQuote as getBluefin7kQuoteOriginal, } from "@bluefin-exchange/bluefin7k-aggregator-sdk";
2
1
  import { normalizeStructTag } from "@mysten/sui/utils";
3
2
  import BigNumber from "bignumber.js";
4
3
  import BN from "bn.js";
@@ -110,6 +109,25 @@ const getCetusQuote = async (sdk, tokenIn, tokenOut, amountIn) => {
110
109
  return standardizedQuote;
111
110
  };
112
111
  const getBluefin7kQuote = async (tokenIn, tokenOut, amountIn) => {
112
+ // Imported here rather than at module scope so 7k is loaded only when a 7k
113
+ // quote is actually requested, keeping a browser-only router out of every
114
+ // consumer's module-init graph.
115
+ //
116
+ // 7k is NOT a CJS package — 7.3.0 is `type: module` and ships no CJS build at
117
+ // all — so this is not the ERR_REQUIRE_ESM_RACE_CONDITION fix that the flowx
118
+ // deferral in transaction.ts is. Spelled out because the reverse mistake is the
119
+ // easy one: noticing 7k is ESM and concluding this can go back to a static
120
+ // import.
121
+ //
122
+ // What it does still protect is a CONSUMER's graph. 7k peer-depends on
123
+ // `@pythnetwork/pyth-sui-js ^2.1.0`, and pyth v2 is CJS and `require()`s
124
+ // ESM-only `@mysten/sui`. `ts/sdks` pins pyth to 4.0.0 so that peer never
125
+ // resolves to v2 here — but that resolution does not travel with the published
126
+ // package, so an installer without the same override gets CJS pyth v2 under 7k.
127
+ // Deferred, it is never in their init graph either way. (The peer range is
128
+ // stale regardless: 7k 7.3.0 calls pyth v4's `SuiPythClient` API and would not
129
+ // work against v2.)
130
+ const { getQuote: getBluefin7kQuoteOriginal } = await import("@bluefin-exchange/bluefin7k-aggregator-sdk");
113
131
  const quote = await getBluefin7kQuoteOriginal({
114
132
  tokenIn: tokenIn.coinType,
115
133
  tokenOut: tokenOut.coinType,
@@ -1,5 +1,5 @@
1
- import { AggregatorClient as CetusSdk } from "@cetusprotocol/aggregator-sdk";
2
- import { AggregatorQuoter as FlowXAggregatorQuoter } from "@flowx-finance/sdk";
1
+ import type { AggregatorClient as CetusSdk } from "@cetusprotocol/aggregator-sdk";
2
+ import type { AggregatorQuoter as FlowXAggregatorQuoter } from "@flowx-finance/sdk";
3
3
  import { SuiGrpcClient } from "@mysten/sui/grpc";
4
4
  import { Transaction, TransactionObjectArgument } from "@mysten/sui/transactions";
5
5
  import { QuoteProvider, StandardizedQuote } from "./quote";
@@ -1,5 +1,3 @@
1
- import { BluefinXTx, buildTx as buildBluefin7kTransaction, } from "@bluefin-exchange/bluefin7k-aggregator-sdk";
2
- import { Coin as FlowXCoin, Commission as FlowXCommission, CommissionType as FlowXCommissionType, TradeBuilder as FlowXTradeBuilder, } from "@flowx-finance/sdk";
3
1
  import { getSpendableCoin } from "@suilend/sui-core";
4
2
  import { QuoteProvider } from "./quote.js";
5
3
  const getSwapTransactionWrapper = async (provider, getSwapTransaction) => {
@@ -35,6 +33,12 @@ export const getSwapTransaction = async (suiGrpcClient, address, quote, slippage
35
33
  }
36
34
  else if (quote.provider === QuoteProvider.BLUEFIN7K) {
37
35
  return getSwapTransactionWrapper(QuoteProvider.BLUEFIN7K, async () => {
36
+ // Deferred for the reason spelled out at the 7k import in quote.ts: 7k is
37
+ // itself ESM, so this is about init cost plus a consumer's exposure to the
38
+ // CJS pyth v2 that 7k's stale peer range still permits — not the
39
+ // require(ESM) fix that the flowx branch below is. `BluefinXTx` comes from
40
+ // the same import because the `instanceof` below needs the real class.
41
+ const { BluefinXTx, buildTx: buildBluefin7kTransaction } = await import("@bluefin-exchange/bluefin7k-aggregator-sdk");
38
42
  const { tx: transaction2, coinOut } = await buildBluefin7kTransaction({
39
43
  quoteResponse: quote.quote,
40
44
  accountAddress: address,
@@ -62,9 +66,25 @@ export const getSwapTransaction = async (suiGrpcClient, address, quote, slippage
62
66
  }
63
67
  else if (quote.provider === QuoteProvider.FLOWX) {
64
68
  return getSwapTransactionWrapper(QuoteProvider.FLOWX, async () => {
65
- if (!coinIn) {
66
- coinIn = await getSpendableCoin(suiGrpcClient, address, quote.in.coinType, quote.quote.amountIn.toString(), transaction);
67
- }
69
+ // flowx is the genuine CJS offender of the three routers: 2.1.0 publishes
70
+ // no `exports` map, so Node ESM resolves its `main` — `index.cjs.js`
71
+ // which `require()`s ESM-only `@mysten/sui/{bcs,grpc,transactions,utils}`.
72
+ // At module scope that lands in every consumer's init graph and Node
73
+ // rejects it mid-evaluation (ERR_REQUIRE_ESM_RACE_CONDITION).
74
+ //
75
+ // Loaded alongside the coin fetch rather than after it, so pulling in a
76
+ // large CJS bundle overlaps a network round-trip instead of following it.
77
+ // Via Promise.all rather than a bare `const p = import(...)` above the
78
+ // fetch: a floating promise becomes an unhandled rejection if the coin
79
+ // fetch throws first, and module evaluation is exactly the thing here that
80
+ // can throw.
81
+ const [flowx, spendableCoinIn] = await Promise.all([
82
+ import("@flowx-finance/sdk"),
83
+ coinIn ??
84
+ getSpendableCoin(suiGrpcClient, address, quote.in.coinType, quote.quote.amountIn.toString(), transaction),
85
+ ]);
86
+ coinIn = spendableCoinIn;
87
+ const { Coin: FlowXCoin, Commission: FlowXCommission, CommissionType: FlowXCommissionType, TradeBuilder: FlowXTradeBuilder, } = flowx;
68
88
  const trade = new FlowXTradeBuilder("mainnet", quote.quote.routes)
69
89
  .slippage((slippagePercent / 100) * 1e6)
70
90
  .commission(new FlowXCommission(partnerIdMap[QuoteProvider.FLOWX], new FlowXCoin(quote.out.coinType), FlowXCommissionType.PERCENTAGE, 0, false))
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Feed ids compared in one encoding.
3
+ *
4
+ * `toHex` emits bare lowercase hex while Hermes' `parsed[].id` has been seen
5
+ * both ways; comparing raw would miss every feed over a `0x` prefix and price
6
+ * nothing, which is a silent failure rather than a loud one.
7
+ *
8
+ * Its own module, importing nothing, because the two places that must agree on
9
+ * this — the reserve-pricing code in `simulate.ts` and the Hermes source in
10
+ * `lib/pyth.ts` — cannot share a definition otherwise. `lib/pyth.ts` is reached
11
+ * from `initializeSuilend` and is deliberately near-leaf, so importing
12
+ * `simulate.ts` for one four-line function would drag `@mysten/sui/bcs`, the
13
+ * generated structs and the `utils` barrel in behind it. The previous answer was
14
+ * a second copy in `lib/pyth.ts`; two definitions of the encoding that decides
15
+ * whether a feed matches is exactly the thing that goes wrong quietly.
16
+ */
17
+ export declare const normalizeFeedId: (id: string) => string;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Feed ids compared in one encoding.
3
+ *
4
+ * `toHex` emits bare lowercase hex while Hermes' `parsed[].id` has been seen
5
+ * both ways; comparing raw would miss every feed over a `0x` prefix and price
6
+ * nothing, which is a silent failure rather than a loud one.
7
+ *
8
+ * Its own module, importing nothing, because the two places that must agree on
9
+ * this — the reserve-pricing code in `simulate.ts` and the Hermes source in
10
+ * `lib/pyth.ts` — cannot share a definition otherwise. `lib/pyth.ts` is reached
11
+ * from `initializeSuilend` and is deliberately near-leaf, so importing
12
+ * `simulate.ts` for one four-line function would drag `@mysten/sui/bcs`, the
13
+ * generated structs and the `utils` barrel in behind it. The previous answer was
14
+ * a second copy in `lib/pyth.ts`; two definitions of the encoding that decides
15
+ * whether a feed matches is exactly the thing that goes wrong quietly.
16
+ */
17
+ export const normalizeFeedId = (id) => id.replace(/^0x/i, "").toLowerCase();
@@ -1,9 +1,9 @@
1
- import { PriceFeed, SuiPriceServiceConnection } from "@pythnetwork/pyth-sui-js";
2
1
  import BigNumber from "bignumber.js";
3
2
  import { Decimal } from "../_generated/suilend/decimal/structs";
4
3
  import { PoolRewardManager, UserRewardManager } from "../_generated/suilend/liquidity-mining/structs";
5
4
  import { Borrow, Obligation } from "../_generated/suilend/obligation/structs";
6
5
  import { Reserve } from "../_generated/suilend/reserve/structs";
6
+ import { normalizeFeedId } from "./feedId";
7
7
  /**
8
8
  * @deprecated since version 1.0.8. Use `calculateUtilizationPercent` instead.
9
9
  */
@@ -21,7 +21,30 @@ export declare const calculateSupplyApr: (reserve: Reserve<string>) => BigNumber
21
21
  export declare const calculateDepositAprPercent: (reserve: Reserve<string>) => BigNumber;
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
- export declare const refreshReservePrice: (reserves: Reserve<string>[], pythConnection: SuiPriceServiceConnection) => Promise<Reserve<string>[]>;
24
+ /**
25
+ * A price read from Pyth, normalised away from any one client's response shape.
26
+ *
27
+ * Hermes returns `{ price: "12345", expo: -8, publish_time }`; v2's `PriceFeed`
28
+ * exposed `getPriceAsNumberUnchecked()` instead. Normalising at the boundary
29
+ * keeps `price * 10 ** expo` in exactly one place, and keeps the reserve-pricing
30
+ * code below independent of which client fetched it.
31
+ */
32
+ export interface FeedPrice {
33
+ id: string;
34
+ price: number;
35
+ emaPrice: number;
36
+ publishTimeS: number;
37
+ }
38
+ /**
39
+ * A source of latest prices. Structural on purpose: any object with this method
40
+ * qualifies, so a caller can inject its own client (authenticated, failover,
41
+ * throttled) without this package depending on it.
42
+ */
43
+ export interface PriceFeedSource {
44
+ getLatestPriceFeeds(ids: string[]): Promise<FeedPrice[] | undefined>;
45
+ }
46
+ export { normalizeFeedId };
47
+ export declare const refreshReservePrice: (reserves: Reserve<string>[], pythConnection: PriceFeedSource) => Promise<Reserve<string>[]>;
25
48
  /**
26
49
  * A connection that can report which of the requested feeds it could not serve,
27
50
  * instead of failing the whole batch. Structural on purpose: any object with
@@ -30,7 +53,7 @@ export declare const refreshReservePrice: (reserves: Reserve<string>[], pythConn
30
53
  */
31
54
  export interface PartialPriceFeedSource {
32
55
  getLatestPriceFeedsPartial(ids: string[]): Promise<{
33
- feeds: PriceFeed[];
56
+ feeds: FeedPrice[];
34
57
  missing: string[];
35
58
  }>;
36
59
  }
@@ -41,7 +64,7 @@ export interface PartialPriceFeedSource {
41
64
  * internal branch: `tolerateMissingPriceFeeds` is meaningless without it (see
42
65
  * `refreshReservesForInitialize`, which refuses the combination).
43
66
  */
44
- export declare function canReportMissingPriceFeeds(connection: SuiPriceServiceConnection | PartialPriceFeedSource): connection is PartialPriceFeedSource;
67
+ export declare function canReportMissingPriceFeeds(connection: PriceFeedSource | PartialPriceFeedSource): connection is PartialPriceFeedSource;
45
68
  /**
46
69
  * The remedy both gap-tolerance failure paths point at.
47
70
  *
@@ -77,7 +100,7 @@ export declare const PARTIAL_FETCH_REQUIRED_HINT = "inject a connection implemen
77
100
  * at literal zero. `unpricedCoinTypes` is the only thing distinguishing a
78
101
  * real price from a kept one; it is not optional to read.
79
102
  * 2. Tolerance requires a connection that can REPORT a gap
80
- * (`getLatestPriceFeedsPartial`). Handed a stock `SuiPriceServiceConnection`,
103
+ * (`getLatestPriceFeedsPartial`). Handed a plain all-or-nothing source,
81
104
  * this falls back to the all-or-nothing fetch, which Hermes v2 answers with
82
105
  * a 404 for the whole batch when any id is unknown — so there is nothing
83
106
  * left to be tolerant with. That fetch is kept because it is correct for an
@@ -85,7 +108,7 @@ export declare const PARTIAL_FETCH_REQUIRED_HINT = "inject a connection implemen
85
108
  * failure is re-thrown with attribution rather than surfacing as a bare HTTP
86
109
  * error. `refreshReservesForInitialize` rejects the combination outright.
87
110
  */
88
- export declare const refreshReservePriceTolerant: (reserves: Reserve<string>[], pythConnection: SuiPriceServiceConnection | PartialPriceFeedSource) => Promise<{
111
+ export declare const refreshReservePriceTolerant: (reserves: Reserve<string>[], pythConnection: PriceFeedSource | PartialPriceFeedSource) => Promise<{
89
112
  reserves: Reserve<string>[];
90
113
  unpricedCoinTypes: string[];
91
114
  }>;
package/utils/simulate.js CHANGED
@@ -6,6 +6,7 @@ import { Decimal } from "../_generated/suilend/decimal/structs.js";
6
6
  import { UserReward, } from "../_generated/suilend/liquidity-mining/structs.js";
7
7
  import { WAD } from "../lib/constants.js";
8
8
  import { linearlyInterpolate } from "../utils/index.js";
9
+ import { normalizeFeedId } from "./feedId.js";
9
10
  /**
10
11
  * @deprecated since version 1.0.8. Use `calculateUtilizationPercent` instead.
11
12
  */
@@ -118,8 +119,12 @@ export const updatePoolRewardsManager = (manager, nowMs) => {
118
119
  updatedManager.lastUpdateTimeMs = BigInt(nowMs);
119
120
  return updatedManager;
120
121
  };
122
+ // Re-exported, not redefined: it was part of this module's public surface before
123
+ // it moved to ./feedId, and every consumer that normalizes a feed id to compare
124
+ // it against `unpricedCoinTypes` imports it from here.
125
+ export { normalizeFeedId };
121
126
  /** 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));
127
+ const reserveFeedId = (reserve) => normalizeFeedId(toHex(new Uint8Array(reserve.priceIdentifier.bytes)));
123
128
  /**
124
129
  * Index feeds by id for O(1) lookup, keeping the FIRST entry per id.
125
130
  *
@@ -130,9 +135,11 @@ const reserveFeedId = (reserve) => toHex(new Uint8Array(reserve.priceIdentifier.
130
135
  */
131
136
  const indexFeedsById = (priceFeeds) => {
132
137
  const byId = new Map();
133
- for (const feed of priceFeeds)
134
- if (!byId.has(feed.id))
135
- byId.set(feed.id, feed);
138
+ for (const feed of priceFeeds) {
139
+ const id = normalizeFeedId(feed.id);
140
+ if (!byId.has(id))
141
+ byId.set(id, feed);
142
+ }
136
143
  return byId;
137
144
  };
138
145
  /**
@@ -148,9 +155,9 @@ const indexFeedsById = (priceFeeds) => {
148
155
  */
149
156
  const withFeedPrice = (reserve, priceFeed) => {
150
157
  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);
158
+ newReserve.price = stringToDecimal(priceFeed.price.toString());
159
+ newReserve.smoothedPrice = stringToDecimal(priceFeed.emaPrice.toString());
160
+ newReserve.priceLastUpdateTimestampS = BigInt(priceFeed.publishTimeS);
154
161
  return newReserve;
155
162
  };
156
163
  export const refreshReservePrice = async (reserves, pythConnection) => {
@@ -220,7 +227,7 @@ export const PARTIAL_FETCH_REQUIRED_HINT = "inject a connection implementing get
220
227
  * at literal zero. `unpricedCoinTypes` is the only thing distinguishing a
221
228
  * real price from a kept one; it is not optional to read.
222
229
  * 2. Tolerance requires a connection that can REPORT a gap
223
- * (`getLatestPriceFeedsPartial`). Handed a stock `SuiPriceServiceConnection`,
230
+ * (`getLatestPriceFeedsPartial`). Handed a plain all-or-nothing source,
224
231
  * this falls back to the all-or-nothing fetch, which Hermes v2 answers with
225
232
  * a 404 for the whole batch when any id is unknown — so there is nothing
226
233
  * left to be tolerant with. That fetch is kept because it is correct for an