@gearbox-protocol/sdk 18.1.0 → 18.2.0-next.10

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.
Files changed (98) hide show
  1. package/dist/cjs/abi/compressors/subcompressors/liquidation/treehouseLiquidationSubcompressor.js +179 -0
  2. package/dist/cjs/abi/compressors/subcompressors/withdrawal/treehouseWithdrawalSubcompressor.js +534 -0
  3. package/dist/cjs/abi/router/treehouseWorker.js +885 -0
  4. package/dist/cjs/onchain/market/adapters/abi/actionAbi.js +1 -0
  5. package/dist/cjs/onchain/market/adapters/abi/conctructorAbi.js +3 -0
  6. package/dist/cjs/onchain/market/adapters/types.js +3 -0
  7. package/dist/cjs/permissionless/api/GearboxPermissionless.js +12 -5
  8. package/dist/cjs/permissionless/api/analytics/PermissionlessAnalytics.js +50 -0
  9. package/dist/cjs/permissionless/api/analytics/index.js +4 -0
  10. package/dist/cjs/permissionless/api/analytics/schemas.js +27 -0
  11. package/dist/cjs/permissionless/api/analytics/types.js +1 -0
  12. package/dist/cjs/permissionless/api/index.js +3 -0
  13. package/dist/cjs/permissionless/api/oracles/PermissionlessOracles.js +444 -28
  14. package/dist/cjs/permissionless/api/oracles/enrich.js +99 -0
  15. package/dist/cjs/permissionless/api/oracles/schemas.js +102 -16
  16. package/dist/cjs/permissionless/api/schemas.js +0 -3
  17. package/dist/cjs/permissionless/bindings/index.js +1 -1
  18. package/dist/cjs/permissionless/index.js +4 -1
  19. package/dist/cjs/permissionless/utils/index.js +1 -0
  20. package/dist/cjs/permissionless/utils/price-update/get-prices.js +25 -0
  21. package/dist/cjs/permissionless/utils/price-update/index.js +1 -0
  22. package/dist/cjs/testnets/TestnetsAPI.js +179 -0
  23. package/dist/cjs/testnets/errors/TestnetNotFoundError.js +15 -0
  24. package/dist/cjs/testnets/errors/TestnetRequestFailedError.js +24 -0
  25. package/dist/cjs/testnets/errors/TestnetStatusError.js +19 -0
  26. package/dist/cjs/testnets/errors/TestnetValidationError.js +24 -0
  27. package/dist/cjs/testnets/errors/TestnetWaitTimeoutError.js +30 -0
  28. package/dist/cjs/testnets/errors/TestnetsError.js +34 -0
  29. package/dist/cjs/testnets/errors/index.js +13 -0
  30. package/dist/cjs/testnets/index.js +32 -0
  31. package/dist/cjs/testnets/package.json +1 -0
  32. package/dist/cjs/testnets/schemas.js +242 -0
  33. package/dist/cjs/testnets/types.js +1 -0
  34. package/dist/esm/abi/compressors/subcompressors/liquidation/treehouseLiquidationSubcompressor.js +178 -0
  35. package/dist/esm/abi/compressors/subcompressors/withdrawal/treehouseWithdrawalSubcompressor.js +533 -0
  36. package/dist/esm/abi/router/treehouseWorker.js +884 -0
  37. package/dist/esm/onchain/market/adapters/abi/actionAbi.js +1 -0
  38. package/dist/esm/onchain/market/adapters/abi/conctructorAbi.js +3 -0
  39. package/dist/esm/onchain/market/adapters/types.js +3 -0
  40. package/dist/esm/permissionless/api/GearboxPermissionless.js +12 -5
  41. package/dist/esm/permissionless/api/analytics/PermissionlessAnalytics.js +49 -0
  42. package/dist/esm/permissionless/api/analytics/index.js +2 -0
  43. package/dist/esm/permissionless/api/analytics/schemas.js +25 -0
  44. package/dist/esm/permissionless/api/analytics/types.js +1 -0
  45. package/dist/esm/permissionless/api/index.js +3 -1
  46. package/dist/esm/permissionless/api/oracles/PermissionlessOracles.js +446 -30
  47. package/dist/esm/permissionless/api/oracles/enrich.js +94 -0
  48. package/dist/esm/permissionless/api/oracles/schemas.js +94 -17
  49. package/dist/esm/permissionless/api/schemas.js +2 -4
  50. package/dist/esm/permissionless/bindings/index.js +1 -1
  51. package/dist/esm/permissionless/index.js +4 -3
  52. package/dist/esm/permissionless/utils/index.js +2 -2
  53. package/dist/esm/permissionless/utils/price-update/get-prices.js +25 -1
  54. package/dist/esm/permissionless/utils/price-update/index.js +2 -2
  55. package/dist/esm/testnets/TestnetsAPI.js +178 -0
  56. package/dist/esm/testnets/errors/TestnetNotFoundError.js +14 -0
  57. package/dist/esm/testnets/errors/TestnetRequestFailedError.js +23 -0
  58. package/dist/esm/testnets/errors/TestnetStatusError.js +18 -0
  59. package/dist/esm/testnets/errors/TestnetValidationError.js +23 -0
  60. package/dist/esm/testnets/errors/TestnetWaitTimeoutError.js +29 -0
  61. package/dist/esm/testnets/errors/TestnetsError.js +33 -0
  62. package/dist/esm/testnets/errors/index.js +7 -0
  63. package/dist/esm/testnets/index.js +11 -0
  64. package/dist/esm/testnets/package.json +1 -0
  65. package/dist/esm/testnets/schemas.js +228 -0
  66. package/dist/esm/testnets/types.js +1 -0
  67. package/dist/types/abi/compressors/subcompressors/liquidation/treehouseLiquidationSubcompressor.d.ts +148 -0
  68. package/dist/types/abi/compressors/subcompressors/withdrawal/treehouseWithdrawalSubcompressor.d.ts +440 -0
  69. package/dist/types/abi/router/treehouseWorker.d.ts +728 -0
  70. package/dist/types/onchain/market/adapters/types.d.ts +4 -1
  71. package/dist/types/permissionless/api/GearboxPermissionless.d.ts +11 -5
  72. package/dist/types/permissionless/api/analytics/PermissionlessAnalytics.d.ts +38 -0
  73. package/dist/types/permissionless/api/analytics/index.d.ts +3 -0
  74. package/dist/types/permissionless/api/analytics/schemas.d.ts +32 -0
  75. package/dist/types/permissionless/api/analytics/types.d.ts +38 -0
  76. package/dist/types/permissionless/api/index.d.ts +5 -2
  77. package/dist/types/permissionless/api/oracles/PermissionlessOracles.d.ts +159 -22
  78. package/dist/types/permissionless/api/oracles/enrich.d.ts +59 -0
  79. package/dist/types/permissionless/api/oracles/index.d.ts +2 -2
  80. package/dist/types/permissionless/api/oracles/schemas.d.ts +202 -7
  81. package/dist/types/permissionless/api/oracles/types.d.ts +401 -22
  82. package/dist/types/permissionless/api/schemas.d.ts +0 -2
  83. package/dist/types/permissionless/index.d.ts +5 -3
  84. package/dist/types/permissionless/utils/index.d.ts +2 -2
  85. package/dist/types/permissionless/utils/price-update/get-prices.d.ts +17 -0
  86. package/dist/types/permissionless/utils/price-update/index.d.ts +2 -2
  87. package/dist/types/testnets/TestnetsAPI.d.ts +108 -0
  88. package/dist/types/testnets/errors/TestnetNotFoundError.d.ts +12 -0
  89. package/dist/types/testnets/errors/TestnetRequestFailedError.d.ts +11 -0
  90. package/dist/types/testnets/errors/TestnetStatusError.d.ts +15 -0
  91. package/dist/types/testnets/errors/TestnetValidationError.d.ts +16 -0
  92. package/dist/types/testnets/errors/TestnetWaitTimeoutError.d.ts +21 -0
  93. package/dist/types/testnets/errors/TestnetsError.d.ts +47 -0
  94. package/dist/types/testnets/errors/index.d.ts +7 -0
  95. package/dist/types/testnets/index.d.ts +11 -0
  96. package/dist/types/testnets/schemas.d.ts +554 -0
  97. package/dist/types/testnets/types.d.ts +717 -0
  98. package/package.json +9 -3
