@suilend/sdk 5.1.1 → 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 +1 -0
- package/lib/index.js +7 -0
- package/lib/initialize.d.ts +44 -16
- package/lib/initialize.js +9 -7
- package/lib/pyth.d.ts +80 -9
- package/lib/pyth.js +107 -12
- package/package.json +1 -1
- package/strategies.d.ts +1 -1
- package/swap/quote.d.ts +3 -3
- package/swap/quote.js +19 -1
- package/swap/transaction.d.ts +2 -2
- package/swap/transaction.js +25 -5
- package/utils/feedId.d.ts +17 -0
- package/utils/feedId.js +17 -0
- package/utils/simulate.d.ts +29 -6
- package/utils/simulate.js +15 -8
package/lib/index.d.ts
CHANGED
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";
|
package/lib/initialize.d.ts
CHANGED
|
@@ -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:
|
|
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;
|
|
58
|
-
* constructed with default options gets a 6x tighter
|
|
59
|
-
*
|
|
60
|
-
* an object it did not construct — set it when you build the
|
|
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
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* `
|
|
65
|
-
*
|
|
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
|
|
72
|
+
* accept the partial-only shape, since it never needs the other method.
|
|
68
73
|
*/
|
|
69
|
-
pythConnection?:
|
|
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
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
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
|
|
98
|
-
// method
|
|
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
|
-
//
|
|
106
|
-
// whether a connection was injected —
|
|
107
|
-
//
|
|
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
|
|
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 {
|
|
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
|
-
*
|
|
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
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
-
*
|
|
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?:
|
|
99
|
+
export declare const resolvePythConnection: (pythConnectionOverride?: PriceFeedSource, fallbackPythEndpoint?: string, hermesConfig?: HermesClientConfig) => Promise<PriceFeedSource>;
|
package/lib/pyth.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
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";
|
|
3
4
|
// Concurrent callers (e.g. one initializeSuilend per lending market) share a
|
|
4
5
|
// single in-flight /live probe. The entry is dropped once it settles, so
|
|
@@ -52,25 +53,119 @@ export const getWorkingPythEndpoint = async (fallbackPythEndpoint) => {
|
|
|
52
53
|
}
|
|
53
54
|
};
|
|
54
55
|
/**
|
|
55
|
-
*
|
|
56
|
+
* Hermes' wire price -> a number.
|
|
56
57
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
|
|
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.
|
|
66
|
+
*
|
|
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.
|
|
62
114
|
*
|
|
63
|
-
*
|
|
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
|
|
64
148
|
* optimisation. getWorkingPythEndpoint fetches hermes.pyth.network/live, which
|
|
65
149
|
* answers for whether Hermes is up — not for whether it still serves the
|
|
66
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.
|
|
67
164
|
*/
|
|
68
|
-
export const resolvePythConnection = async (pythConnectionOverride, fallbackPythEndpoint) => {
|
|
165
|
+
export const resolvePythConnection = async (pythConnectionOverride, fallbackPythEndpoint, hermesConfig) => {
|
|
69
166
|
if (pythConnectionOverride)
|
|
70
167
|
return pythConnectionOverride;
|
|
71
168
|
// Get a working Pyth endpoint (try primary, fallback to fallbackPythEndpoint if provided)
|
|
72
169
|
const pythEndpoint = await getWorkingPythEndpoint(fallbackPythEndpoint);
|
|
73
|
-
return new
|
|
74
|
-
timeout: 30 * 1000,
|
|
75
|
-
});
|
|
170
|
+
return createHermesPriceFeedSource(new HermesClient(pythEndpoint, hermesConnectionConfig(hermesConfig)));
|
|
76
171
|
};
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"@suilend/sdk","version":"
|
|
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,
|
package/swap/transaction.d.ts
CHANGED
|
@@ -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";
|
package/swap/transaction.js
CHANGED
|
@@ -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
|
-
|
|
66
|
-
|
|
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;
|
package/utils/feedId.js
ADDED
|
@@ -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();
|
package/utils/simulate.d.ts
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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
|
-
|
|
135
|
-
|
|
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.
|
|
152
|
-
newReserve.smoothedPrice = stringToDecimal(priceFeed.
|
|
153
|
-
newReserve.priceLastUpdateTimestampS = BigInt(priceFeed.
|
|
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
|
|
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
|