@gearbox-protocol/sdk 18.3.1 → 18.4.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/dist/cjs/abi/310/iKillSwitchProxy.js +130 -0
- package/dist/cjs/onchain/index.js +18 -0
- package/dist/cjs/onchain/market/adapters/abi/adapters/iTreehouseFastlaneAdapter.js +113 -0
- package/dist/cjs/onchain/market/adapters/abi/adapters/iTreehouseRedemptionGatewayAdapter.js +258 -0
- package/dist/cjs/onchain/market/adapters/abi/adapters/iTreehouseRouterAdapter.js +161 -0
- package/dist/cjs/onchain/market/adapters/abi/adapters/index.js +6 -0
- package/dist/cjs/onchain/market/adapters/abi/index.js +13 -0
- package/dist/cjs/onchain/market/adapters/abi/treehouse/iTreehouseFastlane.js +39 -0
- package/dist/cjs/onchain/market/adapters/abi/treehouse/iTreehouseRedemptionGateway.js +230 -0
- package/dist/cjs/onchain/market/adapters/abi/treehouse/iTreehouseRouter.js +29 -0
- package/dist/cjs/onchain/market/adapters/abi/treehouse/index.js +7 -0
- package/dist/cjs/onchain/market/adapters/contracts/TreehouseFastlaneAdapterContract.js +72 -0
- package/dist/cjs/onchain/market/adapters/contracts/TreehouseRedemptionGatewayAdapterContract.js +117 -0
- package/dist/cjs/onchain/market/adapters/contracts/TreehouseRouterAdapterContract.js +61 -0
- package/dist/cjs/onchain/market/adapters/contracts/index.js +6 -0
- package/dist/cjs/onchain/market/adapters/createAdapter.js +6 -0
- package/dist/cjs/onchain/market/adapters/index.js +18 -0
- package/dist/cjs/onchain/market/index.js +18 -0
- package/dist/cjs/permissionless/api/index.js +31 -0
- package/dist/cjs/permissionless/api/oracles/PermissionlessOracles.js +637 -65
- package/dist/cjs/permissionless/api/oracles/enrich.js +99 -0
- package/dist/cjs/permissionless/api/oracles/index.js +33 -0
- package/dist/cjs/permissionless/api/oracles/onchain-feed.js +25 -0
- package/dist/cjs/permissionless/api/oracles/schemas.js +84 -59
- package/dist/cjs/permissionless/api/oracles/score/formula.js +56 -0
- package/dist/cjs/permissionless/api/oracles/score/index.js +11 -0
- package/dist/cjs/permissionless/api/oracles/score/run.js +252 -0
- package/dist/cjs/permissionless/api/oracles/score/types.js +1 -0
- package/dist/cjs/permissionless/api/oracles/stress/classify.js +258 -0
- package/dist/cjs/permissionless/api/oracles/stress/index.js +24 -0
- package/dist/cjs/permissionless/api/oracles/stress/override.js +125 -0
- package/dist/cjs/permissionless/api/oracles/stress/run.js +485 -0
- package/dist/cjs/permissionless/api/oracles/stress/score.js +98 -0
- package/dist/cjs/permissionless/api/oracles/stress/types.js +1 -0
- package/dist/cjs/permissionless/api/schemas.js +0 -3
- package/dist/cjs/permissionless/api/transactions/InstanceOwnerTransactions.js +14 -3
- package/dist/cjs/permissionless/api/transactions/lp-price-feeds.js +10 -4
- package/dist/cjs/permissionless/bindings/bytecode-repository.js +8 -0
- package/dist/cjs/permissionless/bindings/governance/governor.js +1 -1
- package/dist/cjs/permissionless/bindings/index.js +8 -3
- package/dist/cjs/permissionless/bindings/kill-switch-proxy.js +188 -0
- package/dist/cjs/permissionless/bindings/market-configurator-factory.js +39 -1
- package/dist/cjs/permissionless/bindings/market-configurator.js +142 -0
- package/dist/cjs/permissionless/index.js +42 -3
- package/dist/cjs/permissionless/utils/index.js +3 -0
- package/dist/cjs/permissionless/utils/literals.js +2 -0
- package/dist/cjs/permissionless/utils/price-update/get-prices.js +64 -0
- package/dist/cjs/permissionless/utils/price-update/index.js +2 -0
- package/dist/cjs/testnets/TestnetsAPI.js +179 -0
- package/dist/cjs/testnets/errors/TestnetNotFoundError.js +15 -0
- package/dist/cjs/testnets/errors/TestnetRequestFailedError.js +24 -0
- package/dist/cjs/testnets/errors/TestnetStatusError.js +19 -0
- package/dist/cjs/testnets/errors/TestnetValidationError.js +24 -0
- package/dist/cjs/testnets/errors/TestnetWaitTimeoutError.js +30 -0
- package/dist/cjs/testnets/errors/TestnetsError.js +34 -0
- package/dist/cjs/testnets/errors/index.js +13 -0
- package/dist/cjs/testnets/index.js +32 -0
- package/dist/cjs/testnets/package.json +1 -0
- package/dist/cjs/testnets/schemas.js +242 -0
- package/dist/cjs/testnets/types.js +1 -0
- package/dist/esm/abi/310/iKillSwitchProxy.js +129 -0
- package/dist/esm/onchain/index.js +10 -1
- package/dist/esm/onchain/market/adapters/abi/adapters/iTreehouseFastlaneAdapter.js +112 -0
- package/dist/esm/onchain/market/adapters/abi/adapters/iTreehouseRedemptionGatewayAdapter.js +257 -0
- package/dist/esm/onchain/market/adapters/abi/adapters/iTreehouseRouterAdapter.js +160 -0
- package/dist/esm/onchain/market/adapters/abi/adapters/index.js +4 -1
- package/dist/esm/onchain/market/adapters/abi/index.js +8 -1
- package/dist/esm/onchain/market/adapters/abi/treehouse/iTreehouseFastlane.js +38 -0
- package/dist/esm/onchain/market/adapters/abi/treehouse/iTreehouseRedemptionGateway.js +229 -0
- package/dist/esm/onchain/market/adapters/abi/treehouse/iTreehouseRouter.js +28 -0
- package/dist/esm/onchain/market/adapters/abi/treehouse/index.js +4 -0
- package/dist/esm/onchain/market/adapters/contracts/TreehouseFastlaneAdapterContract.js +71 -0
- package/dist/esm/onchain/market/adapters/contracts/TreehouseRedemptionGatewayAdapterContract.js +116 -0
- package/dist/esm/onchain/market/adapters/contracts/TreehouseRouterAdapterContract.js +60 -0
- package/dist/esm/onchain/market/adapters/contracts/index.js +4 -1
- package/dist/esm/onchain/market/adapters/createAdapter.js +6 -0
- package/dist/esm/onchain/market/adapters/index.js +10 -1
- package/dist/esm/onchain/market/index.js +10 -1
- package/dist/esm/permissionless/api/index.js +7 -1
- package/dist/esm/permissionless/api/oracles/PermissionlessOracles.js +639 -67
- package/dist/esm/permissionless/api/oracles/enrich.js +94 -0
- package/dist/esm/permissionless/api/oracles/index.js +10 -1
- package/dist/esm/permissionless/api/oracles/onchain-feed.js +24 -0
- package/dist/esm/permissionless/api/oracles/schemas.js +80 -58
- package/dist/esm/permissionless/api/oracles/score/formula.js +51 -0
- package/dist/esm/permissionless/api/oracles/score/index.js +4 -0
- package/dist/esm/permissionless/api/oracles/score/run.js +250 -0
- package/dist/esm/permissionless/api/oracles/score/types.js +1 -0
- package/dist/esm/permissionless/api/oracles/stress/classify.js +256 -0
- package/dist/esm/permissionless/api/oracles/stress/index.js +6 -0
- package/dist/esm/permissionless/api/oracles/stress/override.js +119 -0
- package/dist/esm/permissionless/api/oracles/stress/run.js +478 -0
- package/dist/esm/permissionless/api/oracles/stress/score.js +95 -0
- package/dist/esm/permissionless/api/oracles/stress/types.js +1 -0
- package/dist/esm/permissionless/api/schemas.js +2 -4
- package/dist/esm/permissionless/api/transactions/InstanceOwnerTransactions.js +14 -3
- package/dist/esm/permissionless/api/transactions/lp-price-feeds.js +10 -4
- package/dist/esm/permissionless/bindings/bytecode-repository.js +8 -0
- package/dist/esm/permissionless/bindings/governance/governor.js +1 -1
- package/dist/esm/permissionless/bindings/index.js +5 -4
- package/dist/esm/permissionless/bindings/kill-switch-proxy.js +184 -0
- package/dist/esm/permissionless/bindings/market-configurator-factory.js +37 -0
- package/dist/esm/permissionless/bindings/market-configurator.js +143 -1
- package/dist/esm/permissionless/index.js +13 -6
- package/dist/esm/permissionless/utils/index.js +3 -3
- package/dist/esm/permissionless/utils/literals.js +2 -1
- package/dist/esm/permissionless/utils/price-update/get-prices.js +63 -1
- package/dist/esm/permissionless/utils/price-update/index.js +2 -2
- package/dist/esm/testnets/TestnetsAPI.js +178 -0
- package/dist/esm/testnets/errors/TestnetNotFoundError.js +14 -0
- package/dist/esm/testnets/errors/TestnetRequestFailedError.js +23 -0
- package/dist/esm/testnets/errors/TestnetStatusError.js +18 -0
- package/dist/esm/testnets/errors/TestnetValidationError.js +23 -0
- package/dist/esm/testnets/errors/TestnetWaitTimeoutError.js +29 -0
- package/dist/esm/testnets/errors/TestnetsError.js +33 -0
- package/dist/esm/testnets/errors/index.js +7 -0
- package/dist/esm/testnets/index.js +11 -0
- package/dist/esm/testnets/package.json +1 -0
- package/dist/esm/testnets/schemas.js +228 -0
- package/dist/esm/testnets/types.js +1 -0
- package/dist/types/abi/310/iKillSwitchProxy.d.ts +114 -0
- package/dist/types/onchain/index.d.ts +10 -1
- package/dist/types/onchain/market/adapters/abi/adapters/iTreehouseFastlaneAdapter.d.ts +101 -0
- package/dist/types/onchain/market/adapters/abi/adapters/iTreehouseRedemptionGatewayAdapter.d.ts +237 -0
- package/dist/types/onchain/market/adapters/abi/adapters/iTreehouseRouterAdapter.d.ts +146 -0
- package/dist/types/onchain/market/adapters/abi/adapters/index.d.ts +4 -1
- package/dist/types/onchain/market/adapters/abi/index.d.ts +8 -1
- package/dist/types/onchain/market/adapters/abi/treehouse/iTreehouseFastlane.d.ts +33 -0
- package/dist/types/onchain/market/adapters/abi/treehouse/iTreehouseRedemptionGateway.d.ts +207 -0
- package/dist/types/onchain/market/adapters/abi/treehouse/iTreehouseRouter.d.ts +27 -0
- package/dist/types/onchain/market/adapters/abi/treehouse/index.d.ts +4 -0
- package/dist/types/onchain/market/adapters/contracts/TreehouseFastlaneAdapterContract.d.ts +156 -0
- package/dist/types/onchain/market/adapters/contracts/TreehouseRedemptionGatewayAdapterContract.d.ts +481 -0
- package/dist/types/onchain/market/adapters/contracts/TreehouseRouterAdapterContract.d.ts +193 -0
- package/dist/types/onchain/market/adapters/contracts/index.d.ts +4 -1
- package/dist/types/onchain/market/adapters/index.d.ts +10 -1
- package/dist/types/onchain/market/index.d.ts +10 -1
- package/dist/types/permissionless/api/index.d.ts +10 -2
- package/dist/types/permissionless/api/oracles/PermissionlessOracles.d.ts +237 -47
- package/dist/types/permissionless/api/oracles/enrich.d.ts +59 -0
- package/dist/types/permissionless/api/oracles/index.d.ts +12 -2
- package/dist/types/permissionless/api/oracles/onchain-feed.d.ts +15 -0
- package/dist/types/permissionless/api/oracles/schemas.d.ts +147 -41
- package/dist/types/permissionless/api/oracles/score/formula.d.ts +36 -0
- package/dist/types/permissionless/api/oracles/score/index.d.ts +4 -0
- package/dist/types/permissionless/api/oracles/score/run.d.ts +42 -0
- package/dist/types/permissionless/api/oracles/score/types.d.ts +118 -0
- package/dist/types/permissionless/api/oracles/stress/classify.d.ts +57 -0
- package/dist/types/permissionless/api/oracles/stress/index.d.ts +6 -0
- package/dist/types/permissionless/api/oracles/stress/override.d.ts +59 -0
- package/dist/types/permissionless/api/oracles/stress/run.d.ts +100 -0
- package/dist/types/permissionless/api/oracles/stress/score.d.ts +57 -0
- package/dist/types/permissionless/api/oracles/stress/types.d.ts +272 -0
- package/dist/types/permissionless/api/oracles/types.d.ts +320 -125
- package/dist/types/permissionless/api/schemas.d.ts +0 -2
- package/dist/types/permissionless/api/transactions/lp-price-feeds.d.ts +9 -1
- package/dist/types/permissionless/bindings/bytecode-repository.d.ts +10 -1
- package/dist/types/permissionless/bindings/index.d.ts +4 -3
- package/dist/types/permissionless/bindings/kill-switch-proxy.d.ts +205 -0
- package/dist/types/permissionless/bindings/market-configurator-factory.d.ts +30 -0
- package/dist/types/permissionless/bindings/market-configurator.d.ts +30 -1
- package/dist/types/permissionless/bindings/types.d.ts +27 -0
- package/dist/types/permissionless/index.d.ts +15 -6
- package/dist/types/permissionless/utils/index.d.ts +3 -3
- package/dist/types/permissionless/utils/literals.d.ts +1 -0
- package/dist/types/permissionless/utils/price-update/get-prices.d.ts +33 -0
- package/dist/types/permissionless/utils/price-update/index.d.ts +2 -2
- package/dist/types/testnets/TestnetsAPI.d.ts +108 -0
- package/dist/types/testnets/errors/TestnetNotFoundError.d.ts +12 -0
- package/dist/types/testnets/errors/TestnetRequestFailedError.d.ts +11 -0
- package/dist/types/testnets/errors/TestnetStatusError.d.ts +15 -0
- package/dist/types/testnets/errors/TestnetValidationError.d.ts +16 -0
- package/dist/types/testnets/errors/TestnetWaitTimeoutError.d.ts +21 -0
- package/dist/types/testnets/errors/TestnetsError.d.ts +47 -0
- package/dist/types/testnets/errors/index.d.ts +7 -0
- package/dist/types/testnets/index.d.ts +11 -0
- package/dist/types/testnets/schemas.d.ts +554 -0
- package/dist/types/testnets/types.d.ts +717 -0
- package/package.json +6 -1
|
@@ -1,19 +1,55 @@
|
|
|
1
|
+
import { getAssetType, getNetworkType, isRWAToken, isSupportedNetwork } from "../../../onchain/chain/chains.js";
|
|
2
|
+
import { AP_PRICE_FEED_COMPRESSOR } from "../../../onchain/constants/address-provider.js";
|
|
1
3
|
import { priceFeedDataSchema } from "../../../model/opportunities.schema.js";
|
|
4
|
+
import { priceFeedCompressorAbi } from "../../../abi/compressors/priceFeedCompressor.js";
|
|
5
|
+
import "../../../onchain/index.js";
|
|
6
|
+
import { OffchainStatusError } from "../../../offchain/errors/OffchainStatusError.js";
|
|
2
7
|
import { AbstractOffchainNamespace } from "../../../offchain/AbstractOffchainNamespace.js";
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
8
|
+
import "../../deployment/addresses.js";
|
|
9
|
+
import { AddressProviderContract } from "../../bindings/address-provider.js";
|
|
10
|
+
import { getFeedDecimals, getLatestPrices, getPrices } from "../../utils/price-update/get-prices.js";
|
|
11
|
+
import { byCentrality, feedKey, pricesByFeed, toCap, totalQuotas } from "./enrich.js";
|
|
12
|
+
import { onchainPriceFeed } from "./onchain-feed.js";
|
|
13
|
+
import { assetDetailsSchema, assetSummariesSchema, marketSlotsSchema, priceFeedDetailsSchema, priceFeedStoresSchema } from "./schemas.js";
|
|
14
|
+
import { readFeedTrees, runStressTest } from "./stress/run.js";
|
|
15
|
+
import { runAssetScores, runFeedPairScore } from "./score/run.js";
|
|
16
|
+
import { erc20Abi, zeroAddress } from "viem";
|
|
5
17
|
//#region src/permissionless/api/oracles/PermissionlessOracles.ts
|
|
6
18
|
const GET_PRICES_GAS_LIMIT_BY_CHAIN_ID = { 143: 100000000n };
|
|
7
19
|
const GET_PRICES_CHUNK_SIZE = 200;
|
|
8
20
|
/**
|
|
21
|
+
* What a market slot is: an oracle, a token, and whether it is the reserve
|
|
22
|
+
* one. Not the market — a market that has switched oracles has rows under
|
|
23
|
+
* each, and the slots are the old oracle's and the new one's separately.
|
|
24
|
+
*/
|
|
25
|
+
function slotKey(oracle, token, reserve) {
|
|
26
|
+
return `${oracle.toLowerCase()}:${token.toLowerCase()}:${reserve}`;
|
|
27
|
+
}
|
|
28
|
+
/** An unknown thrown value, as something a log line can carry. */
|
|
29
|
+
function describe(e) {
|
|
30
|
+
return e instanceof Error ? e.message : String(e);
|
|
31
|
+
}
|
|
32
|
+
/** The backend answered that it has no such feed. */
|
|
33
|
+
function isNotFound(e) {
|
|
34
|
+
return e instanceof OffchainStatusError && e.status === 404;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
9
37
|
* The price feeds of one chain: the PriceFeedStore as the backend has indexed
|
|
10
38
|
* it, and the prices behind it.
|
|
11
39
|
*
|
|
12
40
|
* ```ts
|
|
13
41
|
* const store = await permissionless.oracles.store({ chainId: 1 });
|
|
14
|
-
* const
|
|
42
|
+
* const prices = await permissionless.oracles.getPrices(
|
|
43
|
+
* store.priceFeeds.map(({ address }) => ({ chainId: 1, address })),
|
|
44
|
+
* );
|
|
15
45
|
* ```
|
|
16
46
|
*
|
|
47
|
+
* The backend answers what it has indexed; anything that is only true at a
|
|
48
|
+
* block — a price, a market cap, a quota in one currency, the period an
|
|
49
|
+
* oracle holds for a slot — is read here, off the chain, and folded into what
|
|
50
|
+
* it answered. That is why these reads mix a request with a multicall rather
|
|
51
|
+
* than being either one.
|
|
52
|
+
*
|
|
17
53
|
* Read-only. What the store carries is decided on chain, so putting a feed
|
|
18
54
|
* there is a batch the instance owner executes — see
|
|
19
55
|
* {@link InstanceOwnerTransactions} — not a row a client writes here.
|
|
@@ -29,11 +65,64 @@ var PermissionlessOracles = class extends AbstractOffchainNamespace {
|
|
|
29
65
|
}
|
|
30
66
|
/**
|
|
31
67
|
* The store with the assets and feeds of one chain.
|
|
68
|
+
*
|
|
69
|
+
* {@link stores} narrowed to one and unwrapped, which is the whole of it:
|
|
70
|
+
* there is one route either way, and a chain the backend does not serve has
|
|
71
|
+
* no store to answer with rather than an empty one.
|
|
72
|
+
*
|
|
73
|
+
* @throws When the backend serves no store for the chain.
|
|
32
74
|
**/
|
|
33
75
|
async store({ chainId }) {
|
|
76
|
+
const [store] = await this.stores({ chainId });
|
|
77
|
+
if (!store) throw new Error(`no PriceFeedStore served for chain ${chainId}`);
|
|
78
|
+
return store;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The same stores for every chain the backend serves.
|
|
82
|
+
*
|
|
83
|
+
* One request rather than {@link store} per chain, which is what a page
|
|
84
|
+
* showing all of them would otherwise do: the backend assembles them in the
|
|
85
|
+
* same few queries it assembles one in, so the chains cost little more than
|
|
86
|
+
* the largest of them.
|
|
87
|
+
*
|
|
88
|
+
* Each entry names its own chain, so a store is never attributed by
|
|
89
|
+
* position. `chainId` narrows it to one, for a caller that wants the list
|
|
90
|
+
* shape either way.
|
|
91
|
+
*
|
|
92
|
+
* ```ts
|
|
93
|
+
* const stores = await permissionless.oracles.stores();
|
|
94
|
+
* ```
|
|
95
|
+
**/
|
|
96
|
+
async stores({ chainId } = {}) {
|
|
34
97
|
return this.getData({
|
|
35
|
-
path:
|
|
36
|
-
|
|
98
|
+
path: "/pfs/stores",
|
|
99
|
+
query: { chainId: chainId?.toString() },
|
|
100
|
+
schema: priceFeedStoresSchema
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Every market slot in force, with the feed holding it and what the market
|
|
105
|
+
* has riding on it.
|
|
106
|
+
*
|
|
107
|
+
* The chain's whole slot table, which {@link feed} and {@link asset} answer
|
|
108
|
+
* one subject's slice of. What reads it this way is analysis over every
|
|
109
|
+
* market at once — which feed holds which slot, and what breaks if it
|
|
110
|
+
* lies — and asking a feed at a time would be a few hundred requests for
|
|
111
|
+
* the same rows.
|
|
112
|
+
*
|
|
113
|
+
* Served as indexed, with nothing folded in: the quotas are in each
|
|
114
|
+
* market's own underlying, as everywhere else, and pricing them is
|
|
115
|
+
* {@link getPrices} over the `underlyingPriceFeed` column.
|
|
116
|
+
*
|
|
117
|
+
* ```ts
|
|
118
|
+
* const slots = await permissionless.oracles.slots({ chainId: 1 });
|
|
119
|
+
* ```
|
|
120
|
+
**/
|
|
121
|
+
async slots({ chainId } = {}) {
|
|
122
|
+
return this.getData({
|
|
123
|
+
path: "/pfs/slots",
|
|
124
|
+
query: { chainId: chainId?.toString() },
|
|
125
|
+
schema: marketSlotsSchema
|
|
37
126
|
});
|
|
38
127
|
}
|
|
39
128
|
/**
|
|
@@ -51,14 +140,126 @@ var PermissionlessOracles = class extends AbstractOffchainNamespace {
|
|
|
51
140
|
* });
|
|
52
141
|
* ```
|
|
53
142
|
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
143
|
+
* Each market row also carries the staleness period that market's own
|
|
144
|
+
* oracle holds for the slot, read off the chain here — see
|
|
145
|
+
* {@link PermissionlessPriceFeedMarket.marketStalenessPeriod}.
|
|
146
|
+
*
|
|
147
|
+
* A feed the store does not carry is read off the chain instead, through
|
|
148
|
+
* the PriceFeedCompressor, and answered with `inStore: false`: its type,
|
|
149
|
+
* version and description are the chain's, while what only the store knows
|
|
150
|
+
* — who deployed it, its staleness period, the assets it may price and the
|
|
151
|
+
* markets it fills — is answered as unknown or empty.
|
|
152
|
+
*
|
|
153
|
+
* @throws When neither the store nor the chain knows the feed. A private
|
|
154
|
+
* draft is not here — it lives in whichever app wrote it.
|
|
56
155
|
**/
|
|
57
156
|
async feed({ chainId, address }) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
157
|
+
const feed = address.toLowerCase();
|
|
158
|
+
let indexed;
|
|
159
|
+
try {
|
|
160
|
+
indexed = await this.getData({
|
|
161
|
+
path: `/pfs/${chainId}/price-feed/${address}`,
|
|
162
|
+
schema: priceFeedDetailsSchema
|
|
163
|
+
});
|
|
164
|
+
} catch (e) {
|
|
165
|
+
const node = isNotFound(e) ? await this.#onchainNode(chainId, feed) : void 0;
|
|
166
|
+
if (!node) throw e;
|
|
167
|
+
return {
|
|
168
|
+
chainId,
|
|
169
|
+
priceFeed: onchainPriceFeed(node),
|
|
170
|
+
assets: [],
|
|
171
|
+
markets: [],
|
|
172
|
+
inStore: false
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
const periods = await this.#stalenessPeriods(chainId, feed, indexed.markets);
|
|
176
|
+
return {
|
|
177
|
+
...indexed,
|
|
178
|
+
markets: indexed.markets.map((market) => ({
|
|
179
|
+
...market,
|
|
180
|
+
marketStalenessPeriod: periods.get(slotKey(market.priceOracle, market.token, market.reserve)) ?? null
|
|
181
|
+
})),
|
|
182
|
+
inStore: true
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* One feed as the PriceFeedCompressor reads it, or nothing when the chain
|
|
187
|
+
* cannot be read or the compressor does not know the feed.
|
|
188
|
+
*
|
|
189
|
+
* Failures are warnings and not errors: this only ever stands in for a
|
|
190
|
+
* store that did not have the feed, and the caller reports that miss
|
|
191
|
+
* rather than whatever kept the chain from answering in its place.
|
|
192
|
+
*/
|
|
193
|
+
async #onchainNode(chainId, feed) {
|
|
194
|
+
try {
|
|
195
|
+
const client = this.#chains.client(chainId);
|
|
196
|
+
const compressor = await new AddressProviderContract("0xF7f0a609BfAb9a0A98786951ef10e5FE26cC1E38", client).getLatestVersionAddress(AP_PRICE_FEED_COMPRESSOR);
|
|
197
|
+
if (compressor === zeroAddress) return;
|
|
198
|
+
const { nodes } = await readFeedTrees(client, compressor, [feed]);
|
|
199
|
+
return nodes.get(feed);
|
|
200
|
+
} catch (e) {
|
|
201
|
+
this.logger?.warn(`could not read price feed ${feed} off chain ${chainId}: ${describe(e)}`);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Staleness period each market's own oracle holds for the feed's slots,
|
|
207
|
+
* keyed by {@link slotKey}.
|
|
208
|
+
*
|
|
209
|
+
* One call per oracle rather than per slot: the compressor answers for a
|
|
210
|
+
* whole oracle at once, and a feed in a dozen markets of the same curator
|
|
211
|
+
* would otherwise pay a round trip per token. Scoped to the tokens actually
|
|
212
|
+
* asked about, since the unscoped overload returns the oracle's entire feed
|
|
213
|
+
* tree as well.
|
|
214
|
+
*
|
|
215
|
+
* Failures are warnings and not errors. A slot missing from the answer keeps
|
|
216
|
+
* its `null`, which the caller reports as "the oracle would not say" — the
|
|
217
|
+
* alternative is failing a read whose indexed half is perfectly good.
|
|
218
|
+
*/
|
|
219
|
+
async #stalenessPeriods(chainId, feed, rows) {
|
|
220
|
+
const periods = /* @__PURE__ */ new Map();
|
|
221
|
+
if (rows.length === 0) return periods;
|
|
222
|
+
const tokensByOracle = /* @__PURE__ */ new Map();
|
|
223
|
+
for (const row of rows) {
|
|
224
|
+
let tokens = tokensByOracle.get(row.priceOracle);
|
|
225
|
+
if (!tokens) {
|
|
226
|
+
tokens = /* @__PURE__ */ new Set();
|
|
227
|
+
tokensByOracle.set(row.priceOracle, tokens);
|
|
228
|
+
}
|
|
229
|
+
tokens.add(row.token);
|
|
230
|
+
}
|
|
231
|
+
let client;
|
|
232
|
+
let compressor;
|
|
233
|
+
try {
|
|
234
|
+
client = this.#chains.client(chainId);
|
|
235
|
+
compressor = await new AddressProviderContract("0xF7f0a609BfAb9a0A98786951ef10e5FE26cC1E38", client).getLatestVersionAddress(AP_PRICE_FEED_COMPRESSOR);
|
|
236
|
+
} catch (e) {
|
|
237
|
+
this.logger?.warn(`no PriceFeedCompressor for chain ${chainId}, reporting no market staleness periods: ${describe(e)}`);
|
|
238
|
+
return periods;
|
|
239
|
+
}
|
|
240
|
+
const oracles = [...tokensByOracle.keys()];
|
|
241
|
+
const results = await client.multicall({
|
|
242
|
+
contracts: oracles.map((oracle) => ({
|
|
243
|
+
address: compressor,
|
|
244
|
+
abi: priceFeedCompressorAbi,
|
|
245
|
+
functionName: "getPriceOracleState",
|
|
246
|
+
args: [oracle, [...tokensByOracle.get(oracle) ?? []]]
|
|
247
|
+
})),
|
|
248
|
+
allowFailure: true
|
|
249
|
+
});
|
|
250
|
+
oracles.forEach((oracle, i) => {
|
|
251
|
+
const result = results[i];
|
|
252
|
+
if (result?.status !== "success") {
|
|
253
|
+
this.logger?.warn(`price oracle ${oracle} of chain ${chainId} would not report its feed map`);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const { priceFeedMap } = result.result;
|
|
257
|
+
for (const entry of priceFeedMap) {
|
|
258
|
+
if (entry.priceFeed.toLowerCase() !== feed) continue;
|
|
259
|
+
periods.set(slotKey(oracle, entry.token, entry.reserve), Number(entry.stalenessPeriod));
|
|
260
|
+
}
|
|
61
261
|
});
|
|
262
|
+
return periods;
|
|
62
263
|
}
|
|
63
264
|
/**
|
|
64
265
|
* One asset of the store: the feeds allowed for it, and the markets that
|
|
@@ -74,37 +275,230 @@ var PermissionlessOracles = class extends AbstractOffchainNamespace {
|
|
|
74
275
|
* await permissionless.oracles.asset({ chainId: 1, address: token });
|
|
75
276
|
* ```
|
|
76
277
|
*
|
|
278
|
+
* The quota totals are worked out here, over the very market rows this
|
|
279
|
+
* answers with — see {@link PermissionlessQuotaTotals}.
|
|
280
|
+
*
|
|
77
281
|
* @throws When the chain has no metadata for the address. An asset nothing
|
|
78
282
|
* was indexed for has no symbol or decimals, so there is no row to answer.
|
|
79
283
|
**/
|
|
80
284
|
async asset({ chainId, address }) {
|
|
81
|
-
|
|
285
|
+
const indexed = await this.getData({
|
|
82
286
|
path: `/pfs/${chainId}/asset/${address}`,
|
|
83
287
|
schema: assetDetailsSchema
|
|
84
288
|
});
|
|
289
|
+
const priceOf = await this.#underlyingPrices(indexed.markets.map((market) => ({
|
|
290
|
+
chainId,
|
|
291
|
+
underlyingPriceFeed: market.underlyingPriceFeed
|
|
292
|
+
})));
|
|
293
|
+
return {
|
|
294
|
+
...indexed,
|
|
295
|
+
...totalQuotas(indexed.markets, (feed) => priceOf(chainId, feed))
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* The price of every underlying the given quota rows name, looked up by
|
|
300
|
+
* chain and feed.
|
|
301
|
+
*
|
|
302
|
+
* Distinct feeds only, because an asset in a dozen markets of the same
|
|
303
|
+
* underlying names the same feed a dozen times, and a list of every chain's
|
|
304
|
+
* assets names a handful of feeds per chain between them all.
|
|
305
|
+
*/
|
|
306
|
+
async #underlyingPrices(rows) {
|
|
307
|
+
const refs = /* @__PURE__ */ new Map();
|
|
308
|
+
for (const { chainId, underlyingPriceFeed } of rows) {
|
|
309
|
+
if (underlyingPriceFeed === null) continue;
|
|
310
|
+
const address = underlyingPriceFeed.toLowerCase();
|
|
311
|
+
refs.set(feedKey(chainId, address), {
|
|
312
|
+
chainId,
|
|
313
|
+
address
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
const prices = pricesByFeed(await this.getPrices([...refs.values()]));
|
|
317
|
+
return (chainId, feed) => prices.get(feedKey(chainId, feed));
|
|
85
318
|
}
|
|
86
319
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
320
|
+
* Every asset the store carries, with what its markets make of it.
|
|
321
|
+
*
|
|
322
|
+
* Enough on its own to render a list of assets: the alternative is the
|
|
323
|
+
* store's whole read for the names plus a call per row for the quotas, and
|
|
324
|
+
* this is one request either way you scope it.
|
|
89
325
|
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
326
|
+
* Ordered by how central each asset is — the markets it underlies first,
|
|
327
|
+
* then what is quoted against it, then what it is allowed — so a consumer
|
|
328
|
+
* renders the rows as they arrive. Omit `chainId` for every chain the
|
|
329
|
+
* backend serves, ordered the same way across all of them.
|
|
93
330
|
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
331
|
+
* Each row also carries its quota totals and what this SDK's tables say the
|
|
332
|
+
* asset is, neither of which is served — see
|
|
333
|
+
* {@link PermissionlessAssetSummary}. The totals are what the last two terms
|
|
334
|
+
* of the order are over, so this waits for them rather than handing back a
|
|
335
|
+
* list that would reshuffle a moment later.
|
|
96
336
|
*
|
|
97
337
|
* ```ts
|
|
98
|
-
* const
|
|
338
|
+
* const assets = await permissionless.oracles.assets({ chainId: 1 });
|
|
339
|
+
* const everywhere = await permissionless.oracles.assets();
|
|
340
|
+
* ```
|
|
341
|
+
**/
|
|
342
|
+
async assets({ chainId } = {}) {
|
|
343
|
+
const indexed = await this.getData({
|
|
344
|
+
path: "/pfs/assets",
|
|
345
|
+
query: { chainId: chainId?.toString() },
|
|
346
|
+
schema: assetSummariesSchema
|
|
347
|
+
});
|
|
348
|
+
const networks = /* @__PURE__ */ new Map();
|
|
349
|
+
for (const { chainId: id } of indexed) if (!networks.has(id)) networks.set(id, isSupportedNetwork(id) ? getNetworkType(id) : void 0);
|
|
350
|
+
const priceOf = await this.#underlyingPrices(indexed.flatMap((asset) => asset.marketQuotas.map((quota) => ({
|
|
351
|
+
chainId: asset.chainId,
|
|
352
|
+
underlyingPriceFeed: quota.underlyingPriceFeed
|
|
353
|
+
}))));
|
|
354
|
+
return indexed.map((asset) => {
|
|
355
|
+
const network = networks.get(asset.chainId);
|
|
356
|
+
return {
|
|
357
|
+
...asset,
|
|
358
|
+
...totalQuotas(asset.marketQuotas, (feed) => priceOf(asset.chainId, feed)),
|
|
359
|
+
assetType: network === void 0 ? null : getAssetType(asset.address, network) ?? null,
|
|
360
|
+
isRwa: network === void 0 ? false : isRWAToken(asset.address, network)
|
|
361
|
+
};
|
|
362
|
+
}).sort(byCentrality);
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* What each source of price information can do to the chain's markets if it
|
|
366
|
+
* lies.
|
|
367
|
+
*
|
|
368
|
+
* The feeds under a chain's oracles are partitioned by where their numbers
|
|
369
|
+
* actually come from — Chainlink, Redstone, Pyth, Lido, Curve, each vault's
|
|
370
|
+
* own issuer — on the grounds that a source fails as a whole: whoever
|
|
371
|
+
* trusts one Chainlink aggregator trusts the operator behind all of them.
|
|
372
|
+
* Each group is then made to report a multiple of what it reports now, and
|
|
373
|
+
* every feed the markets read is read again inside that same call.
|
|
374
|
+
*
|
|
375
|
+
* No fork and no harness. The lie is a state override on an `eth_call`, so
|
|
376
|
+
* the composite feeds above it, their bounds and the oracle's own checks all
|
|
377
|
+
* run as deployed, against real state, at the current block — what comes
|
|
378
|
+
* back is what the market would have read.
|
|
379
|
+
*
|
|
380
|
+
* Each slot is scored by how its collateral moved *against its market's own
|
|
381
|
+
* underlying*, because that is what an account is valued by: a group that
|
|
382
|
+
* moves both legs has moved nothing. Beyond the two unbounded ends, each
|
|
383
|
+
* group is bisected for the multiple at which something first breaks, which
|
|
384
|
+
* is the figure that separates a source held in check by a bound from one
|
|
385
|
+
* nothing is checking.
|
|
386
|
+
*
|
|
387
|
+
* ```ts
|
|
388
|
+
* const report = await permissionless.oracles.stressTest({ chainId: 1 });
|
|
389
|
+
* for (const group of report.groups) {
|
|
390
|
+
* console.log(group.verdict);
|
|
391
|
+
* }
|
|
392
|
+
* ```
|
|
393
|
+
*
|
|
394
|
+
* Report {@link StressReport.unidentifiedUsd} alongside anything taken from
|
|
395
|
+
* this: it is the quota priced through a source the partition could only
|
|
396
|
+
* name after its own description, and nothing says what that fails with.
|
|
397
|
+
*
|
|
398
|
+
* Reads the chain directly, so the client has to have attached — see
|
|
399
|
+
* {@link GearboxPermissionless.attach}. Costs one request for the slots and
|
|
400
|
+
* then, per group, one `eth_call` per scenario plus the bisection's; a whole
|
|
401
|
+
* chain comes to a minute or so, and `thresholds: false` cuts it to seconds.
|
|
402
|
+
*
|
|
403
|
+
* @throws When the client has not attached, when the chain has no client
|
|
404
|
+
* configured, or when no PriceFeedCompressor is deployed on it — none of the
|
|
405
|
+
* three leaves anything to simulate.
|
|
406
|
+
**/
|
|
407
|
+
/**
|
|
408
|
+
* Every asset's manipulation resistance: how far, from 1 to 10 times, a
|
|
409
|
+
* lying source can push the price its markets hold for it.
|
|
410
|
+
*
|
|
411
|
+
* Each source group behind an asset's feeds — Chainlink, Redstone, Lido and
|
|
412
|
+
* so on — is made in turn to answer ten times what it answers now, and a
|
|
413
|
+
* market scores by how far that moves its final price, `min(main,
|
|
414
|
+
* reserve)`, as a multiple either way: 1 for not at all, 1.04 for 4%, 10
|
|
415
|
+
* for the whole lie. A group under which the price stops answering moves
|
|
416
|
+
* nothing and is listed rather than scored. An asset scores its worst
|
|
417
|
+
* running market, underlying or collateral.
|
|
418
|
+
*
|
|
419
|
+
* Simulated on the chain as it stands, through state overrides in
|
|
420
|
+
* `eth_call`: one call per source group per chain, so the node behind the
|
|
421
|
+
* client has to accept overrides. A chain with no client configured, or
|
|
422
|
+
* whose node refuses, is left out with a warning rather than failing the
|
|
423
|
+
* rest.
|
|
424
|
+
*
|
|
425
|
+
* ```ts
|
|
426
|
+
* const scores = await permissionless.oracles.assetScores({ chainId: 1 });
|
|
427
|
+
* ```
|
|
428
|
+
*
|
|
429
|
+
* @throws {PermissionlessNotAttachedError} before
|
|
430
|
+
* {@link GearboxPermissionless.attach} has resolved.
|
|
431
|
+
**/
|
|
432
|
+
async assetScores({ chainId } = {}) {
|
|
433
|
+
const slots = await this.slots({ chainId });
|
|
434
|
+
const byChain = /* @__PURE__ */ new Map();
|
|
435
|
+
for (const slot of slots) byChain.set(slot.chainId, [...byChain.get(slot.chainId) ?? [], slot]);
|
|
436
|
+
return (await Promise.all([...byChain].map(async ([id, chainSlots]) => {
|
|
437
|
+
let client;
|
|
438
|
+
try {
|
|
439
|
+
client = this.#chains.client(id);
|
|
440
|
+
} catch (e) {
|
|
441
|
+
this.logger?.warn(`chain ${id} has no client, leaving its assets unscored: ${describe(e)}`);
|
|
442
|
+
return [];
|
|
443
|
+
}
|
|
444
|
+
try {
|
|
445
|
+
const compressor = await new AddressProviderContract("0xF7f0a609BfAb9a0A98786951ef10e5FE26cC1E38", client).getLatestVersionAddress(AP_PRICE_FEED_COMPRESSOR);
|
|
446
|
+
return await runAssetScores({
|
|
447
|
+
client,
|
|
448
|
+
chainId: id,
|
|
449
|
+
slots: chainSlots,
|
|
450
|
+
compressor,
|
|
451
|
+
logger: this.logger ?? void 0
|
|
452
|
+
});
|
|
453
|
+
} catch (e) {
|
|
454
|
+
this.logger?.warn(`chain ${id} would not report its feed trees, leaving its assets unscored: ${describe(e)}`);
|
|
455
|
+
return [];
|
|
456
|
+
}
|
|
457
|
+
}))).flat();
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* The manipulation resistance of any main feed and reserve on a chain,
|
|
461
|
+
* scored the way {@link assetScores} scores a market holding them — for
|
|
462
|
+
* trying a pair before any market does.
|
|
463
|
+
*
|
|
464
|
+
* ```ts
|
|
465
|
+
* const pair = await permissionless.oracles.feedPairScore({
|
|
99
466
|
* chainId: 1,
|
|
100
|
-
*
|
|
467
|
+
* main: "0x…",
|
|
468
|
+
* reserve: "0x…",
|
|
101
469
|
* });
|
|
102
470
|
* ```
|
|
471
|
+
*
|
|
472
|
+
* Reads the chain directly, through state overrides in `eth_call`, so the
|
|
473
|
+
* client has to have attached and the node behind it has to accept them.
|
|
474
|
+
*
|
|
475
|
+
* @throws When the client has not attached, when the chain has no client or
|
|
476
|
+
* no PriceFeedCompressor, or when there is nothing to score: a feed the
|
|
477
|
+
* compressor cannot read, a `ZeroPriceFeed` main, or a final price that does
|
|
478
|
+
* not answer.
|
|
103
479
|
**/
|
|
104
|
-
async
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
480
|
+
async feedPairScore({ chainId, main, reserve }) {
|
|
481
|
+
const client = this.#chains.client(chainId);
|
|
482
|
+
const compressor = await new AddressProviderContract("0xF7f0a609BfAb9a0A98786951ef10e5FE26cC1E38", client).getLatestVersionAddress(AP_PRICE_FEED_COMPRESSOR);
|
|
483
|
+
return runFeedPairScore({
|
|
484
|
+
client,
|
|
485
|
+
chainId,
|
|
486
|
+
compressor,
|
|
487
|
+
main,
|
|
488
|
+
reserve,
|
|
489
|
+
logger: this.logger ?? void 0
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
async stressTest({ chainId, ...options }) {
|
|
493
|
+
const client = this.#chains.client(chainId);
|
|
494
|
+
const compressor = await new AddressProviderContract("0xF7f0a609BfAb9a0A98786951ef10e5FE26cC1E38", client).getLatestVersionAddress(AP_PRICE_FEED_COMPRESSOR);
|
|
495
|
+
return runStressTest({
|
|
496
|
+
client,
|
|
497
|
+
chainId,
|
|
498
|
+
compressor,
|
|
499
|
+
slots: await this.slots({ chainId }),
|
|
500
|
+
logger: this.logger ?? void 0,
|
|
501
|
+
...options
|
|
108
502
|
});
|
|
109
503
|
}
|
|
110
504
|
/**
|
|
@@ -128,69 +522,247 @@ var PermissionlessOracles = class extends AbstractOffchainNamespace {
|
|
|
128
522
|
});
|
|
129
523
|
}
|
|
130
524
|
/**
|
|
131
|
-
* What the
|
|
525
|
+
* What the named feeds price, read off the chain.
|
|
526
|
+
*
|
|
527
|
+
* Prices exactly what is asked for, at the current block, across as many
|
|
528
|
+
* chains as the list names — one read per chain, so a screen listing every
|
|
529
|
+
* chain's feeds pays per chain and not per row. A feed the store does not
|
|
530
|
+
* carry prices the same way, which is what makes this the call to reach for
|
|
531
|
+
* after a deploy, or while a form is still choosing between candidates.
|
|
132
532
|
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
533
|
+
* Read in two passes. The first asks every feed what it reports as it
|
|
534
|
+
* stands, which is one multicall per chain; a feed that will not answer
|
|
535
|
+
* until it has been given a fresh one is then priced inside a simulation
|
|
536
|
+
* that updates it first, so a push-based feed prices as readily as a
|
|
537
|
+
* pull-based one. The second pass costs an `OnchainSDK` to attach, hence
|
|
538
|
+
* only the feeds that need it rather than all of them.
|
|
539
|
+
*
|
|
540
|
+
* `null` is a feed neither pass could read, and a chain with no client
|
|
541
|
+
* configured is every one of its feeds reading `null` rather than a failed
|
|
542
|
+
* call — the figures built on them are floors either way.
|
|
137
543
|
*
|
|
138
544
|
* ```ts
|
|
139
|
-
* const {
|
|
140
|
-
* chainId: 1,
|
|
141
|
-
*
|
|
142
|
-
* });
|
|
545
|
+
* const [{ price }] = await permissionless.oracles.getPrices([
|
|
546
|
+
* { chainId: 1, address: feed },
|
|
547
|
+
* ]);
|
|
143
548
|
* ```
|
|
549
|
+
*
|
|
550
|
+
* @returns One entry per feed asked for, in the order asked, each price in
|
|
551
|
+
* the 8 decimals every Gearbox feed reports.
|
|
144
552
|
**/
|
|
145
|
-
async
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
553
|
+
async getPrices(feeds) {
|
|
554
|
+
const byChain = /* @__PURE__ */ new Map();
|
|
555
|
+
for (const { chainId, address } of feeds) {
|
|
556
|
+
const addresses = byChain.get(chainId) ?? /* @__PURE__ */ new Set();
|
|
557
|
+
addresses.add(address.toLowerCase());
|
|
558
|
+
byChain.set(chainId, addresses);
|
|
559
|
+
}
|
|
560
|
+
const chainIds = [...byChain.keys()];
|
|
561
|
+
const results = await Promise.all(chainIds.map((chainId) => this.#pricesOf(chainId, [...byChain.get(chainId) ?? []])));
|
|
562
|
+
const prices = new Map(chainIds.map((id, i) => [id, results[i] ?? {}]));
|
|
563
|
+
return feeds.map(({ chainId, address }) => ({
|
|
564
|
+
chainId,
|
|
565
|
+
address,
|
|
566
|
+
price: prices.get(chainId)?.[address.toLowerCase()] ?? null
|
|
567
|
+
}));
|
|
150
568
|
}
|
|
151
569
|
/**
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
570
|
+
* The scale each of the named feeds answers in.
|
|
571
|
+
*
|
|
572
|
+
* The companion to {@link getPrices} for the callers whose feeds are not
|
|
573
|
+
* all an oracle's: everything an oracle holds reports 8 decimals, but the
|
|
574
|
+
* feeds under a composite are whatever was deployed, and a rate leg at 18
|
|
575
|
+
* read as 8 is wrong by ten orders of magnitude rather than visibly wrong.
|
|
576
|
+
*
|
|
577
|
+
* Separate from the price because the two keep different company: this is
|
|
578
|
+
* fixed for the life of the contract and worth caching for the session,
|
|
579
|
+
* while a price is re-read whenever the reader asks. One multicall per
|
|
580
|
+
* chain either way.
|
|
155
581
|
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
582
|
+
* `null` is a feed that would not answer, or one on a chain with no client
|
|
583
|
+
* configured.
|
|
584
|
+
*
|
|
585
|
+
* ```ts
|
|
586
|
+
* const [{ decimals }] = await permissionless.oracles.getDecimals([
|
|
587
|
+
* { chainId: 1, address: feed },
|
|
588
|
+
* ]);
|
|
589
|
+
* ```
|
|
590
|
+
*
|
|
591
|
+
* @returns One entry per feed asked for, in the order asked.
|
|
158
592
|
**/
|
|
159
|
-
async
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
593
|
+
async getDecimals(feeds) {
|
|
594
|
+
const byChain = /* @__PURE__ */ new Map();
|
|
595
|
+
for (const { chainId, address } of feeds) {
|
|
596
|
+
const addresses = byChain.get(chainId) ?? /* @__PURE__ */ new Set();
|
|
597
|
+
addresses.add(address.toLowerCase());
|
|
598
|
+
byChain.set(chainId, addresses);
|
|
599
|
+
}
|
|
600
|
+
const chainIds = [...byChain.keys()];
|
|
601
|
+
const results = await Promise.all(chainIds.map((chainId) => this.#decimalsOf(chainId, [...byChain.get(chainId) ?? []])));
|
|
602
|
+
const decimals = new Map(chainIds.map((id, i) => [id, results[i] ?? {}]));
|
|
603
|
+
return feeds.map(({ chainId, address }) => ({
|
|
604
|
+
chainId,
|
|
605
|
+
address,
|
|
606
|
+
decimals: decimals.get(chainId)?.[address.toLowerCase()] ?? null
|
|
607
|
+
}));
|
|
608
|
+
}
|
|
609
|
+
/** One chain's half of {@link getDecimals}. */
|
|
610
|
+
async #decimalsOf(chainId, priceFeeds) {
|
|
611
|
+
let client;
|
|
612
|
+
try {
|
|
613
|
+
client = this.#chains.client(chainId);
|
|
614
|
+
} catch (e) {
|
|
615
|
+
this.logger?.warn(`chain ${chainId} has no client, reporting its ${priceFeeds.length} feed(s) without a scale: ${describe(e)}`);
|
|
616
|
+
return {};
|
|
617
|
+
}
|
|
618
|
+
try {
|
|
619
|
+
return await getFeedDecimals({
|
|
620
|
+
client,
|
|
621
|
+
priceFeeds
|
|
622
|
+
});
|
|
623
|
+
} catch (e) {
|
|
624
|
+
this.logger?.warn(`chain ${chainId} would not answer decimals for its ${priceFeeds.length} feed(s): ${describe(e)}`);
|
|
625
|
+
return {};
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
/** One chain's half of {@link getPrices}: the cheap pass, then the rest. */
|
|
629
|
+
async #pricesOf(chainId, priceFeeds) {
|
|
630
|
+
let client;
|
|
631
|
+
try {
|
|
632
|
+
client = this.#chains.client(chainId);
|
|
633
|
+
} catch (e) {
|
|
634
|
+
this.logger?.warn(`chain ${chainId} has no client, reporting its ${priceFeeds.length} feed(s) unpriced: ${describe(e)}`);
|
|
635
|
+
return {};
|
|
636
|
+
}
|
|
637
|
+
let prices;
|
|
638
|
+
try {
|
|
639
|
+
prices = await getLatestPrices({
|
|
640
|
+
client,
|
|
641
|
+
priceFeeds
|
|
642
|
+
});
|
|
643
|
+
} catch (e) {
|
|
644
|
+
this.logger?.warn(`chain ${chainId} would not answer for its ${priceFeeds.length} feed(s), trying each with an update: ${describe(e)}`);
|
|
645
|
+
prices = {};
|
|
646
|
+
}
|
|
647
|
+
const stale = priceFeeds.filter((feed) => (prices[feed] ?? null) === null);
|
|
648
|
+
if (stale.length === 0) return prices;
|
|
649
|
+
try {
|
|
650
|
+
return {
|
|
651
|
+
...prices,
|
|
652
|
+
...await getPrices({
|
|
653
|
+
client,
|
|
654
|
+
priceFeeds: stale,
|
|
655
|
+
chunkSize: 200,
|
|
656
|
+
gasLimit: GET_PRICES_GAS_LIMIT_BY_CHAIN_ID[chainId]
|
|
657
|
+
})
|
|
658
|
+
};
|
|
659
|
+
} catch (e) {
|
|
660
|
+
this.logger?.warn(`chain ${chainId} would not price ${stale.length} of its feed(s) even with an update, reporting them unpriced: ${describe(e)}`);
|
|
661
|
+
return prices;
|
|
662
|
+
}
|
|
164
663
|
}
|
|
165
664
|
/**
|
|
166
|
-
*
|
|
665
|
+
* What one chain's feeds value one chain's assets at: `price × totalSupply`
|
|
666
|
+
* per pair.
|
|
167
667
|
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
* form is still choosing between candidates.
|
|
668
|
+
* Asked from whichever side the caller is looking at — one asset through
|
|
669
|
+
* each of its feeds, or one feed over each of the assets it prices — because
|
|
670
|
+
* a screen shows one row per feed or one row per asset, never the full
|
|
671
|
+
* cross product. Both answer the same pairs in the same shape.
|
|
173
672
|
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
673
|
+
* Two reads per chain: the prices, and one multicall for the assets'
|
|
674
|
+
* supplies and decimals. Several scopes can be asked at once, which is how a
|
|
675
|
+
* table of every asset's feeds pays per chain rather than per row; the pairs
|
|
676
|
+
* come back in the order the scopes named them. A pair neither read could
|
|
677
|
+
* resolve has a `null` cap, which is not a zero.
|
|
177
678
|
*
|
|
178
679
|
* ```ts
|
|
179
|
-
* const
|
|
680
|
+
* const caps = await permissionless.oracles.getMarketCap({
|
|
180
681
|
* chainId: 1,
|
|
181
|
-
*
|
|
682
|
+
* asset: token,
|
|
683
|
+
* priceFeeds: [main, reserve],
|
|
182
684
|
* });
|
|
183
685
|
* ```
|
|
184
|
-
*
|
|
185
|
-
* @returns A price per feed, in the 8 decimals every Gearbox feed reports.
|
|
186
686
|
**/
|
|
187
|
-
async
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
687
|
+
async getMarketCap(scope) {
|
|
688
|
+
const pairs = (Array.isArray(scope) ? scope : [scope]).flatMap((one) => "asset" in one ? one.priceFeeds.map((priceFeed) => ({
|
|
689
|
+
chainId: one.chainId,
|
|
690
|
+
priceFeed: priceFeed.toLowerCase(),
|
|
691
|
+
asset: one.asset.toLowerCase()
|
|
692
|
+
})) : one.assets.map((asset) => ({
|
|
693
|
+
chainId: one.chainId,
|
|
694
|
+
priceFeed: one.priceFeed.toLowerCase(),
|
|
695
|
+
asset: asset.toLowerCase()
|
|
696
|
+
})));
|
|
697
|
+
if (pairs.length === 0) return [];
|
|
698
|
+
const feeds = /* @__PURE__ */ new Map();
|
|
699
|
+
const assetsByChain = /* @__PURE__ */ new Map();
|
|
700
|
+
for (const { chainId, priceFeed, asset } of pairs) {
|
|
701
|
+
feeds.set(feedKey(chainId, priceFeed), {
|
|
702
|
+
chainId,
|
|
703
|
+
address: priceFeed
|
|
704
|
+
});
|
|
705
|
+
const assets = assetsByChain.get(chainId) ?? /* @__PURE__ */ new Set();
|
|
706
|
+
assets.add(asset);
|
|
707
|
+
assetsByChain.set(chainId, assets);
|
|
708
|
+
}
|
|
709
|
+
const chainIds = [...assetsByChain.keys()];
|
|
710
|
+
const [prices, perChain] = await Promise.all([this.getPrices([...feeds.values()]), Promise.all(chainIds.map((chainId) => this.#supplies(chainId, [...assetsByChain.get(chainId) ?? []])))]);
|
|
711
|
+
const priced = pricesByFeed(prices);
|
|
712
|
+
const supplies = new Map(chainIds.map((id, i) => [id, perChain[i]]));
|
|
713
|
+
return pairs.map(({ chainId, priceFeed, asset }) => {
|
|
714
|
+
const supply = supplies.get(chainId)?.get(asset);
|
|
715
|
+
return {
|
|
716
|
+
chainId,
|
|
717
|
+
priceFeed,
|
|
718
|
+
asset,
|
|
719
|
+
cap: supply === void 0 ? null : toCap(priced.get(feedKey(chainId, priceFeed)), supply.totalSupply, supply.decimals)
|
|
720
|
+
};
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Each asset's supply and the decimals it is counted in, which is what turns
|
|
725
|
+
* a supply into an amount.
|
|
726
|
+
*
|
|
727
|
+
* Both read rather than one of them taken from the index, so that a cap can
|
|
728
|
+
* be asked for an address nothing was indexed for at all — a token a form is
|
|
729
|
+
* still choosing, say. An asset missing from the answer is left out, and
|
|
730
|
+
* caps over it come back `null`.
|
|
731
|
+
*/
|
|
732
|
+
async #supplies(chainId, assets) {
|
|
733
|
+
const supplies = /* @__PURE__ */ new Map();
|
|
734
|
+
let client;
|
|
735
|
+
try {
|
|
736
|
+
client = this.#chains.client(chainId);
|
|
737
|
+
} catch (e) {
|
|
738
|
+
this.logger?.warn(`chain ${chainId} has no client, reporting no market caps: ${describe(e)}`);
|
|
739
|
+
return supplies;
|
|
740
|
+
}
|
|
741
|
+
const results = await client.multicall({
|
|
742
|
+
contracts: assets.flatMap((address) => [{
|
|
743
|
+
address,
|
|
744
|
+
abi: erc20Abi,
|
|
745
|
+
functionName: "totalSupply"
|
|
746
|
+
}, {
|
|
747
|
+
address,
|
|
748
|
+
abi: erc20Abi,
|
|
749
|
+
functionName: "decimals"
|
|
750
|
+
}]),
|
|
751
|
+
allowFailure: true
|
|
752
|
+
});
|
|
753
|
+
assets.forEach((address, i) => {
|
|
754
|
+
const supply = results[2 * i];
|
|
755
|
+
const decimals = results[2 * i + 1];
|
|
756
|
+
if (decimals?.status !== "success") {
|
|
757
|
+
this.logger?.warn(`asset ${address} of chain ${chainId} would not report its decimals, reporting no market cap for it`);
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
supplies.set(address, {
|
|
761
|
+
totalSupply: supply?.status === "success" ? supply.result : null,
|
|
762
|
+
decimals: Number(decimals.result)
|
|
763
|
+
});
|
|
193
764
|
});
|
|
765
|
+
return supplies;
|
|
194
766
|
}
|
|
195
767
|
};
|
|
196
768
|
//#endregion
|