@@ -1,19 +1,48 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_onchain_chain_chains = require("../../../onchain/chain/chains.js");
3
+ const require_onchain_constants_address_provider = require("../../../onchain/constants/address-provider.js");
4
+ const require_model_opportunities_schema = require("../../../model/opportunities.schema.js");
5
+ const require_abi_compressors_priceFeedCompressor = require("../../../abi/compressors/priceFeedCompressor.js");
6
+ require("../../../onchain/index.js");
2
7
  const require_offchain_AbstractOffchainNamespace = require("../../../offchain/AbstractOffchainNamespace.js");
8
+ require("../../deployment/addresses.js");
9
+ const require_permissionless_bindings_address_provider = require("../../bindings/address-provider.js");
3
10
  const require_permissionless_utils_price_update_get_prices = require("../../utils/price-update/get-prices.js");
11
+ const require_permissionless_api_oracles_enrich = require("./enrich.js");
4
12
  const require_permissionless_api_oracles_schemas = require("./schemas.js");
13
+ let viem = require("viem");
5
14
  //#region src/permissionless/api/oracles/PermissionlessOracles.ts
6
15
  const GET_PRICES_GAS_LIMIT_BY_CHAIN_ID = { 143: 100000000n };
7
16
  const GET_PRICES_CHUNK_SIZE = 200;
