@suilend/sdk 4.0.0 → 5.1.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/client.d.ts +21 -0
- package/client.js +149 -28
- package/lib/initialize.d.ts +83 -1
- package/lib/initialize.js +106 -32
- package/lib/pyth.d.ts +16 -0
- package/lib/pyth.js +24 -0
- package/package.json +1 -1
- package/utils/obligation.d.ts +4 -0
- package/utils/obligation.js +1 -1
- package/utils/simulate.d.ts +68 -1
- package/utils/simulate.js +134 -8
package/client.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SuiClientTypes } from "@mysten/sui/client";
|
|
1
2
|
import { SuiGrpcClient } from "@mysten/sui/grpc";
|
|
2
3
|
import { Transaction, TransactionObjectArgument, TransactionObjectInput } from "@mysten/sui/transactions";
|
|
3
4
|
import { SuiPriceServiceConnection, SuiPythClient } from "@pythnetwork/pyth-sui-js";
|
|
@@ -10,6 +11,17 @@ export declare const ADMIN_ADDRESS: string;
|
|
|
10
11
|
export declare const LENDING_MARKET_REGISTRY_ID: string;
|
|
11
12
|
export declare const LENDING_MARKET_ID: string, LENDING_MARKET_TYPE: string;
|
|
12
13
|
export declare const STEAMM_LM_LENDING_MARKET_ID: string, STEAMM_LM_LENDING_MARKET_TYPE: string;
|
|
14
|
+
export declare function getLatestPackageId(suiGrpcClient: SuiGrpcClient, upgradeCapId: string): Promise<any>;
|
|
15
|
+
/**
|
|
16
|
+
* Lists every owned object of `type`, following pagination.
|
|
17
|
+
*
|
|
18
|
+
* `object_type` matches all instantiations when the type params are omitted
|
|
19
|
+
* (`0x2::coin::Coin` returns every `Coin<T>`), so passing a bare struct type
|
|
20
|
+
* lets one request serve callers that each only want their own type args.
|
|
21
|
+
* Filter the result on `obj.type` — comparing via normalizeStructTag, since
|
|
22
|
+
* the node returns fully-normalized addresses.
|
|
23
|
+
*/
|
|
24
|
+
export declare function listAllOwnedObjects<Include extends SuiClientTypes.ObjectInclude>(suiGrpcClient: SuiGrpcClient, owner: string, type: string, include?: Include): Promise<SuiClientTypes.Object<Include>[]>;
|
|
13
25
|
export type ObligationWithUnclaimedRewards = {
|
|
14
26
|
id: string;
|
|
15
27
|
lendingMarketId?: string;
|
|
@@ -34,6 +46,14 @@ export declare class SuilendClient {
|
|
|
34
46
|
pythConnection: SuiPriceServiceConnection;
|
|
35
47
|
constructor(lendingMarket: LendingMarket<string>, suiGrpcClient: SuiGrpcClient);
|
|
36
48
|
static initialize(lendingMarketId: string, lendingMarketType: string, suiGrpcClient: SuiGrpcClient, logPackageId?: boolean): Promise<SuilendClient>;
|
|
49
|
+
/**
|
|
50
|
+
* Same as `initialize`, but fetches every LendingMarket object in one
|
|
51
|
+
* request rather than one per market.
|
|
52
|
+
*/
|
|
53
|
+
static initializeAll(lendingMarkets: {
|
|
54
|
+
id: string;
|
|
55
|
+
type: string;
|
|
56
|
+
}[], suiGrpcClient: SuiGrpcClient, logPackageId?: boolean): Promise<SuilendClient[]>;
|
|
37
57
|
static getFeeReceivers(suiGrpcClient: SuiGrpcClient, lendingMarketId: string): Promise<{
|
|
38
58
|
receivers: string[];
|
|
39
59
|
weights: string[];
|
|
@@ -44,6 +64,7 @@ export declare class SuilendClient {
|
|
|
44
64
|
};
|
|
45
65
|
static getObligationOwnerCaps(ownerId: string, lendingMarketTypeArgs: string[], suiGrpcClient: SuiGrpcClient): Promise<ObligationOwnerCap<string>[]>;
|
|
46
66
|
static getObligation(obligationId: string, lendingMarketTypeArgs: string[], suiGrpcClient: SuiGrpcClient): Promise<Obligation<string>>;
|
|
67
|
+
static getObligations(obligationIds: string[], lendingMarketTypeArgs: string[], suiGrpcClient: SuiGrpcClient): Promise<Obligation<string>[]>;
|
|
47
68
|
getObligation(obligationId: string): Promise<Obligation<string>>;
|
|
48
69
|
static getLendingMarketOwnerCapId(ownerId: string, lendingMarketTypeArgs: string[], suiGrpcClient: SuiGrpcClient): Promise<string | undefined>;
|
|
49
70
|
getLendingMarketOwnerCapId(ownerId: string): Promise<string | undefined>;
|
package/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SUI_CLOCK_OBJECT_ID, SUI_SYSTEM_STATE_OBJECT_ID, normalizeStructTag, toHex, } from "@mysten/sui/utils";
|
|
1
|
+
import { SUI_CLOCK_OBJECT_ID, SUI_SYSTEM_STATE_OBJECT_ID, normalizeStructTag, normalizeSuiObjectId, toHex, } from "@mysten/sui/utils";
|
|
2
2
|
import { SuiPriceServiceConnection, SuiPythClient, } from "@pythnetwork/pyth-sui-js";
|
|
3
3
|
import { extractCTokenCoinType, getAllCoins, getSpendableCoin, } from "@suilend/sui-core";
|
|
4
4
|
import { PriceInfoObject } from "./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/price-info/structs.js";
|
|
@@ -14,6 +14,7 @@ import { createReserveConfig, } from "./_generated/suilend/reserve-config/functi
|
|
|
14
14
|
import { PRIMARY_PYTH_ENDPOINT } from "./lib/pyth.js";
|
|
15
15
|
import { createJsonRpcAdapter } from "./lib/pythAdapter.js";
|
|
16
16
|
import { Side } from "./lib/types.js";
|
|
17
|
+
import { chunkedMultiGet } from "./utils/obligation.js";
|
|
17
18
|
const SUI_COINTYPE = "0x2::sui::SUI";
|
|
18
19
|
const NORMALIZED_SUI_COINTYPE = normalizeStructTag(SUI_COINTYPE);
|
|
19
20
|
const isSui = (coinType) => normalizeStructTag(coinType) === NORMALIZED_SUI_COINTYPE;
|
|
@@ -46,12 +47,86 @@ export const [STEAMM_LM_LENDING_MARKET_ID, STEAMM_LM_LENDING_MARKET_TYPE] = proc
|
|
|
46
47
|
"0xc1888ec1b81a414e427a44829310508352aec38252ee0daa9f8b181b6947de9f",
|
|
47
48
|
"0x0a071f4976abae1a7f722199cf0bfcbe695ef9408a878e7d12a7ca87b7e582a6::lp_rewards::LP_REWARDS",
|
|
48
49
|
];
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
// Keyed by upgradeCapId. Only dedupes calls that are still in flight — e.g.
|
|
51
|
+
// initializing N lending markets at once each calling SuilendClient.initialize()
|
|
52
|
+
// with the same upgrade cap. Deliberately not cached past settlement: this
|
|
53
|
+
// runs on long-running indexer processes, and caching a success forever would
|
|
54
|
+
// mean a real on-chain package upgrade is never picked up without a restart;
|
|
55
|
+
// caching a failure forever would permanently break initialize() for one
|
|
56
|
+
// transient RPC error.
|
|
57
|
+
const latestPackageIdCache = new Map();
|
|
58
|
+
export async function getLatestPackageId(suiGrpcClient, upgradeCapId) {
|
|
59
|
+
const cached = latestPackageIdCache.get(upgradeCapId);
|
|
60
|
+
if (cached)
|
|
61
|
+
return cached;
|
|
62
|
+
const promise = suiGrpcClient
|
|
63
|
+
.getObject({
|
|
51
64
|
objectId: upgradeCapId,
|
|
52
65
|
include: { json: true },
|
|
53
|
-
})
|
|
54
|
-
|
|
66
|
+
})
|
|
67
|
+
.then(({ object }) => object.json.package)
|
|
68
|
+
.finally(() => latestPackageIdCache.delete(upgradeCapId));
|
|
69
|
+
latestPackageIdCache.set(upgradeCapId, promise);
|
|
70
|
+
return promise;
|
|
71
|
+
}
|
|
72
|
+
// Keyed by owner + type + include mask, and scoped to the issuing client: two
|
|
73
|
+
// SuiGrpcClients can point at different endpoints or networks (this file builds
|
|
74
|
+
// beta-vs-mainnet constants off NEXT_PUBLIC_SUILEND_USE_BETA_MARKET, and a
|
|
75
|
+
// frontend swapping RPCs holds both clients briefly), so one client must never
|
|
76
|
+
// be served another's in-flight response.
|
|
77
|
+
//
|
|
78
|
+
// Same in-flight-only contract as latestPackageIdCache above: N lending markets
|
|
79
|
+
// asking for the same owner's caps at once collapse to one request, and the
|
|
80
|
+
// entry is dropped on settle so long-running processes never serve a stale
|
|
81
|
+
// object list.
|
|
82
|
+
const ownedObjectsCache = new WeakMap();
|
|
83
|
+
function ownedObjectsCacheFor(suiGrpcClient) {
|
|
84
|
+
const existing = ownedObjectsCache.get(suiGrpcClient);
|
|
85
|
+
if (existing)
|
|
86
|
+
return existing;
|
|
87
|
+
const created = new Map();
|
|
88
|
+
ownedObjectsCache.set(suiGrpcClient, created);
|
|
89
|
+
return created;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Lists every owned object of `type`, following pagination.
|
|
93
|
+
*
|
|
94
|
+
* `object_type` matches all instantiations when the type params are omitted
|
|
95
|
+
* (`0x2::coin::Coin` returns every `Coin<T>`), so passing a bare struct type
|
|
96
|
+
* lets one request serve callers that each only want their own type args.
|
|
97
|
+
* Filter the result on `obj.type` — comparing via normalizeStructTag, since
|
|
98
|
+
* the node returns fully-normalized addresses.
|
|
99
|
+
*/
|
|
100
|
+
export async function listAllOwnedObjects(suiGrpcClient, owner, type, include) {
|
|
101
|
+
// Encodes each flag's value, not just its presence, so {content: true} and
|
|
102
|
+
// {content: false} cannot collide on one key.
|
|
103
|
+
const key = `${owner}|${type}|${Object.entries(include ?? {})
|
|
104
|
+
.map(([flag, value]) => `${flag}=${value}`)
|
|
105
|
+
.sort()
|
|
106
|
+
.join(",")}`;
|
|
107
|
+
const cache = ownedObjectsCacheFor(suiGrpcClient);
|
|
108
|
+
const cached = cache.get(key);
|
|
109
|
+
if (cached)
|
|
110
|
+
return cached;
|
|
111
|
+
const promise = (async () => {
|
|
112
|
+
const allObjs = [];
|
|
113
|
+
let cursor = null;
|
|
114
|
+
let hasNextPage = true;
|
|
115
|
+
while (hasNextPage) {
|
|
116
|
+
const objs = await suiGrpcClient.listOwnedObjects({
|
|
117
|
+
owner,
|
|
118
|
+
cursor,
|
|
119
|
+
type,
|
|
120
|
+
include,
|
|
121
|
+
});
|
|
122
|
+
allObjs.push(...objs.objects);
|
|
123
|
+
cursor = objs.cursor;
|
|
124
|
+
hasNextPage = objs.hasNextPage;
|
|
125
|
+
}
|
|
126
|
+
return allObjs;
|
|
127
|
+
})().finally(() => cache.delete(key));
|
|
128
|
+
cache.set(key, promise);
|
|
129
|
+
return promise;
|
|
55
130
|
}
|
|
56
131
|
export class SuilendClient {
|
|
57
132
|
lendingMarket;
|
|
@@ -75,6 +150,53 @@ export class SuilendClient {
|
|
|
75
150
|
setPublishedAt(publishedAt);
|
|
76
151
|
return new SuilendClient(lendingMarket, suiGrpcClient);
|
|
77
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* Same as `initialize`, but fetches every LendingMarket object in one
|
|
155
|
+
* request rather than one per market.
|
|
156
|
+
*/
|
|
157
|
+
static async initializeAll(lendingMarkets, suiGrpcClient, logPackageId) {
|
|
158
|
+
// Refreshed even for an empty list, so PUBLISHED_AT parity with
|
|
159
|
+
// initialize() holds for every call — move-call targets are built off that
|
|
160
|
+
// module global regardless of how many clients came back.
|
|
161
|
+
const publishedAt = await getLatestPackageId(suiGrpcClient, SUILEND_UPGRADE_CAP_ID);
|
|
162
|
+
if (logPackageId)
|
|
163
|
+
console.log("@suilend/sdk | publishedAt:", publishedAt);
|
|
164
|
+
setPublishedAt(publishedAt);
|
|
165
|
+
if (lendingMarkets.length === 0)
|
|
166
|
+
return [];
|
|
167
|
+
const { objects } = await suiGrpcClient.getObjects({
|
|
168
|
+
objectIds: lendingMarkets.map((lendingMarket) => lendingMarket.id),
|
|
169
|
+
include: { content: true },
|
|
170
|
+
});
|
|
171
|
+
// Paired by objectId rather than by position: BatchGetObjectsResponse
|
|
172
|
+
// documents no ordering guarantee, and every object here is a
|
|
173
|
+
// LendingMarket, so a type check cannot catch a reordered response — it
|
|
174
|
+
// would silently attach the wrong type arg to a market.
|
|
175
|
+
const objectById = new Map();
|
|
176
|
+
const failures = [];
|
|
177
|
+
for (const object of objects) {
|
|
178
|
+
// The error branch of GetObjectResult carries no objectId, so a failed
|
|
179
|
+
// entry cannot be attributed by id. Rather than fall back to position —
|
|
180
|
+
// which the pairing above deliberately does not trust — failures are
|
|
181
|
+
// identified by which requested ids are missing below.
|
|
182
|
+
if (object instanceof Error)
|
|
183
|
+
failures.push(object);
|
|
184
|
+
else
|
|
185
|
+
objectById.set(normalizeSuiObjectId(object.objectId), object);
|
|
186
|
+
}
|
|
187
|
+
return lendingMarkets.map(({ id, type }) => {
|
|
188
|
+
const object = objectById.get(normalizeSuiObjectId(id));
|
|
189
|
+
if (!object)
|
|
190
|
+
throw new Error(`Failed to fetch lending market ${id}${failures.length > 0 ? `: ${failures[0].message}` : ""}`, failures.length > 0 ? { cause: failures[0] } : undefined);
|
|
191
|
+
// Compares the full parameterized type, not just the struct name: an
|
|
192
|
+
// id/type pair built from two drifted lists would otherwise pass and
|
|
193
|
+
// decode under the wrong phantom type.
|
|
194
|
+
const expectedType = normalizeStructTag(`${PACKAGE_ID}::lending_market::LendingMarket<${type}>`);
|
|
195
|
+
if (normalizeStructTag(object.type) !== expectedType)
|
|
196
|
+
throw new Error(`object at id ${id} is not a LendingMarket<${type}> (got ${object.type})`);
|
|
197
|
+
return new SuilendClient(LendingMarket.fromBcs(phantom(type), object.content), suiGrpcClient);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
78
200
|
static async getFeeReceivers(suiGrpcClient, lendingMarketId) {
|
|
79
201
|
const { dynamicField } = await suiGrpcClient.getDynamicField({
|
|
80
202
|
parentId: lendingMarketId,
|
|
@@ -102,21 +224,11 @@ export class SuilendClient {
|
|
|
102
224
|
return ownerCap;
|
|
103
225
|
}
|
|
104
226
|
static async getObligationOwnerCaps(ownerId, lendingMarketTypeArgs, suiGrpcClient) {
|
|
105
|
-
const allObjs =
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
owner: ownerId,
|
|
111
|
-
cursor,
|
|
112
|
-
type: `${PACKAGE_ID}::lending_market::ObligationOwnerCap<${lendingMarketTypeArgs[0]}>`,
|
|
113
|
-
include: { content: true },
|
|
114
|
-
});
|
|
115
|
-
allObjs.push(...objs.objects);
|
|
116
|
-
cursor = objs.cursor;
|
|
117
|
-
hasNextPage = objs.hasNextPage;
|
|
118
|
-
}
|
|
119
|
-
return allObjs.map((obj) => ObligationOwnerCap.fromBcs(phantom(lendingMarketTypeArgs[0]), obj.content));
|
|
227
|
+
const allObjs = await listAllOwnedObjects(suiGrpcClient, ownerId, `${PACKAGE_ID}::lending_market::ObligationOwnerCap`, { content: true });
|
|
228
|
+
const type = normalizeStructTag(`${PACKAGE_ID}::lending_market::ObligationOwnerCap<${lendingMarketTypeArgs[0]}>`);
|
|
229
|
+
return allObjs
|
|
230
|
+
.filter((obj) => normalizeStructTag(obj.type) === type)
|
|
231
|
+
.map((obj) => ObligationOwnerCap.fromBcs(phantom(lendingMarketTypeArgs[0]), obj.content));
|
|
120
232
|
}
|
|
121
233
|
static async getObligation(obligationId, lendingMarketTypeArgs, suiGrpcClient) {
|
|
122
234
|
const { object } = await suiGrpcClient.getObject({
|
|
@@ -125,18 +237,27 @@ export class SuilendClient {
|
|
|
125
237
|
});
|
|
126
238
|
return Obligation.fromBcs(phantom(lendingMarketTypeArgs[0]), object.content);
|
|
127
239
|
}
|
|
240
|
+
// Batched analog of getObligation — chunkedMultiGet issues one getObjects
|
|
241
|
+
// call per 50 ids, run concurrently, instead of one getObject round-trip
|
|
242
|
+
// per id.
|
|
243
|
+
static async getObligations(obligationIds, lendingMarketTypeArgs, suiGrpcClient) {
|
|
244
|
+
if (obligationIds.length === 0)
|
|
245
|
+
return [];
|
|
246
|
+
const objects = await chunkedMultiGet(suiGrpcClient, obligationIds);
|
|
247
|
+
return objects.map((object) => {
|
|
248
|
+
if (object instanceof Error)
|
|
249
|
+
throw object;
|
|
250
|
+
return Obligation.fromBcs(phantom(lendingMarketTypeArgs[0]), object.content);
|
|
251
|
+
});
|
|
252
|
+
}
|
|
128
253
|
async getObligation(obligationId) {
|
|
129
254
|
return SuilendClient.getObligation(obligationId, this.lendingMarket.$typeArgs, this.suiGrpcClient);
|
|
130
255
|
}
|
|
131
256
|
static async getLendingMarketOwnerCapId(ownerId, lendingMarketTypeArgs, suiGrpcClient) {
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (objs.objects.length > 0)
|
|
137
|
-
return objs.objects[0].objectId;
|
|
138
|
-
else
|
|
139
|
-
return undefined;
|
|
257
|
+
const allObjs = await listAllOwnedObjects(suiGrpcClient, ownerId, `${PACKAGE_ID}::lending_market::LendingMarketOwnerCap`);
|
|
258
|
+
const type = normalizeStructTag(`${PACKAGE_ID}::lending_market::LendingMarketOwnerCap<${lendingMarketTypeArgs[0]}>`);
|
|
259
|
+
return allObjs.find((obj) => normalizeStructTag(obj.type) === type)
|
|
260
|
+
?.objectId;
|
|
140
261
|
}
|
|
141
262
|
async getLendingMarketOwnerCapId(ownerId) {
|
|
142
263
|
return SuilendClient.getLendingMarketOwnerCapId(ownerId, this.lendingMarket.$typeArgs, this.suiGrpcClient);
|
package/lib/initialize.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SuiClientTypes } from "@mysten/sui/client";
|
|
2
2
|
import { SuiGrpcClient } from "@mysten/sui/grpc";
|
|
3
|
+
import { SuiPriceServiceConnection } from "@pythnetwork/pyth-sui-js";
|
|
3
4
|
import BigNumber from "bignumber.js";
|
|
4
5
|
import { Reserve } from "../_generated/suilend/reserve/structs";
|
|
5
6
|
import { SuilendClient } from "../client";
|
|
@@ -9,7 +10,87 @@ export declare const RESERVES_CUSTOM_ORDER: Record<string, string[]>;
|
|
|
9
10
|
export declare const NORMALIZED_MAYA_COINTYPE: string;
|
|
10
11
|
export declare const NORMALIZED_mPOINTS_COINTYPE: string;
|
|
11
12
|
export declare const NORMALIZED_TREATS_COINTYPE: string;
|
|
12
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Price reserves for `initializeSuilend`, honouring the opt-in tolerance flag.
|
|
15
|
+
*
|
|
16
|
+
* Extracted so both branches are directly testable — the difference between
|
|
17
|
+
* them is a throw, which is the whole reason the flag exists and is exactly
|
|
18
|
+
* what a regression here would silently change.
|
|
19
|
+
*/
|
|
20
|
+
export declare const refreshReservesForInitialize: (reserves: Reserve<string>[], pythConnection: SuiPriceServiceConnection, tolerateMissingPriceFeeds?: boolean) => Promise<{
|
|
21
|
+
reserves: Reserve<string>[];
|
|
22
|
+
unpricedCoinTypes: string[];
|
|
23
|
+
}>;
|
|
24
|
+
/**
|
|
25
|
+
* Price the temporary-Pyth-feed reserves from the cached-price service, in
|
|
26
|
+
* place, and report which ones ended up with a FABRICATED price.
|
|
27
|
+
*
|
|
28
|
+
* These reserves never reach the Pyth refresh at all, so nothing else can
|
|
29
|
+
* report them. When the cached lookup fails the price falls back to a
|
|
30
|
+
* hardcoded 0.0001 — a placeholder chosen to be non-zero, not a market price —
|
|
31
|
+
* and previously that was indistinguishable from a real quote: the reserve
|
|
32
|
+
* carried a plausible-looking number and appeared in no gap report.
|
|
33
|
+
*
|
|
34
|
+
* Extracted for the same reason as `refreshReservesForInitialize`:
|
|
35
|
+
* `initializeSuilend` needs a gRPC client and a SuilendClient, so logic left
|
|
36
|
+
* inline in it cannot be tested, and "is this price real" is not something to
|
|
37
|
+
* leave unverified.
|
|
38
|
+
*/
|
|
39
|
+
export declare const priceTemporaryPythFeedReserves: (reserves: Reserve<string>[], fetchPrice?: (coinType: string) => Promise<number | undefined>) => Promise<string[]>;
|
|
40
|
+
export declare const initializeSuilend: (suiGrpcClient: SuiGrpcClient, suilendClient: SuilendClient, lendingMarketMetadata?: LendingMarketMetadata, fallbackPythEndpoint?: string, options?: {
|
|
41
|
+
/**
|
|
42
|
+
* Price connection to use instead of the one this function would build.
|
|
43
|
+
*
|
|
44
|
+
* Without this, the connection is constructed here and never exposed, so a
|
|
45
|
+
* caller cannot authenticate it, point it at a different Hermes deployment,
|
|
46
|
+
* or give it failover — it is unreachable from outside.
|
|
47
|
+
*
|
|
48
|
+
* SUPERSEDES `fallbackPythEndpoint`, which is ignored when this is set.
|
|
49
|
+
* That parameter is a hint for building a connection, and there is nothing
|
|
50
|
+
* left to build; it engages only when a `/live` probe fails, which tests
|
|
51
|
+
* whether Hermes is UP, not whether it still serves your feeds (a 401 or a
|
|
52
|
+
* per-feed 404 never trips it). Supply failover on the connection you
|
|
53
|
+
* inject — a multi-endpoint connection covers strictly more than the probe
|
|
54
|
+
* did.
|
|
55
|
+
*
|
|
56
|
+
* YOU ALSO OWN THE TIMEOUT. The connection this function builds for itself
|
|
57
|
+
* uses 30s; pyth-sui-js defaults to 5s, so an injected connection
|
|
58
|
+
* constructed with default options gets a 6x tighter deadline than
|
|
59
|
+
* initializeSuilend used to guarantee. Nothing here can impose a timeout on
|
|
60
|
+
* an object it did not construct — set it when you build the connection.
|
|
61
|
+
*
|
|
62
|
+
* Must be a `SuiPriceServiceConnection` (a subclass is fine, and is how you
|
|
63
|
+
* add `getLatestPriceFeedsPartial`). Not the structural
|
|
64
|
+
* `PartialPriceFeedSource` shape: with `tolerateMissingPriceFeeds` off this
|
|
65
|
+
* connection goes to `refreshReservePrice`, which needs the class's own
|
|
66
|
+
* `getLatestPriceFeeds`. The lower-level `refreshReservePriceTolerant` does
|
|
67
|
+
* accept the structural shape, since it only ever needs the partial fetch.
|
|
68
|
+
*/
|
|
69
|
+
pythConnection?: SuiPriceServiceConnection;
|
|
70
|
+
/**
|
|
71
|
+
* Let a reserve whose feed no endpoint can serve keep the price already in
|
|
72
|
+
* its on-chain state, instead of failing the whole market. Off by default.
|
|
73
|
+
*
|
|
74
|
+
* OPT-IN because it trades a loud failure for a quiet one, and only the
|
|
75
|
+
* caller knows which it wants. A UI is usually better off failing visibly
|
|
76
|
+
* than rendering a stale price with no indication; a liquidator watching 45
|
|
77
|
+
* reserves is better off pricing the 41 it can than going blind over the 4
|
|
78
|
+
* it cannot.
|
|
79
|
+
*
|
|
80
|
+
* REQUIRES `pythConnection` to implement `getLatestPriceFeedsPartial` —
|
|
81
|
+
* throws otherwise, rather than degrading to the identical hard failure
|
|
82
|
+
* this flag exists to avoid. The connection this function builds for itself
|
|
83
|
+
* is all-or-nothing, so the flag cannot be used alone.
|
|
84
|
+
*
|
|
85
|
+
* Callers that enable this MUST read `unpricedCoinTypes` and decide per
|
|
86
|
+
* reserve what a kept price is allowed to mean — nothing here makes that
|
|
87
|
+
* judgement for them. An unpriced reserve stays in `refreshedRawReserves`
|
|
88
|
+
* carrying its on-chain price, which may be zero, so folding that array
|
|
89
|
+
* into collateral or borrow-limit math without excluding those coin types
|
|
90
|
+
* prices them at literal zero.
|
|
91
|
+
*/
|
|
92
|
+
tolerateMissingPriceFeeds?: boolean;
|
|
93
|
+
}) => Promise<{
|
|
13
94
|
lendingMarket: {
|
|
14
95
|
version: bigint;
|
|
15
96
|
reserves: {
|
|
@@ -148,6 +229,7 @@ export declare const initializeSuilend: (suiGrpcClient: SuiGrpcClient, suilendCl
|
|
|
148
229
|
};
|
|
149
230
|
coinMetadataMap: Record<string, SuiClientTypes.CoinMetadata>;
|
|
150
231
|
refreshedRawReserves: Reserve<string>[];
|
|
232
|
+
unpricedCoinTypes: string[];
|
|
151
233
|
reserveMap: Record<string, {
|
|
152
234
|
config: {
|
|
153
235
|
$typeName: string;
|
package/lib/initialize.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { normalizeStructTag } from "@mysten/sui/utils";
|
|
2
|
-
import { SuiPriceServiceConnection } from "@pythnetwork/pyth-sui-js";
|
|
3
2
|
import BigNumber from "bignumber.js";
|
|
4
|
-
import { NORMALIZED_ALKIMI_COINTYPE, NORMALIZED_AUSD_COINTYPE, NORMALIZED_BLUE_COINTYPE, NORMALIZED_BUCK_COINTYPE, NORMALIZED_DEEP_COINTYPE, NORMALIZED_DMC_COINTYPE, NORMALIZED_FUD_COINTYPE, NORMALIZED_HAEDAL_COINTYPE, NORMALIZED_HIPPO_COINTYPE, NORMALIZED_IKA_COINTYPE, NORMALIZED_KOBAN_COINTYPE, NORMALIZED_LBTC_COINTYPE, NORMALIZED_NS_COINTYPE, NORMALIZED_SEND_COINTYPE, NORMALIZED_SEND_POINTS_S1_COINTYPE, NORMALIZED_SEND_POINTS_S2_COINTYPE, NORMALIZED_SOL_COINTYPE, NORMALIZED_SUI_COINTYPE, NORMALIZED_UP_COINTYPE, NORMALIZED_USDB_COINTYPE, NORMALIZED_USDC_COINTYPE, NORMALIZED_USDsui_COINTYPE, NORMALIZED_WAL_COINTYPE, NORMALIZED_WBTC_COINTYPE, NORMALIZED_WETH_COINTYPE, NORMALIZED_XAUm_COINTYPE, NORMALIZED_eEARN_COINTYPE, NORMALIZED_eTHIRD_COINTYPE, NORMALIZED_flSUI_COINTYPE, NORMALIZED_fpSUI_COINTYPE, NORMALIZED_fudSUI_COINTYPE, NORMALIZED_iSUI_COINTYPE, NORMALIZED_jugSUI_COINTYPE, NORMALIZED_kSUI_COINTYPE, NORMALIZED_mSUI_COINTYPE, NORMALIZED_mUSD_COINTYPE, NORMALIZED_oshiSUI_COINTYPE, NORMALIZED_sSUI_COINTYPE, NORMALIZED_sdeUSD_COINTYPE, NORMALIZED_stratSUI_COINTYPE, NORMALIZED_suiETH_COINTYPE, NORMALIZED_suiUSDT_COINTYPE, NORMALIZED_suiUSDe_COINTYPE, NORMALIZED_suiWBTC_COINTYPE, NORMALIZED_trevinSUI_COINTYPE, NORMALIZED_upSUI_COINTYPE, NORMALIZED_wUSDC_COINTYPE, NORMALIZED_wUSDT_COINTYPE, NORMALIZED_xBTC_COINTYPE, NORMALIZED_yapSUI_COINTYPE, TEMPORARY_PYTH_PRICE_FEED_COINTYPES,
|
|
5
|
-
import { LENDING_MARKET_ID, SuilendClient } from "../client.js";
|
|
3
|
+
import { NORMALIZED_ALKIMI_COINTYPE, NORMALIZED_AUSD_COINTYPE, NORMALIZED_BLUE_COINTYPE, NORMALIZED_BUCK_COINTYPE, NORMALIZED_DEEP_COINTYPE, NORMALIZED_DMC_COINTYPE, NORMALIZED_FUD_COINTYPE, NORMALIZED_HAEDAL_COINTYPE, NORMALIZED_HIPPO_COINTYPE, NORMALIZED_IKA_COINTYPE, NORMALIZED_KOBAN_COINTYPE, NORMALIZED_LBTC_COINTYPE, NORMALIZED_NS_COINTYPE, NORMALIZED_SEND_COINTYPE, NORMALIZED_SEND_POINTS_S1_COINTYPE, NORMALIZED_SEND_POINTS_S2_COINTYPE, NORMALIZED_SOL_COINTYPE, NORMALIZED_SUI_COINTYPE, NORMALIZED_UP_COINTYPE, NORMALIZED_USDB_COINTYPE, NORMALIZED_USDC_COINTYPE, NORMALIZED_USDsui_COINTYPE, NORMALIZED_WAL_COINTYPE, NORMALIZED_WBTC_COINTYPE, NORMALIZED_WETH_COINTYPE, NORMALIZED_XAUm_COINTYPE, NORMALIZED_eEARN_COINTYPE, NORMALIZED_eTHIRD_COINTYPE, NORMALIZED_flSUI_COINTYPE, NORMALIZED_fpSUI_COINTYPE, NORMALIZED_fudSUI_COINTYPE, NORMALIZED_iSUI_COINTYPE, NORMALIZED_jugSUI_COINTYPE, NORMALIZED_kSUI_COINTYPE, NORMALIZED_mSUI_COINTYPE, NORMALIZED_mUSD_COINTYPE, NORMALIZED_oshiSUI_COINTYPE, NORMALIZED_sSUI_COINTYPE, NORMALIZED_sdeUSD_COINTYPE, NORMALIZED_stratSUI_COINTYPE, NORMALIZED_suiETH_COINTYPE, NORMALIZED_suiUSDT_COINTYPE, NORMALIZED_suiUSDe_COINTYPE, NORMALIZED_suiWBTC_COINTYPE, NORMALIZED_trevinSUI_COINTYPE, NORMALIZED_upSUI_COINTYPE, NORMALIZED_wUSDC_COINTYPE, NORMALIZED_wUSDT_COINTYPE, NORMALIZED_xBTC_COINTYPE, NORMALIZED_yapSUI_COINTYPE, TEMPORARY_PYTH_PRICE_FEED_COINTYPES, getCoinMetadataMap, getPrice, isSendPoints, isSteammPoints, } from "@suilend/sui-core";
|
|
4
|
+
import { LENDING_MARKET_ID, SuilendClient, listAllOwnedObjects, } from "../client.js";
|
|
6
5
|
import { parseLendingMarket, parseObligation } from "../parsers/index.js";
|
|
7
6
|
import * as simulate from "../utils/simulate.js";
|
|
8
7
|
import { WAD } from "./constants.js";
|
|
9
|
-
import {
|
|
8
|
+
import { resolvePythConnection } from "./pyth.js";
|
|
10
9
|
import { STRATEGY_TYPE_INFO_MAP, STRATEGY_WRAPPER_PACKAGE_ID_V1, } from "./strategyOwnerCap.js";
|
|
11
10
|
export const RESERVES_CUSTOM_ORDER = {
|
|
12
11
|
[LENDING_MARKET_ID]: [
|
|
@@ -85,7 +84,79 @@ const TREATS_COINTYPE = "0x0dadb7fa2771c2952f96161fc1f0c105d1f22d53926b9ff2498a8
|
|
|
85
84
|
export const NORMALIZED_MAYA_COINTYPE = normalizeStructTag(MAYA_COINTYPE);
|
|
86
85
|
export const NORMALIZED_mPOINTS_COINTYPE = normalizeStructTag(mPOINTS_COINTYPE);
|
|
87
86
|
export const NORMALIZED_TREATS_COINTYPE = normalizeStructTag(TREATS_COINTYPE);
|
|
88
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Price reserves for `initializeSuilend`, honouring the opt-in tolerance flag.
|
|
89
|
+
*
|
|
90
|
+
* Extracted so both branches are directly testable — the difference between
|
|
91
|
+
* them is a throw, which is the whole reason the flag exists and is exactly
|
|
92
|
+
* what a regression here would silently change.
|
|
93
|
+
*/
|
|
94
|
+
export const refreshReservesForInitialize = async (reserves, pythConnection, tolerateMissingPriceFeeds) => {
|
|
95
|
+
if (tolerateMissingPriceFeeds) {
|
|
96
|
+
// Tolerance is only expressible over a connection that can report WHICH
|
|
97
|
+
// feeds it could not serve. A stock SuiPriceServiceConnection cannot: the
|
|
98
|
+
// method does not exist in pyth-sui-js, so the tolerant path would fall
|
|
99
|
+
// back to the all-or-nothing fetch, and Hermes v2 404s the whole batch over
|
|
100
|
+
// one unknown id — the caller would get the same hard failure as before the
|
|
101
|
+
// flag existed, having explicitly asked not to.
|
|
102
|
+
//
|
|
103
|
+
// Rejected here rather than left to degrade quietly, because the pairing is
|
|
104
|
+
// otherwise invisible: nothing in the flag's name or type says it needs a
|
|
105
|
+
// connection the SDK cannot build for itself. Checked on CAPABILITY, not on
|
|
106
|
+
// whether a connection was injected — injecting a stock one is equally
|
|
107
|
+
// useless. Direct callers of refreshReservePriceTolerant keep the strict
|
|
108
|
+
// fallback, which is correct for an endpoint that omits unknown ids.
|
|
109
|
+
if (!simulate.canReportMissingPriceFeeds(pythConnection)) {
|
|
110
|
+
throw new Error(`tolerateMissingPriceFeeds requires a connection that can report per-feed gaps. The connection in use is all-or-nothing, so an endpoint that rejects the whole batch over one unknown feed would fail initialization regardless of this flag — ${simulate.PARTIAL_FETCH_REQUIRED_HINT}. Note that patching SuiPriceServiceConnection.prototype does NOT satisfy this: it can reroute getLatestPriceFeeds but cannot add a per-feed gap report.`);
|
|
111
|
+
}
|
|
112
|
+
return simulate.refreshReservePriceTolerant(reserves, pythConnection);
|
|
113
|
+
}
|
|
114
|
+
// Default path, behaviourally identical to what this function did before the
|
|
115
|
+
// flag existed: throws if any requested feed is absent from the response.
|
|
116
|
+
return {
|
|
117
|
+
reserves: await simulate.refreshReservePrice(reserves, pythConnection),
|
|
118
|
+
unpricedCoinTypes: [],
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* Price the temporary-Pyth-feed reserves from the cached-price service, in
|
|
123
|
+
* place, and report which ones ended up with a FABRICATED price.
|
|
124
|
+
*
|
|
125
|
+
* These reserves never reach the Pyth refresh at all, so nothing else can
|
|
126
|
+
* report them. When the cached lookup fails the price falls back to a
|
|
127
|
+
* hardcoded 0.0001 — a placeholder chosen to be non-zero, not a market price —
|
|
128
|
+
* and previously that was indistinguishable from a real quote: the reserve
|
|
129
|
+
* carried a plausible-looking number and appeared in no gap report.
|
|
130
|
+
*
|
|
131
|
+
* Extracted for the same reason as `refreshReservesForInitialize`:
|
|
132
|
+
* `initializeSuilend` needs a gRPC client and a SuilendClient, so logic left
|
|
133
|
+
* inline in it cannot be tested, and "is this price real" is not something to
|
|
134
|
+
* leave unverified.
|
|
135
|
+
*/
|
|
136
|
+
export const priceTemporaryPythFeedReserves = async (reserves, fetchPrice = getPrice) => {
|
|
137
|
+
const fabricatedPriceCoinTypes = [];
|
|
138
|
+
await Promise.all(reserves.map(async (reserve) => {
|
|
139
|
+
const coinType = normalizeStructTag(reserve.coinType.name);
|
|
140
|
+
let cachedUsdPrice;
|
|
141
|
+
try {
|
|
142
|
+
cachedUsdPrice = await fetchPrice(coinType);
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
console.error(err);
|
|
146
|
+
}
|
|
147
|
+
if (cachedUsdPrice === undefined) {
|
|
148
|
+
cachedUsdPrice = 0.0001; // Non-zero price override if no price
|
|
149
|
+
fabricatedPriceCoinTypes.push(coinType);
|
|
150
|
+
}
|
|
151
|
+
const parsedCachedUsdPrice = BigInt(+new BigNumber(cachedUsdPrice)
|
|
152
|
+
.times(WAD)
|
|
153
|
+
.integerValue(BigNumber.ROUND_DOWN));
|
|
154
|
+
reserve.price.value = parsedCachedUsdPrice;
|
|
155
|
+
reserve.smoothedPrice.value = parsedCachedUsdPrice;
|
|
156
|
+
}));
|
|
157
|
+
return fabricatedPriceCoinTypes;
|
|
158
|
+
};
|
|
159
|
+
export const initializeSuilend = async (suiGrpcClient, suilendClient, lendingMarketMetadata, fallbackPythEndpoint, options) => {
|
|
89
160
|
const nowMs = Date.now();
|
|
90
161
|
const nowS = Math.floor(nowMs / 1000);
|
|
91
162
|
const interestCompoundedRawReserves = suilendClient.lendingMarket.reserves.map((r) => simulate.compoundReserveInterest(r, nowS));
|
|
@@ -100,35 +171,35 @@ export const initializeSuilend = async (suiGrpcClient, suilendClient, lendingMar
|
|
|
100
171
|
reservesWithoutTemporaryPythPriceFeeds.push(reserve);
|
|
101
172
|
}
|
|
102
173
|
}
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
});
|
|
108
|
-
const [refreshedReservesWithoutTemporaryPythPriceFeeds] = await Promise.all([
|
|
109
|
-
simulate.refreshReservePrice(reservesWithoutTemporaryPythPriceFeeds, pythConnection),
|
|
110
|
-
Promise.all(reservesWithTemporaryPythPriceFeeds.map((reserve) => (async () => {
|
|
111
|
-
let cachedUsdPrice;
|
|
112
|
-
try {
|
|
113
|
-
cachedUsdPrice = await getPrice(normalizeStructTag(reserve.coinType.name));
|
|
114
|
-
}
|
|
115
|
-
catch (err) {
|
|
116
|
-
console.error(err);
|
|
117
|
-
}
|
|
118
|
-
if (cachedUsdPrice === undefined)
|
|
119
|
-
cachedUsdPrice = 0.0001; // Non-zero price override if no price
|
|
120
|
-
const parsedCachedUsdPrice = BigInt(+new BigNumber(cachedUsdPrice)
|
|
121
|
-
.times(WAD)
|
|
122
|
-
.integerValue(BigNumber.ROUND_DOWN));
|
|
123
|
-
reserve.price.value = parsedCachedUsdPrice;
|
|
124
|
-
reserve.smoothedPrice.value = parsedCachedUsdPrice;
|
|
125
|
-
})())),
|
|
174
|
+
const pythConnection = await resolvePythConnection(options?.pythConnection, fallbackPythEndpoint);
|
|
175
|
+
const [refreshedWithoutTemporary, fabricatedPriceCoinTypes] = await Promise.all([
|
|
176
|
+
refreshReservesForInitialize(reservesWithoutTemporaryPythPriceFeeds, pythConnection, options?.tolerateMissingPriceFeeds),
|
|
177
|
+
priceTemporaryPythFeedReserves(reservesWithTemporaryPythPriceFeeds),
|
|
126
178
|
]);
|
|
127
179
|
// Recombine reserves back into a single array
|
|
128
180
|
const refreshedRawReserves = [
|
|
129
|
-
...
|
|
181
|
+
...refreshedWithoutTemporary.reserves,
|
|
130
182
|
...reservesWithTemporaryPythPriceFeeds,
|
|
131
183
|
];
|
|
184
|
+
// Reserves NOT carrying a real refreshed price, from either cause: no endpoint
|
|
185
|
+
// served the Pyth feed (needs `tolerateMissingPriceFeeds`, else the refresh
|
|
186
|
+
// threw), or a temporary-feed reserve fell back to a fabricated placeholder
|
|
187
|
+
// (independent of that flag). Surfaced rather than logged-and-forgotten: only
|
|
188
|
+
// the caller knows whether a given reserve can safely hold such a price.
|
|
189
|
+
//
|
|
190
|
+
// Normalized, matching `reserveMap` / `reserveCoinTypes` keys. Entries in
|
|
191
|
+
// `refreshedRawReserves` carry the RAW on-chain `coinType.name`, which never
|
|
192
|
+
// compares equal to these (no `0x` prefix), so filtering that array means
|
|
193
|
+
// normalizing first:
|
|
194
|
+
//
|
|
195
|
+
// const unpriced = new Set(unpricedCoinTypes);
|
|
196
|
+
// refreshedRawReserves.filter(
|
|
197
|
+
// (r) => !unpriced.has(normalizeStructTag(r.coinType.name)),
|
|
198
|
+
// );
|
|
199
|
+
const unpricedCoinTypes = [
|
|
200
|
+
...refreshedWithoutTemporary.unpricedCoinTypes,
|
|
201
|
+
...fabricatedPriceCoinTypes,
|
|
202
|
+
];
|
|
132
203
|
const miscCoinTypes = [
|
|
133
204
|
NORMALIZED_SEND_POINTS_S1_COINTYPE,
|
|
134
205
|
NORMALIZED_SEND_POINTS_S2_COINTYPE,
|
|
@@ -193,6 +264,7 @@ export const initializeSuilend = async (suiGrpcClient, suilendClient, lendingMar
|
|
|
193
264
|
lendingMarket,
|
|
194
265
|
coinMetadataMap,
|
|
195
266
|
refreshedRawReserves,
|
|
267
|
+
unpricedCoinTypes,
|
|
196
268
|
reserveMap,
|
|
197
269
|
reserveCoinTypes: uniqueReserveCoinTypes,
|
|
198
270
|
reserveCoinMetadataMap,
|
|
@@ -230,7 +302,9 @@ export const initializeObligations = async (suiGrpcClient, suilendClient, refres
|
|
|
230
302
|
const hasStrategies = Object.values(STRATEGY_TYPE_INFO_MAP).some((info) => info.lendingMarketId === suilendClient.lendingMarket.id);
|
|
231
303
|
if (!hasStrategies)
|
|
232
304
|
return [];
|
|
233
|
-
const
|
|
305
|
+
const allObjects = await listAllOwnedObjects(suiGrpcClient, address, `${STRATEGY_WRAPPER_PACKAGE_ID_V1}::strategy_wrapper::StrategyOwnerCap`, { json: true });
|
|
306
|
+
const type = normalizeStructTag(`${STRATEGY_WRAPPER_PACKAGE_ID_V1}::strategy_wrapper::StrategyOwnerCap<${suilendClient.lendingMarket.$typeArgs[0]}>`);
|
|
307
|
+
const objects = allObjects.filter((obj) => normalizeStructTag(obj.type) === type);
|
|
234
308
|
return objects.map((obj) => {
|
|
235
309
|
const json = obj.json;
|
|
236
310
|
const id = json.id;
|
|
@@ -248,10 +322,10 @@ export const initializeObligations = async (suiGrpcClient, suilendClient, refres
|
|
|
248
322
|
})(),
|
|
249
323
|
SuilendClient.getObligationOwnerCaps(address, suilendClient.lendingMarket.$typeArgs, suiGrpcClient),
|
|
250
324
|
]);
|
|
251
|
-
const obligations = (await
|
|
325
|
+
const obligations = (await SuilendClient.getObligations([
|
|
252
326
|
...strategyOwnerCaps.map((soc) => soc.obligationId),
|
|
253
327
|
...obligationOwnerCaps.map((ownerCap) => ownerCap.obligationId),
|
|
254
|
-
]
|
|
328
|
+
], suilendClient.lendingMarket.$typeArgs, suiGrpcClient))
|
|
255
329
|
.map((rawObligation) => simulate.refreshObligation(rawObligation, refreshedRawReserves))
|
|
256
330
|
.map((refreshedObligation) => parseObligation(refreshedObligation, reserveMap, strategyOwnerCaps.some((soc) => soc.obligationId === refreshedObligation.id)))
|
|
257
331
|
.sort((a, b) => +b.netValueUsd.minus(a.netValueUsd));
|
package/lib/pyth.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SuiPriceServiceConnection } from "@pythnetwork/pyth-sui-js";
|
|
1
2
|
export declare const PRIMARY_PYTH_ENDPOINT = "https://hermes.pyth.network";
|
|
2
3
|
/**
|
|
3
4
|
* Tests if the primary Pyth connection endpoint is working by checking the /live endpoint
|
|
@@ -10,3 +11,18 @@ export declare const testPrimaryPythConnection: () => Promise<boolean>;
|
|
|
10
11
|
* @returns The endpoint URL that is working, or the primary endpoint if fallback fails
|
|
11
12
|
*/
|
|
12
13
|
export declare const getWorkingPythEndpoint: (fallbackPythEndpoint?: string) => Promise<string>;
|
|
14
|
+
/**
|
|
15
|
+
* Pick the price connection `initializeSuilend` will use.
|
|
16
|
+
*
|
|
17
|
+
* Lives here rather than in initialize.ts for two reasons: it belongs beside
|
|
18
|
+
* getWorkingPythEndpoint, the thing it decides whether to call; and this module
|
|
19
|
+
* imports nothing, so the seam is testable without resolving
|
|
20
|
+
* `@suilend/sui-core` (whose in-repo `exports` points at a .js file that does
|
|
21
|
+
* not exist, making initialize.ts unimportable from source).
|
|
22
|
+
*
|
|
23
|
+
* Skipping the probe for an injected connection is deliberate, not an
|
|
24
|
+
* optimisation. getWorkingPythEndpoint fetches hermes.pyth.network/live, which
|
|
25
|
+
* answers for whether Hermes is up — not for whether it still serves the
|
|
26
|
+
* caller's feeds, and not for whether the caller is even pointed at Hermes.
|
|
27
|
+
*/
|
|
28
|
+
export declare const resolvePythConnection: (pythConnectionOverride?: SuiPriceServiceConnection, fallbackPythEndpoint?: string) => Promise<SuiPriceServiceConnection>;
|
package/lib/pyth.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SuiPriceServiceConnection } from "@pythnetwork/pyth-sui-js";
|
|
1
2
|
export const PRIMARY_PYTH_ENDPOINT = "https://hermes.pyth.network";
|
|
2
3
|
/**
|
|
3
4
|
* Tests if the primary Pyth connection endpoint is working by checking the /live endpoint
|
|
@@ -37,3 +38,26 @@ export const getWorkingPythEndpoint = async (fallbackPythEndpoint) => {
|
|
|
37
38
|
return PRIMARY_PYTH_ENDPOINT;
|
|
38
39
|
}
|
|
39
40
|
};
|
|
41
|
+
/**
|
|
42
|
+
* Pick the price connection `initializeSuilend` will use.
|
|
43
|
+
*
|
|
44
|
+
* Lives here rather than in initialize.ts for two reasons: it belongs beside
|
|
45
|
+
* getWorkingPythEndpoint, the thing it decides whether to call; and this module
|
|
46
|
+
* imports nothing, so the seam is testable without resolving
|
|
47
|
+
* `@suilend/sui-core` (whose in-repo `exports` points at a .js file that does
|
|
48
|
+
* not exist, making initialize.ts unimportable from source).
|
|
49
|
+
*
|
|
50
|
+
* Skipping the probe for an injected connection is deliberate, not an
|
|
51
|
+
* optimisation. getWorkingPythEndpoint fetches hermes.pyth.network/live, which
|
|
52
|
+
* answers for whether Hermes is up — not for whether it still serves the
|
|
53
|
+
* caller's feeds, and not for whether the caller is even pointed at Hermes.
|
|
54
|
+
*/
|
|
55
|
+
export const resolvePythConnection = async (pythConnectionOverride, fallbackPythEndpoint) => {
|
|
56
|
+
if (pythConnectionOverride)
|
|
57
|
+
return pythConnectionOverride;
|
|
58
|
+
// Get a working Pyth endpoint (try primary, fallback to fallbackPythEndpoint if provided)
|
|
59
|
+
const pythEndpoint = await getWorkingPythEndpoint(fallbackPythEndpoint);
|
|
60
|
+
return new SuiPriceServiceConnection(pythEndpoint, {
|
|
61
|
+
timeout: 30 * 1000,
|
|
62
|
+
});
|
|
63
|
+
};
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"@suilend/sdk","version":"
|
|
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"}
|
package/utils/obligation.d.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import { SuiClientTypes } from "@mysten/sui/client";
|
|
1
2
|
import { SuiGraphQLClient } from "@mysten/sui/graphql";
|
|
2
3
|
import { SuiGrpcClient } from "@mysten/sui/grpc";
|
|
3
4
|
import { Obligation } from "../_generated/suilend/obligation/structs";
|
|
4
5
|
import { ParsedObligation } from "../parsers";
|
|
5
6
|
export declare function fetchAllObligationsForMarketWithHandler(suiGrpcClient: SuiGrpcClient, lendingMarketId: string, lendingMarketType: string, chunkHandler: (obligations: Obligation<string>[]) => Promise<void>): Promise<void>;
|
|
6
7
|
export declare function fetchAllObligationsForMarket(suiGrpcClient: SuiGrpcClient, lendingMarketId: string, lendingMarketType: string): Promise<Obligation<string>[]>;
|
|
8
|
+
export declare function chunkedMultiGet(suiGrpcClient: SuiGrpcClient, objectIds: string[]): Promise<(Error | SuiClientTypes.Object<{
|
|
9
|
+
content: true;
|
|
10
|
+
}>)[]>;
|
|
7
11
|
export type FormattedObligationHistory = NonLiquidationHistoryEvent | LiquidationHistoryEvent;
|
|
8
12
|
export type NonLiquidationHistoryEvent = {
|
|
9
13
|
reserveId: string;
|
package/utils/obligation.js
CHANGED
|
@@ -66,7 +66,7 @@ export async function fetchAllObligationsForMarket(suiGrpcClient, lendingMarketI
|
|
|
66
66
|
}
|
|
67
67
|
return obligations;
|
|
68
68
|
}
|
|
69
|
-
async function chunkedMultiGet(suiGrpcClient, objectIds) {
|
|
69
|
+
export async function chunkedMultiGet(suiGrpcClient, objectIds) {
|
|
70
70
|
const limit = pLimit(30);
|
|
71
71
|
const results = [];
|
|
72
72
|
const chunks = splitIntoChunks(objectIds, 50);
|
package/utils/simulate.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SuiPriceServiceConnection } from "@pythnetwork/pyth-sui-js";
|
|
1
|
+
import { PriceFeed, SuiPriceServiceConnection } from "@pythnetwork/pyth-sui-js";
|
|
2
2
|
import BigNumber from "bignumber.js";
|
|
3
3
|
import { Decimal } from "../_generated/suilend/decimal/structs";
|
|
4
4
|
import { PoolRewardManager, UserRewardManager } from "../_generated/suilend/liquidity-mining/structs";
|
|
@@ -22,6 +22,73 @@ export declare const calculateDepositAprPercent: (reserve: Reserve<string>) => B
|
|
|
22
22
|
export declare const compoundReserveInterest: (reserve: Reserve<string>, nowS: number) => Reserve<string>;
|
|
23
23
|
export declare const updatePoolRewardsManager: (manager: PoolRewardManager, nowMs: number) => PoolRewardManager;
|
|
24
24
|
export declare const refreshReservePrice: (reserves: Reserve<string>[], pythConnection: SuiPriceServiceConnection) => Promise<Reserve<string>[]>;
|
|
25
|
+
/**
|
|
26
|
+
* A connection that can report which of the requested feeds it could not serve,
|
|
27
|
+
* instead of failing the whole batch. Structural on purpose: any object with
|
|
28
|
+
* this method qualifies, so a caller can inject its own client without this
|
|
29
|
+
* package depending on it.
|
|
30
|
+
*/
|
|
31
|
+
export interface PartialPriceFeedSource {
|
|
32
|
+
getLatestPriceFeedsPartial(ids: string[]): Promise<{
|
|
33
|
+
feeds: PriceFeed[];
|
|
34
|
+
missing: string[];
|
|
35
|
+
}>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Whether this connection can report WHICH requested feeds it could not serve.
|
|
39
|
+
*
|
|
40
|
+
* Exported because it is a precondition callers need to check, not just an
|
|
41
|
+
* internal branch: `tolerateMissingPriceFeeds` is meaningless without it (see
|
|
42
|
+
* `refreshReservesForInitialize`, which refuses the combination).
|
|
43
|
+
*/
|
|
44
|
+
export declare function canReportMissingPriceFeeds(connection: SuiPriceServiceConnection | PartialPriceFeedSource): connection is PartialPriceFeedSource;
|
|
45
|
+
/**
|
|
46
|
+
* The remedy both gap-tolerance failure paths point at.
|
|
47
|
+
*
|
|
48
|
+
* One constant because the two paths fail at different moments for the same
|
|
49
|
+
* underlying reason — `refreshReservesForInitialize` refuses up front on
|
|
50
|
+
* capability, `refreshReservePriceTolerant` only once an endpoint actually
|
|
51
|
+
* rejects a batch — and separately worded advice for one cause is how the two
|
|
52
|
+
* drift apart.
|
|
53
|
+
*/
|
|
54
|
+
export declare const PARTIAL_FETCH_REQUIRED_HINT = "inject a connection implementing getLatestPriceFeedsPartial (e.g. a failover connection that reports per-feed gaps)";
|
|
55
|
+
/**
|
|
56
|
+
* Like `refreshReservePrice`, but a reserve whose feed no endpoint can serve
|
|
57
|
+
* keeps the price already in its on-chain state instead of failing every other
|
|
58
|
+
* reserve with it.
|
|
59
|
+
*
|
|
60
|
+
* `refreshReservePrice` is all-or-nothing in both directions, and both are
|
|
61
|
+
* hazardous once an endpoint stops carrying every feed: a missing feed throws
|
|
62
|
+
* (so one retired market blinds pricing for the whole lending market), while an
|
|
63
|
+
* undefined response silently returns EVERY reserve at its stale price with no
|
|
64
|
+
* signal at all. Hermes v2 rejects an entire batch when any requested id is
|
|
65
|
+
* unknown, which makes both reachable in practice.
|
|
66
|
+
*
|
|
67
|
+
* This reports `unpricedCoinTypes` rather than deciding what an unpriceable
|
|
68
|
+
* reserve means — that judgement needs reserve config the caller has and this
|
|
69
|
+
* function should not second-guess. Nothing is ever fabricated here.
|
|
70
|
+
*
|
|
71
|
+
* TWO THINGS A CALLER MUST KNOW:
|
|
72
|
+
*
|
|
73
|
+
* 1. An unpriced reserve is returned in `reserves` carrying whatever price its
|
|
74
|
+
* on-chain state holds — which may be ZERO for a reserve that was never
|
|
75
|
+
* priced on-chain. Folding `reserves` into collateral or borrow-limit math
|
|
76
|
+
* without first excluding `unpricedCoinTypes` therefore prices that reserve
|
|
77
|
+
* at literal zero. `unpricedCoinTypes` is the only thing distinguishing a
|
|
78
|
+
* real price from a kept one; it is not optional to read.
|
|
79
|
+
* 2. Tolerance requires a connection that can REPORT a gap
|
|
80
|
+
* (`getLatestPriceFeedsPartial`). Handed a stock `SuiPriceServiceConnection`,
|
|
81
|
+
* this falls back to the all-or-nothing fetch, which Hermes v2 answers with
|
|
82
|
+
* a 404 for the whole batch when any id is unknown — so there is nothing
|
|
83
|
+
* left to be tolerant with. That fetch is kept because it is correct for an
|
|
84
|
+
* endpoint that OMITS unknown ids rather than rejecting the batch, but the
|
|
85
|
+
* failure is re-thrown with attribution rather than surfacing as a bare HTTP
|
|
86
|
+
* error. `refreshReservesForInitialize` rejects the combination outright.
|
|
87
|
+
*/
|
|
88
|
+
export declare const refreshReservePriceTolerant: (reserves: Reserve<string>[], pythConnection: SuiPriceServiceConnection | PartialPriceFeedSource) => Promise<{
|
|
89
|
+
reserves: Reserve<string>[];
|
|
90
|
+
unpricedCoinTypes: string[];
|
|
91
|
+
}>;
|
|
25
92
|
export declare const updateUserRewardManager: (poolManager: PoolRewardManager, userRewardManager: UserRewardManager, nowMs: number) => UserRewardManager;
|
|
26
93
|
export declare const refreshObligation: (unrefreshedObligation: Obligation<string>, refreshedReserves: Reserve<string>[]) => Obligation<string>;
|
|
27
94
|
export declare const numberToDecimal: (value: number) => Decimal;
|
package/utils/simulate.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { bcs } from "@mysten/sui/bcs";
|
|
2
|
-
import { toHex } from "@mysten/sui/utils";
|
|
2
|
+
import { normalizeStructTag, toHex } from "@mysten/sui/utils";
|
|
3
3
|
import BigNumber from "bignumber.js";
|
|
4
4
|
import { v4 as uuidv4 } from "uuid";
|
|
5
5
|
import { Decimal } from "../_generated/suilend/decimal/structs.js";
|
|
@@ -118,25 +118,151 @@ export const updatePoolRewardsManager = (manager, nowMs) => {
|
|
|
118
118
|
updatedManager.lastUpdateTimeMs = BigInt(nowMs);
|
|
119
119
|
return updatedManager;
|
|
120
120
|
};
|
|
121
|
+
/** The Pyth feed id a reserve is priced from, in the response's `id` encoding. */
|
|
122
|
+
const reserveFeedId = (reserve) => toHex(new Uint8Array(reserve.priceIdentifier.bytes));
|
|
123
|
+
/**
|
|
124
|
+
* Index feeds by id for O(1) lookup, keeping the FIRST entry per id.
|
|
125
|
+
*
|
|
126
|
+
* First-wins matches the `priceFeeds.find(...)` this replaced, so a duplicated
|
|
127
|
+
* id in a response resolves to the same feed it always did. Pyth does not
|
|
128
|
+
* duplicate ids today; the point is that switching to a Map is not allowed to
|
|
129
|
+
* quietly change which one is picked if it ever does.
|
|
130
|
+
*/
|
|
131
|
+
const indexFeedsById = (priceFeeds) => {
|
|
132
|
+
const byId = new Map();
|
|
133
|
+
for (const feed of priceFeeds)
|
|
134
|
+
if (!byId.has(feed.id))
|
|
135
|
+
byId.set(feed.id, feed);
|
|
136
|
+
return byId;
|
|
137
|
+
};
|
|
138
|
+
/**
|
|
139
|
+
* Apply a feed's price to a COPY of `reserve`.
|
|
140
|
+
*
|
|
141
|
+
* Shared by both refresh functions so the price-mapping math has exactly one
|
|
142
|
+
* definition. It was duplicated verbatim, which meant a change to how a feed
|
|
143
|
+
* becomes a price had to be made twice or the two silently diverged.
|
|
144
|
+
*
|
|
145
|
+
* The two functions still own their own control flow — what a MISSING feed
|
|
146
|
+
* means is precisely where they are supposed to differ (see
|
|
147
|
+
* `refreshReservePriceTolerant`).
|
|
148
|
+
*/
|
|
149
|
+
const withFeedPrice = (reserve, priceFeed) => {
|
|
150
|
+
const newReserve = { ...reserve };
|
|
151
|
+
newReserve.price = stringToDecimal(priceFeed.getPriceUnchecked().getPriceAsNumberUnchecked().toString());
|
|
152
|
+
newReserve.smoothedPrice = stringToDecimal(priceFeed.getEmaPriceUnchecked().getPriceAsNumberUnchecked().toString());
|
|
153
|
+
newReserve.priceLastUpdateTimestampS = BigInt(priceFeed.getPriceUnchecked().publishTime);
|
|
154
|
+
return newReserve;
|
|
155
|
+
};
|
|
121
156
|
export const refreshReservePrice = async (reserves, pythConnection) => {
|
|
122
|
-
const priceIdentifiers = Array.from(new Set(reserves.map(
|
|
157
|
+
const priceIdentifiers = Array.from(new Set(reserves.map(reserveFeedId)));
|
|
123
158
|
const priceFeeds = await pythConnection.getLatestPriceFeeds(priceIdentifiers);
|
|
159
|
+
// Deliberately NOT a throw, and deliberately not expressed over the tolerant
|
|
160
|
+
// function below: consumers outside this repo depend on `undefined` meaning
|
|
161
|
+
// "keep every reserve's existing price". Hazardous (see the tolerant
|
|
162
|
+
// function's comment), but changing it here would turn a quiet failure loud
|
|
163
|
+
// under callers who never asked for that.
|
|
124
164
|
if (!priceFeeds)
|
|
125
165
|
return reserves;
|
|
166
|
+
const byId = indexFeedsById(priceFeeds);
|
|
126
167
|
const updatedReserves = [];
|
|
127
168
|
for (let i = 0; i < reserves.length; i++) {
|
|
128
169
|
const reserve = reserves[i];
|
|
129
|
-
const priceFeed =
|
|
170
|
+
const priceFeed = byId.get(reserveFeedId(reserve));
|
|
130
171
|
if (!priceFeed)
|
|
131
172
|
throw new Error(`Price feed not found for reserve ${reserve.coinType.name}`);
|
|
132
|
-
|
|
133
|
-
newReserve.price = stringToDecimal(priceFeed.getPriceUnchecked().getPriceAsNumberUnchecked().toString());
|
|
134
|
-
newReserve.smoothedPrice = stringToDecimal(priceFeed.getEmaPriceUnchecked().getPriceAsNumberUnchecked().toString());
|
|
135
|
-
newReserve.priceLastUpdateTimestampS = BigInt(priceFeed.getPriceUnchecked().publishTime);
|
|
136
|
-
updatedReserves.push(newReserve);
|
|
173
|
+
updatedReserves.push(withFeedPrice(reserve, priceFeed));
|
|
137
174
|
}
|
|
138
175
|
return updatedReserves;
|
|
139
176
|
};
|
|
177
|
+
/**
|
|
178
|
+
* Whether this connection can report WHICH requested feeds it could not serve.
|
|
179
|
+
*
|
|
180
|
+
* Exported because it is a precondition callers need to check, not just an
|
|
181
|
+
* internal branch: `tolerateMissingPriceFeeds` is meaningless without it (see
|
|
182
|
+
* `refreshReservesForInitialize`, which refuses the combination).
|
|
183
|
+
*/
|
|
184
|
+
export function canReportMissingPriceFeeds(connection) {
|
|
185
|
+
return (typeof connection.getLatestPriceFeedsPartial ===
|
|
186
|
+
"function");
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* The remedy both gap-tolerance failure paths point at.
|
|
190
|
+
*
|
|
191
|
+
* One constant because the two paths fail at different moments for the same
|
|
192
|
+
* underlying reason — `refreshReservesForInitialize` refuses up front on
|
|
193
|
+
* capability, `refreshReservePriceTolerant` only once an endpoint actually
|
|
194
|
+
* rejects a batch — and separately worded advice for one cause is how the two
|
|
195
|
+
* drift apart.
|
|
196
|
+
*/
|
|
197
|
+
export const PARTIAL_FETCH_REQUIRED_HINT = "inject a connection implementing getLatestPriceFeedsPartial (e.g. a failover connection that reports per-feed gaps)";
|
|
198
|
+
/**
|
|
199
|
+
* Like `refreshReservePrice`, but a reserve whose feed no endpoint can serve
|
|
200
|
+
* keeps the price already in its on-chain state instead of failing every other
|
|
201
|
+
* reserve with it.
|
|
202
|
+
*
|
|
203
|
+
* `refreshReservePrice` is all-or-nothing in both directions, and both are
|
|
204
|
+
* hazardous once an endpoint stops carrying every feed: a missing feed throws
|
|
205
|
+
* (so one retired market blinds pricing for the whole lending market), while an
|
|
206
|
+
* undefined response silently returns EVERY reserve at its stale price with no
|
|
207
|
+
* signal at all. Hermes v2 rejects an entire batch when any requested id is
|
|
208
|
+
* unknown, which makes both reachable in practice.
|
|
209
|
+
*
|
|
210
|
+
* This reports `unpricedCoinTypes` rather than deciding what an unpriceable
|
|
211
|
+
* reserve means — that judgement needs reserve config the caller has and this
|
|
212
|
+
* function should not second-guess. Nothing is ever fabricated here.
|
|
213
|
+
*
|
|
214
|
+
* TWO THINGS A CALLER MUST KNOW:
|
|
215
|
+
*
|
|
216
|
+
* 1. An unpriced reserve is returned in `reserves` carrying whatever price its
|
|
217
|
+
* on-chain state holds — which may be ZERO for a reserve that was never
|
|
218
|
+
* priced on-chain. Folding `reserves` into collateral or borrow-limit math
|
|
219
|
+
* without first excluding `unpricedCoinTypes` therefore prices that reserve
|
|
220
|
+
* at literal zero. `unpricedCoinTypes` is the only thing distinguishing a
|
|
221
|
+
* real price from a kept one; it is not optional to read.
|
|
222
|
+
* 2. Tolerance requires a connection that can REPORT a gap
|
|
223
|
+
* (`getLatestPriceFeedsPartial`). Handed a stock `SuiPriceServiceConnection`,
|
|
224
|
+
* this falls back to the all-or-nothing fetch, which Hermes v2 answers with
|
|
225
|
+
* a 404 for the whole batch when any id is unknown — so there is nothing
|
|
226
|
+
* left to be tolerant with. That fetch is kept because it is correct for an
|
|
227
|
+
* endpoint that OMITS unknown ids rather than rejecting the batch, but the
|
|
228
|
+
* failure is re-thrown with attribution rather than surfacing as a bare HTTP
|
|
229
|
+
* error. `refreshReservesForInitialize` rejects the combination outright.
|
|
230
|
+
*/
|
|
231
|
+
export const refreshReservePriceTolerant = async (reserves, pythConnection) => {
|
|
232
|
+
const priceIdentifiers = Array.from(new Set(reserves.map(reserveFeedId)));
|
|
233
|
+
let priceFeeds;
|
|
234
|
+
if (canReportMissingPriceFeeds(pythConnection)) {
|
|
235
|
+
priceFeeds = (await pythConnection.getLatestPriceFeedsPartial(priceIdentifiers)).feeds;
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
try {
|
|
239
|
+
priceFeeds = await pythConnection.getLatestPriceFeeds(priceIdentifiers);
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
// Not swallowed — a transport failure must stay a failure, or every
|
|
243
|
+
// reserve silently reports as unpriced and the caller cannot tell a dead
|
|
244
|
+
// endpoint from a retired feed. Re-thrown only to name the cause, which a
|
|
245
|
+
// bare "HTTP error! status: 404" does not.
|
|
246
|
+
throw new Error(`Tolerant price refresh failed on an all-or-nothing connection: ${error instanceof Error ? error.message : String(error)}. This connection cannot report which feeds are missing, so an endpoint that rejects the whole batch over one unknown id (Hermes v2) defeats tolerance entirely — ${PARTIAL_FETCH_REQUIRED_HINT}.`, { cause: error });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const byId = indexFeedsById(priceFeeds ?? []);
|
|
250
|
+
const unpricedCoinTypes = [];
|
|
251
|
+
const updatedReserves = reserves.map((reserve) => {
|
|
252
|
+
const priceFeed = byId.get(reserveFeedId(reserve));
|
|
253
|
+
if (!priceFeed) {
|
|
254
|
+
// Normalized to match every other coin type this SDK returns
|
|
255
|
+
// (`reserveMap`, `reserveCoinTypes`, and the parsers are all
|
|
256
|
+
// normalizeStructTag'd). The raw on-chain form never compares equal to
|
|
257
|
+
// one of those keys — it lacks the `0x` prefix — so reporting it raw made
|
|
258
|
+
// the cross-reference this field exists for silently never match.
|
|
259
|
+
unpricedCoinTypes.push(normalizeStructTag(reserve.coinType.name));
|
|
260
|
+
return reserve;
|
|
261
|
+
}
|
|
262
|
+
return withFeedPrice(reserve, priceFeed);
|
|
263
|
+
});
|
|
264
|
+
return { reserves: updatedReserves, unpricedCoinTypes };
|
|
265
|
+
};
|
|
140
266
|
export const updateUserRewardManager = (poolManager, userRewardManager, nowMs) => {
|
|
141
267
|
const updatedUserRewardManager = { ...userRewardManager };
|
|
142
268
|
for (let i = 0; i < poolManager.poolRewards.length; i++) {
|