8
17
  /**
18
+ * What a market slot is: an oracle, a token, and whether it is the reserve
19
+ * one. Not the market — a market that has switched oracles has rows under
20
+ * each, and the slots are the old oracle's and the new one's separately.
21
+ */
22
+ function slotKey(oracle, token, reserve) {
23
+ return `${oracle.toLowerCase()}:${token.toLowerCase()}:${reserve}`;
24
+ }
25
+ /** An unknown thrown value, as something a log line can carry. */
26
+ function describe(e) {
27
+ return e instanceof Error ? e.message : String(e);
28
+ }
29
+ /**
9
30
  * The price feeds of one chain: the PriceFeedStore as the backend has indexed
10
31
  * it, and the prices behind it.
11
32
  *
12
33
  * ```ts
13
34
  * const store = await permissionless.oracles.store({ chainId: 1 });
14
- * const { prices } = await permissionless.oracles.prices({ chainId: 1 });
35
+ * const prices = await permissionless.oracles.getPrices(
36
+ * store.priceFeeds.map(({ address }) => ({ chainId: 1, address })),
37
+ * );
15
38
  * ```
16
39
  *
40
+ * The backend answers what it has indexed; anything that is only true at a
41
+ * block — a price, a market cap, a quota in one currency, the period an
42
+ * oracle holds for a slot — is read here, off the chain, and folded into what
43
+ * it answered. That is why these reads mix a request with a multicall rather
44
+ * than being either one.
45
+ *
17
46
  * Read-only. What the store carries is decided on chain, so putting a feed
18
47
  * there is a batch the instance owner executes — see
19
48
  * {@link InstanceOwnerTransactions} — not a row a client writes here.
@@ -29,53 +58,440 @@ var PermissionlessOracles = class extends require_offchain_AbstractOffchainNames
29
58
  }
30
59
  /**
31
60
  * The store with the assets and feeds of one chain.
61
+ *
62
+ * {@link stores} narrowed to one and unwrapped, which is the whole of it:
63
+ * there is one route either way, and a chain the backend does not serve has
64
+ * no store to answer with rather than an empty one.
65
+ *
66
+ * @throws When the backend serves no store for the chain.
32
67
  **/
33
68
  async store({ chainId }) {
69
+ const [store] = await this.stores({ chainId });
70
+ if (!store) throw new Error(`no PriceFeedStore served for chain ${chainId}`);
71
+ return store;
72
+ }
73
+ /**
74
+ * The same stores for every chain the backend serves.
75
+ *
76
+ * One request rather than {@link store} per chain, which is what a page
77
+ * showing all of them would otherwise do: the backend assembles them in the
78
+ * same few queries it assembles one in, so the chains cost little more than
79
+ * the largest of them.
80
+ *
81
+ * Each entry names its own chain, so a store is never attributed by
82
+ * position. `chainId` narrows it to one, for a caller that wants the list
83
+ * shape either way.
84
+ *
85
+ * ```ts
86
+ * const stores = await permissionless.oracles.stores();
87
+ * ```
88
+ **/
89
+ async stores({ chainId } = {}) {
34
90
  return this.getData({
35
- path: `/pfs/${chainId}`,
36
- schema: require_permissionless_api_oracles_schemas.priceFeedStoreSchema
91
+ path: "/pfs/stores",
92
+ query: { chainId: chainId?.toString() },
93
+ schema: require_permissionless_api_oracles_schemas.priceFeedStoresSchema
94
+ });
95
+ }
96
+ /**
97
+ * One feed of the store: what it is, which assets it may price, and which
98
+ * market slots it currently fills.
99
+ *
100
+ * Singular where {@link store} is plural, and not a filter over it: the
101
+ * store answers what a list renders, while this answers what a feed is used
102
+ * for, which the store does not carry at all.
103
+ *
104
+ * ```ts
105
+ * const { priceFeed, markets } = await permissionless.oracles.feed({
106
+ * chainId: 1,
107
+ * address: feed,
108
+ * });
109
+ * ```
110
+ *
111
+ * Each market row also carries the staleness period that market's own
112
+ * oracle holds for the slot, read off the chain here — see
113
+ * {@link PermissionlessPriceFeedMarket.marketStalenessPeriod}.
114
+ *
115
+ * @throws When the store does not carry the feed. A private draft is not
116
+ * here — it lives in whichever app wrote it.
117
+ **/
118
+ async feed({ chainId, address }) {
119
+ const feed = address.toLowerCase();
120
+ const indexed = await this.getData({
121
+ path: `/pfs/${chainId}/price-feed/${address}`,
122
+ schema: require_permissionless_api_oracles_schemas.priceFeedDetailsSchema
37
123
  });
124
+ const periods = await this.#stalenessPeriods(chainId, feed, indexed.markets);
125
+ return {
126
+ ...indexed,
127
+ markets: indexed.markets.map((market) => ({
128
+ ...market,
129
+ marketStalenessPeriod: periods.get(slotKey(market.priceOracle, market.token, market.reserve)) ?? null
130
+ }))
131
+ };
38
132
  }
39
133
  /**
40
- * A price per feed, and a market cap per feed and asset. The feeds the
41
- * indexer has not seen are read off the chain by the backend, so the store
42
- * is priced whole.
134
+ * Staleness period each market's own oracle holds for the feed's slots,
135
+ * keyed by {@link slotKey}.
136
+ *
137
+ * One call per oracle rather than per slot: the compressor answers for a
138
+ * whole oracle at once, and a feed in a dozen markets of the same curator
139
+ * would otherwise pay a round trip per token. Scoped to the tokens actually
140
+ * asked about, since the unscoped overload returns the oracle's entire feed
141
+ * tree as well.
142
+ *
143
+ * Failures are warnings and not errors. A slot missing from the answer keeps
144
+ * its `null`, which the caller reports as "the oracle would not say" — the
145
+ * alternative is failing a read whose indexed half is perfectly good.
146
+ */
147
+ async #stalenessPeriods(chainId, feed, rows) {
148
+ const periods = /* @__PURE__ */ new Map();
149
+ if (rows.length === 0) return periods;
150
+ const tokensByOracle = /* @__PURE__ */ new Map();
151
+ for (const row of rows) {
152
+ let tokens = tokensByOracle.get(row.priceOracle);
153
+ if (!tokens) {
154
+ tokens = /* @__PURE__ */ new Set();
155
+ tokensByOracle.set(row.priceOracle, tokens);
156
+ }
157
+ tokens.add(row.token);
158
+ }
159
+ let client;
160
+ let compressor;
161
+ try {
162
+ client = this.#chains.client(chainId);
163
+ compressor = await new require_permissionless_bindings_address_provider.AddressProviderContract("0xF7f0a609BfAb9a0A98786951ef10e5FE26cC1E38", client).getLatestVersionAddress(require_onchain_constants_address_provider.AP_PRICE_FEED_COMPRESSOR);
164
+ } catch (e) {
165
+ this.logger?.warn(`no PriceFeedCompressor for chain ${chainId}, reporting no market staleness periods: ${describe(e)}`);
166
+ return periods;
167
+ }
168
+ const oracles = [...tokensByOracle.keys()];
169
+ const results = await client.multicall({
170
+ contracts: oracles.map((oracle) => ({
171
+ address: compressor,
172
+ abi: require_abi_compressors_priceFeedCompressor.priceFeedCompressorAbi,
173
+ functionName: "getPriceOracleState",
174
+ args: [oracle, [...tokensByOracle.get(oracle) ?? []]]
175
+ })),
176
+ allowFailure: true
177
+ });
178
+ oracles.forEach((oracle, i) => {
179
+ const result = results[i];
180
+ if (result?.status !== "success") {
181
+ this.logger?.warn(`price oracle ${oracle} of chain ${chainId} would not report its feed map`);
182
+ return;
183
+ }
184
+ const { priceFeedMap } = result.result;
185
+ for (const entry of priceFeedMap) {
186
+ if (entry.priceFeed.toLowerCase() !== feed) continue;
187
+ periods.set(slotKey(oracle, entry.token, entry.reserve), Number(entry.stalenessPeriod));
188
+ }
189
+ });
190
+ return periods;
191
+ }
192
+ /**
193
+ * One asset of the store: the feeds allowed for it, and the markets that
194
+ * take it as collateral.
195
+ *
196
+ * The mirror of {@link feed}, and the call to reach for when the question is
197
+ * "what prices this token and what is it backing" rather than "where is this
198
+ * feed used". Both read the same market slots; which end they are indexed
199
+ * from is the difference.
200
+ *
201
+ * ```ts
202
+ * const { priceFeeds, markets, quotaUsedUsd } =
203
+ * await permissionless.oracles.asset({ chainId: 1, address: token });
204
+ * ```
205
+ *
206
+ * The quota totals are worked out here, over the very market rows this
207
+ * answers with — see {@link PermissionlessQuotaTotals}.
208
+ *
209
+ * @throws When the chain has no metadata for the address. An asset nothing
210
+ * was indexed for has no symbol or decimals, so there is no row to answer.
43
211
  **/
44
- async prices({ chainId }) {
45
- return this.getData({
46
- path: `/pfs/${chainId}/prices`,
47
- schema: require_permissionless_api_oracles_schemas.pricesSchema
212
+ async asset({ chainId, address }) {
213
+ const indexed = await this.getData({
214
+ path: `/pfs/${chainId}/asset/${address}`,
215
+ schema: require_permissionless_api_oracles_schemas.assetDetailsSchema
48
216
  });
217
+ const priceOf = await this.#underlyingPrices(indexed.markets.map((market) => ({
218
+ chainId,
219
+ underlyingPriceFeed: market.underlyingPriceFeed
220
+ })));
221
+ return {
222
+ ...indexed,
223
+ ...require_permissionless_api_oracles_enrich.totalQuotas(indexed.markets, (feed) => priceOf(chainId, feed))
224
+ };
225
+ }
226
+ /**
227
+ * The price of every underlying the given quota rows name, looked up by
228
+ * chain and feed.
229
+ *
230
+ * Distinct feeds only, because an asset in a dozen markets of the same
231
+ * underlying names the same feed a dozen times, and a list of every chain's
232
+ * assets names a handful of feeds per chain between them all.
233
+ */
234
+ async #underlyingPrices(rows) {
235
+ const refs = /* @__PURE__ */ new Map();
236
+ for (const { chainId, underlyingPriceFeed } of rows) {
237
+ if (underlyingPriceFeed === null) continue;
238
+ const address = underlyingPriceFeed.toLowerCase();
239
+ refs.set(require_permissionless_api_oracles_enrich.feedKey(chainId, address), {
240
+ chainId,
241
+ address
242
+ });
243
+ }
244
+ const prices = require_permissionless_api_oracles_enrich.pricesByFeed(await this.getPrices([...refs.values()]));
245
+ return (chainId, feed) => prices.get(require_permissionless_api_oracles_enrich.feedKey(chainId, feed));
49
246
  }
50
247
  /**
51
- * The price each of the named feeds reports, read off the chain.
248
+ * Every asset the store carries, with what its markets make of it.
249
+ *
250
+ * Enough on its own to render a list of assets: the alternative is the
251
+ * store's whole read for the names plus a call per row for the quotas, and
252
+ * this is one request either way you scope it.
52
253
  *
53
- * Where {@link prices} answers for the store as a whole and at whatever
54
- * moment the backend priced it, this prices exactly the feeds asked for, at
55
- * the current block. A feed the store does not carry prices the same way,
56
- * which is what makes this the call to reach for after a deploy, or while a
57
- * form is still choosing between candidates.
254
+ * Ordered by how central each asset is — the markets it underlies first,
255
+ * then what is quoted against it, then what it is allowed — so a consumer
256
+ * renders the rows as they arrive. Omit `chainId` for every chain the
257
+ * backend serves, ordered the same way across all of them.
58
258
  *
59
- * Feeds needing a fresh answer before they can be read are updated inside
60
- * the same simulation, so a push-based feed prices as readily as a
61
- * pull-based one. `null` is a feed whose read reverted, not a zero.
259
+ * Each row also carries its quota totals and what this SDK's tables say the
260
+ * asset is, neither of which is served — see
261
+ * {@link PermissionlessAssetSummary}. The totals are what the last two terms
262
+ * of the order are over, so this waits for them rather than handing back a
263
+ * list that would reshuffle a moment later.
62
264
  *
63
265
  * ```ts
64
- * const prices = await permissionless.oracles.getPrices({
266
+ * const assets = await permissionless.oracles.assets({ chainId: 1 });
267
+ * const everywhere = await permissionless.oracles.assets();
268
+ * ```
269
+ **/
270
+ async assets({ chainId } = {}) {
271
+ const indexed = await this.getData({
272
+ path: "/pfs/assets",
273
+ query: { chainId: chainId?.toString() },
274
+ schema: require_permissionless_api_oracles_schemas.assetSummariesSchema
275
+ });
276
+ const networks = /* @__PURE__ */ new Map();
277
+ for (const { chainId: id } of indexed) if (!networks.has(id)) networks.set(id, require_onchain_chain_chains.isSupportedNetwork(id) ? require_onchain_chain_chains.getNetworkType(id) : void 0);
278
+ const priceOf = await this.#underlyingPrices(indexed.flatMap((asset) => asset.marketQuotas.map((quota) => ({
279
+ chainId: asset.chainId,
280
+ underlyingPriceFeed: quota.underlyingPriceFeed
281
+ }))));
282
+ return indexed.map((asset) => {
283
+ const network = networks.get(asset.chainId);
284
+ return {
285
+ ...asset,
286
+ ...require_permissionless_api_oracles_enrich.totalQuotas(asset.marketQuotas, (feed) => priceOf(asset.chainId, feed)),
287
+ assetType: network === void 0 ? null : require_onchain_chain_chains.getAssetType(asset.address, network) ?? null,
288
+ isRwa: network === void 0 ? false : require_onchain_chain_chains.isRWAToken(asset.address, network)
289
+ };
290
+ }).sort(require_permissionless_api_oracles_enrich.byCentrality);
291
+ }
292
+ /**
293
+ * The feeds one feed reads from, recursively.
294
+ *
295
+ * Answers for any feed the chain has indexed, store member or not: a
296
+ * composite feed's children are rarely in the store themselves, and a tree
297
+ * that stopped at the first of them would not be one.
298
+ *
299
+ * ```ts
300
+ * const tree = await permissionless.oracles.dependencies({
65
301
  * chainId: 1,
66
- * priceFeeds: [feed],
302
+ * address: feed,
67
303
  * });
68
304
  * ```
305
+ **/
306
+ async dependencies({ chainId, address }) {
307
+ return this.getData({
308
+ path: `/pfs/${chainId}/price-feed/${address}/graph`,
309
+ schema: require_model_opportunities_schema.priceFeedDataSchema
310
+ });
311
+ }
312
+ /**
313
+ * What the named feeds price, read off the chain.
314
+ *
315
+ * Prices exactly what is asked for, at the current block, across as many
316
+ * chains as the list names — one read per chain, so a screen listing every
317
+ * chain's feeds pays per chain and not per row. A feed the store does not
318
+ * carry prices the same way, which is what makes this the call to reach for
319
+ * after a deploy, or while a form is still choosing between candidates.
320
+ *
321
+ * Read in two passes. The first asks every feed what it reports as it
322
+ * stands, which is one multicall per chain; a feed that will not answer
323
+ * until it has been given a fresh one is then priced inside a simulation
324
+ * that updates it first, so a push-based feed prices as readily as a
325
+ * pull-based one. The second pass costs an `OnchainSDK` to attach, hence
326
+ * only the feeds that need it rather than all of them.
327
+ *
328
+ * `null` is a feed neither pass could read, and a chain with no client
329
+ * configured is every one of its feeds reading `null` rather than a failed
330
+ * call — the figures built on them are floors either way.
331
+ *
332
+ * ```ts
333
+ * const [{ price }] = await permissionless.oracles.getPrices([
334
+ * { chainId: 1, address: feed },
335
+ * ]);
336
+ * ```
69
337
  *
70
- * @returns A price per feed, in the 8 decimals every Gearbox feed reports.
338
+ * @returns One entry per feed asked for, in the order asked, each price in
339
+ * the 8 decimals every Gearbox feed reports.
71
340
  **/
72
- async getPrices({ chainId, priceFeeds }) {
73
- return require_permissionless_utils_price_update_get_prices.getPrices({
74
- client: this.#chains.client(chainId),
75
- priceFeeds,
76
- chunkSize: 200,
77
- gasLimit: GET_PRICES_GAS_LIMIT_BY_CHAIN_ID[chainId]
341
+ async getPrices(feeds) {
342
+ const byChain = /* @__PURE__ */ new Map();
343
+ for (const { chainId, address } of feeds) {
344
+ const addresses = byChain.get(chainId) ?? /* @__PURE__ */ new Set();
345
+ addresses.add(address.toLowerCase());
346
+ byChain.set(chainId, addresses);
347
+ }
348
+ const chainIds = [...byChain.keys()];
349
+ const results = await Promise.all(chainIds.map((chainId) => this.#pricesOf(chainId, [...byChain.get(chainId) ?? []])));
350
+ const prices = new Map(chainIds.map((id, i) => [id, results[i] ?? {}]));
351
+ return feeds.map(({ chainId, address }) => ({
352
+ chainId,
353
+ address,
354
+ price: prices.get(chainId)?.[address.toLowerCase()] ?? null
355
+ }));
356
+ }
357
+ /** One chain's half of {@link getPrices}: the cheap pass, then the rest. */
358
+ async #pricesOf(chainId, priceFeeds) {
359
+ let client;
360
+ try {
361
+ client = this.#chains.client(chainId);
362
+ } catch (e) {
363
+ this.logger?.warn(`chain ${chainId} has no client, reporting its ${priceFeeds.length} feed(s) unpriced: ${describe(e)}`);
364
+ return {};
365
+ }
366
+ let prices;
367
+ try {
368
+ prices = await require_permissionless_utils_price_update_get_prices.getLatestPrices({
369
+ client,
370
+ priceFeeds
371
+ });
372
+ } catch (e) {
373
+ this.logger?.warn(`chain ${chainId} would not answer for its ${priceFeeds.length} feed(s), trying each with an update: ${describe(e)}`);
374
+ prices = {};
375
+ }
376
+ const stale = priceFeeds.filter((feed) => (prices[feed] ?? null) === null);
377
+ if (stale.length === 0) return prices;
378
+ try {
379
+ return {
380
+ ...prices,
381
+ ...await require_permissionless_utils_price_update_get_prices.getPrices({
382
+ client,
383
+ priceFeeds: stale,
384
+ chunkSize: 200,
385
+ gasLimit: GET_PRICES_GAS_LIMIT_BY_CHAIN_ID[chainId]
386
+ })
387
+ };
388
+ } catch (e) {
389
+ this.logger?.warn(`chain ${chainId} would not price ${stale.length} of its feed(s) even with an update, reporting them unpriced: ${describe(e)}`);
390
+ return prices;
391
+ }
392
+ }
393
+ /**
394
+ * What one chain's feeds value one chain's assets at: `price × totalSupply`
395
+ * per pair.
396
+ *
397
+ * Asked from whichever side the caller is looking at — one asset through
398
+ * each of its feeds, or one feed over each of the assets it prices — because
399
+ * a screen shows one row per feed or one row per asset, never the full
400
+ * cross product. Both answer the same pairs in the same shape.
401
+ *
402
+ * Two reads per chain: the prices, and one multicall for the assets'
403
+ * supplies and decimals. Several scopes can be asked at once, which is how a
404
+ * table of every asset's feeds pays per chain rather than per row; the pairs
405
+ * come back in the order the scopes named them. A pair neither read could
406
+ * resolve has a `null` cap, which is not a zero.
407
+ *
408
+ * ```ts
409
+ * const caps = await permissionless.oracles.getMarketCap({
410
+ * chainId: 1,
411
+ * asset: token,
412
+ * priceFeeds: [main, reserve],
413
+ * });
414
+ * ```
415
+ **/
416
+ async getMarketCap(scope) {
417
+ const pairs = (Array.isArray(scope) ? scope : [scope]).flatMap((one) => "asset" in one ? one.priceFeeds.map((priceFeed) => ({
418
+ chainId: one.chainId,
419
+ priceFeed: priceFeed.toLowerCase(),
420
+ asset: one.asset.toLowerCase()
421
+ })) : one.assets.map((asset) => ({
422
+ chainId: one.chainId,
423
+ priceFeed: one.priceFeed.toLowerCase(),
424
+ asset: asset.toLowerCase()
425
+ })));
426
+ if (pairs.length === 0) return [];
427
+ const feeds = /* @__PURE__ */ new Map();
428
+ const assetsByChain = /* @__PURE__ */ new Map();
429
+ for (const { chainId, priceFeed, asset } of pairs) {
430
+ feeds.set(require_permissionless_api_oracles_enrich.feedKey(chainId, priceFeed), {
431
+ chainId,
432
+ address: priceFeed
433
+ });
434
+ const assets = assetsByChain.get(chainId) ?? /* @__PURE__ */ new Set();
435
+ assets.add(asset);
436
+ assetsByChain.set(chainId, assets);
437
+ }
438
+ const chainIds = [...assetsByChain.keys()];
439
+ const [prices, perChain] = await Promise.all([this.getPrices([...feeds.values()]), Promise.all(chainIds.map((chainId) => this.#supplies(chainId, [...assetsByChain.get(chainId) ?? []])))]);
440
+ const priced = require_permissionless_api_oracles_enrich.pricesByFeed(prices);
441
+ const supplies = new Map(chainIds.map((id, i) => [id, perChain[i]]));
442
+ return pairs.map(({ chainId, priceFeed, asset }) => {
443
+ const supply = supplies.get(chainId)?.get(asset);
444
+ return {
445
+ chainId,
446
+ priceFeed,
447
+ asset,
448
+ cap: supply === void 0 ? null : require_permissionless_api_oracles_enrich.toCap(priced.get(require_permissionless_api_oracles_enrich.feedKey(chainId, priceFeed)), supply.totalSupply, supply.decimals)
449
+ };
450
+ });
451
+ }
452
+ /**
453
+ * Each asset's supply and the decimals it is counted in, which is what turns
454
+ * a supply into an amount.
455
+ *
456
+ * Both read rather than one of them taken from the index, so that a cap can
457
+ * be asked for an address nothing was indexed for at all — a token a form is
458
+ * still choosing, say. An asset missing from the answer is left out, and
459
+ * caps over it come back `null`.
460
+ */
461
+ async #supplies(chainId, assets) {
462
+ const supplies = /* @__PURE__ */ new Map();
463
+ let client;
464
+ try {
465
+ client = this.#chains.client(chainId);
466
+ } catch (e) {
467
+ this.logger?.warn(`chain ${chainId} has no client, reporting no market caps: ${describe(e)}`);
468
+ return supplies;
469
+ }
470
+ const results = await client.multicall({
471
+ contracts: assets.flatMap((address) => [{
472
+ address,
473
+ abi: viem.erc20Abi,
474
+ functionName: "totalSupply"
475
+ }, {
476
+ address,
477
+ abi: viem.erc20Abi,
478
+ functionName: "decimals"
479
+ }]),
480
+ allowFailure: true
481
+ });
482
+ assets.forEach((address, i) => {
483
+ const supply = results[2 * i];
484
+ const decimals = results[2 * i + 1];
485
+ if (decimals?.status !== "success") {
486
+ this.logger?.warn(`asset ${address} of chain ${chainId} would not report its decimals, reporting no market cap for it`);
487
+ return;
488
+ }
489
+ supplies.set(address, {
490
+ totalSupply: supply?.status === "success" ? supply.result : null,
491
+ decimals: Number(decimals.result)
492
+ });
78
493
  });
494
+ return supplies;
79
495
  }
80
496
  };
81
497
  //#endregion
@@ -0,0 +1,99 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let viem = require("viem");
3
+ //#region src/permissionless/api/oracles/enrich.ts
4
+ /**
5
+ * What the oracles namespace works out for itself once the backend has
6
+ * answered.
7
+ *
8
+ * All of it used to be the backend's, and all of it needs the chain: a quota
9
+ * is denominated in its market's own underlying and a market cap is a supply
10
+ * times a price, so none of these can be indexed — they can only be read at
11
+ * the block the reader is on. Keeping them here means one description of each
12
+ * rule rather than one per service that wants it, and a backend that holds no
13
+ * viem client at all.
14
+ **/
15
+ /** Identifies a feed across chains, since an address alone does not. */
16
+ function feedKey(chainId, address) {
17
+ return `${chainId}:${address.toLowerCase()}`;
18
+ }
19
+ /** Prices looked up the way {@link totalQuotas} and the caps need them. */
20
+ function pricesByFeed(prices) {
21
+ return new Map(prices.map(({ chainId, address, price }) => [feedKey(chainId, address), price]));
22
+ }
23
+ /**
24
+ * The asset's quota across its markets, totalled in USD.
25
+ *
26
+ * Each market's figures are converted at the price that market's own oracle
27
+ * holds for its underlying, which is the same number that market's accounting
28
+ * uses.
29
+ *
30
+ * Limits count only the markets still running, because a shut down market's
31
+ * limit is not borrowable; what is already quoted counts everywhere, because
32
+ * debt outstanding is exposure either way. A market whose underlying could not
33
+ * be priced is skipped, which makes both figures floors, and `null` is nothing
34
+ * to total rather than a zero.
35
+ *
36
+ * @param priceOf - The price each underlying feed reports, in the 8 decimals
37
+ * every Gearbox feed uses.
38
+ */
39
+ function totalQuotas(quotas, priceOf) {
40
+ let limit = 0;
41
+ let used = 0;
42
+ let anyLimit = false;
43
+ let anyUsed = false;
44
+ for (const quota of quotas) {
45
+ const raw = quota.underlyingPriceFeed === null ? null : priceOf(quota.underlyingPriceFeed);
46
+ if (raw === null || raw === void 0) continue;
47
+ const price = Number((0, viem.formatUnits)(raw, 8));
48
+ if (quota.quotaUsed !== null) {
49
+ used += quota.quotaUsed * price;
50
+ anyUsed = true;
51
+ }
52
+ if (quota.quotaLimit !== null && !quota.isShutdown) {
53
+ limit += quota.quotaLimit * price;
54
+ anyLimit = true;
55
+ }
56
+ }
57
+ return {
58
+ quotaLimitUsd: anyLimit ? limit : null,
59
+ quotaUsedUsd: anyUsed ? used : null
60
+ };
61
+ }
62
+ /**
63
+ * What one feed values one asset's supply at: `price × totalSupply`.
64
+ *
65
+ * Multiplied as integers and formatted once, rather than through two floats:
66
+ * an 8-decimal price times an 18-decimal supply loses its small end to a JS
67
+ * number long before it loses the large one, and the figure is a total rather
68
+ * than an estimate.
69
+ *
70
+ * `null` if either side could not be read — an asset nobody could price is
71
+ * not an asset worth nothing.
72
+ */
73
+ function toCap(price, totalSupply, decimals) {
74
+ if (price === null || price === void 0 || totalSupply === null || totalSupply === void 0) return null;
75
+ return (0, viem.formatUnits)(price * totalSupply, 8 + decimals);
76
+ }
77
+ /** Larger first, with `null` after every figure including a real zero. */
78
+ function descending(a, b) {
79
+ return (b ?? -1) - (a ?? -1);
80
+ }
81
+ /**
82
+ * Assets ordered the way a reader scans them: what the protocol is built on
83
+ * first, then what is actually borrowed against it, then what it is merely
84
+ * allowed to be borrowed against.
85
+ *
86
+ * Applied here and not by the backend because the two middle terms are the
87
+ * converted quotas, which only exist once the chain has been read. The
88
+ * backend orders by the first term and the tie-breakers, so a caller reading
89
+ * the route directly still gets a stable order; this refines it.
90
+ */
91
+ function byCentrality(a, b) {
92
+ return b.underlyingMarketCount - a.underlyingMarketCount || descending(a.quotaUsedUsd, b.quotaUsedUsd) || descending(a.quotaLimitUsd, b.quotaLimitUsd) || a.symbol.localeCompare(b.symbol) || a.address.localeCompare(b.address) || a.chainId - b.chainId;
93
+ }
94
+ //#endregion
95
+ exports.byCentrality = byCentrality;
96
+ exports.feedKey = feedKey;
97
+ exports.pricesByFeed = pricesByFeed;
98
+ exports.toCap = toCap;
99
+ exports.totalQuotas = totalQuotas;