@gearbox-protocol/sdk 18.2.0-next.13 → 18.2.0-next.14

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 (29) hide show
  1. package/dist/cjs/permissionless/api/index.js +2 -1
  2. package/dist/cjs/permissionless/api/oracles/PermissionlessOracles.js +40 -5
  3. package/dist/cjs/permissionless/api/oracles/index.js +2 -1
  4. package/dist/cjs/permissionless/api/oracles/score/formula.js +19 -24
  5. package/dist/cjs/permissionless/api/oracles/score/index.js +1 -1
  6. package/dist/cjs/permissionless/api/oracles/score/run.js +186 -105
  7. package/dist/cjs/permissionless/api/oracles/stress/index.js +1 -0
  8. package/dist/cjs/permissionless/api/oracles/stress/run.js +63 -13
  9. package/dist/cjs/permissionless/index.js +2 -1
  10. package/dist/esm/permissionless/api/index.js +4 -4
  11. package/dist/esm/permissionless/api/oracles/PermissionlessOracles.js +41 -6
  12. package/dist/esm/permissionless/api/oracles/index.js +4 -4
  13. package/dist/esm/permissionless/api/oracles/score/formula.js +20 -24
  14. package/dist/esm/permissionless/api/oracles/score/index.js +3 -3
  15. package/dist/esm/permissionless/api/oracles/score/run.js +187 -107
  16. package/dist/esm/permissionless/api/oracles/stress/index.js +2 -2
  17. package/dist/esm/permissionless/api/oracles/stress/run.js +64 -15
  18. package/dist/esm/permissionless/index.js +4 -4
  19. package/dist/types/permissionless/api/index.d.ts +5 -5
  20. package/dist/types/permissionless/api/oracles/PermissionlessOracles.d.ts +30 -6
  21. package/dist/types/permissionless/api/oracles/index.d.ts +5 -5
  22. package/dist/types/permissionless/api/oracles/score/formula.d.ts +14 -17
  23. package/dist/types/permissionless/api/oracles/score/index.d.ts +4 -4
  24. package/dist/types/permissionless/api/oracles/score/run.d.ts +28 -10
  25. package/dist/types/permissionless/api/oracles/score/types.d.ts +33 -28
  26. package/dist/types/permissionless/api/oracles/stress/index.d.ts +2 -2
  27. package/dist/types/permissionless/api/oracles/stress/run.d.ts +11 -0
  28. package/dist/types/permissionless/index.d.ts +5 -5
  29. package/package.json +1 -1
@@ -1,63 +1,72 @@
1
1
  import { classifySources, sourcesByFeed } from "../stress/classify.js";
2
- import { RATE_FEED_TYPE, readForest, readRateTargets, readUnder } from "../stress/run.js";
2
+ import { RATE_FEED_TYPE, readFeedTrees, readForest, readRateTargets, readUnder } from "../stress/run.js";
3
3
  import { factorOf, finalPrice, scoreOfFactor, worstOf } from "./formula.js";
4
4
  import { formatUnits } from "viem";
5
5
  //#region src/permissionless/api/oracles/score/run.ts
6
6
  const ZERO_TYPE = "PRICE_FEED::ZERO";
7
7
  /** Groups simulated at once: one `eth_call` each, all against one node. */
8
8
  const CONCURRENCY = 6;
9
- const feedOf = (slot) => slot.priceFeed.toLowerCase();
9
+ const lower = (address) => address.toLowerCase();
10
10
  /**
11
- * Every asset of one chain, scored by what each of its source groups can do
12
- * to the price its running markets hold for it.
11
+ * A main feed and the reserve a slot or a caller names for it, as the score
12
+ * takes them: a reserve that is a `ZeroPriceFeed` is no reserve.
13
+ */
14
+ function pairOf(main, reserve, nodes) {
15
+ const notes = [];
16
+ if (!reserve) {
17
+ notes.push("no reserve feed");
18
+ return {
19
+ main,
20
+ reserve: void 0,
21
+ notes
22
+ };
23
+ }
24
+ if (nodes.get(reserve)?.contractType === ZERO_TYPE) {
25
+ notes.push("reserve is a ZeroPriceFeed");
26
+ return {
27
+ main,
28
+ reserve: void 0,
29
+ notes
30
+ };
31
+ }
32
+ if (reserve === main) notes.push("reserve is the main feed");
33
+ return {
34
+ main,
35
+ reserve,
36
+ notes
37
+ };
38
+ }
39
+ /**
40
+ * Every pair's final price under each source group lying, from one read of
41
+ * the LP feeds' rate contracts, one of every feed as it stands, and then one
42
+ * `eth_call` per source group with the group's sources overridden to answer
43
+ * {@link SCORE_MULTIPLIER} times what they answer now.
13
44
  *
14
- * One read of the oracles' feed trees, one of the LP feeds' rate contracts,
15
- * one of every market feed as it stands — then one `eth_call` per source
16
- * group, with the group's sources overridden to answer
17
- * {@link SCORE_MULTIPLIER} times what they answer now and every market feed
18
- * read under it.
19
- **/
20
- async function runAssetScores({ client, chainId, slots, compressor, logger }) {
21
- const running = slots.filter((slot) => !slot.isShutdown);
22
- const oracles = [...new Set(running.map((slot) => slot.priceOracle.toLowerCase()))];
23
- const { nodes, answers, unreadableOracles } = await readForest(client, compressor, oracles);
24
- for (const oracle of unreadableOracles) logger?.warn(`price oracle ${oracle} of chain ${chainId} would not report its feed tree; the markets on it are not scored`);
45
+ * A pair whose final price does not answer as it stands comes back `null`:
46
+ * there is no price to move.
47
+ */
48
+ async function simulatePairs({ client, chainId, nodes, answers, pairs, logger }) {
25
49
  const { targets, baselines: rateBaselines } = await readRateTargets(client, [...nodes.values()].filter((node) => RATE_FEED_TYPE.test(node.contractType)).map((node) => node.address));
26
50
  const { sources, unresolved } = classifySources(chainId, [...nodes.values()], targets, (target) => rateBaselines.get(target) ?? answers.get(target) ?? null);
27
51
  for (const node of unresolved) logger?.warn(`feed ${node.address} of chain ${chainId} would not say what it reads its rate from; its protocol is not perturbed`);
28
- const slotsByPosition = /* @__PURE__ */ new Map();
29
- for (const slot of running) {
30
- const key = `${slot.market.toLowerCase()}:${slot.token.toLowerCase()}`;
31
- const entry = slotsByPosition.get(key) ?? {};
32
- entry[slot.reserve ? "reserve" : "main"] = slot;
33
- slotsByPosition.set(key, entry);
34
- }
35
- const positions = [];
36
- for (const { main, reserve } of slotsByPosition.values()) {
37
- if (!main || main.isUnderlying) continue;
38
- const mainNode = nodes.get(feedOf(main));
39
- if (!mainNode || mainNode.contractType === ZERO_TYPE) continue;
40
- const notes = [];
41
- let reserveFeed;
42
- if (!reserve) notes.push("no reserve feed");
43
- else if (nodes.get(feedOf(reserve))?.contractType === ZERO_TYPE) notes.push("reserve is a ZeroPriceFeed");
44
- else {
45
- reserveFeed = feedOf(reserve);
46
- if (reserveFeed === feedOf(main)) notes.push("reserve is the main feed");
52
+ const roots = [...new Set(pairs.flatMap(({ main, reserve }) => reserve ? [main, reserve] : [main]))];
53
+ const reach = sourcesByFeed(roots, nodes, sources);
54
+ const sourceByTarget = new Map(sources.map((source) => [source.target, source]));
55
+ const drivable = (source) => source !== void 0 && source.perturbable && source.baseline !== null;
56
+ const groupsOf = (feed) => {
57
+ const out = /* @__PURE__ */ new Map();
58
+ for (const target of feed ? reach.get(feed) ?? [] : []) {
59
+ const source = sourceByTarget.get(target);
60
+ if (drivable(source)) out.set(source.group, [...out.get(source.group) ?? [], source]);
47
61
  }
48
- positions.push({
49
- main,
50
- reserve,
51
- reserveFeed,
52
- notes
53
- });
54
- }
55
- const priced = [...new Set(positions.flatMap(({ main, reserveFeed }) => reserveFeed ? [feedOf(main), reserveFeed] : [feedOf(main)]))];
56
- if (priced.length === 0) return [];
57
- const underlyingOf = (slot) => slot.underlyingPriceFeed?.toLowerCase();
58
- const roots = [.../* @__PURE__ */ new Set([...priced, ...positions.map(({ main }) => underlyingOf(main)).filter((feed) => feed !== void 0)])];
62
+ return out;
63
+ };
64
+ const sourcesOf = (feed) => [...groupsOf(feed).keys()];
65
+ if (roots.length === 0) return {
66
+ results: [],
67
+ sourcesOf
68
+ };
59
69
  const updatedAt = BigInt(Math.floor(Date.now() / 1e3));
60
- const drivable = (source) => source !== void 0 && source.perturbable && source.baseline !== null;
61
70
  const fresh = sources.filter((source) => drivable(source) && source.kind === "feed");
62
71
  const asIs = await readUnder(client, roots, [], 1, updatedAt);
63
72
  const stale = roots.filter((root) => (asIs.get(root) ?? null) === null);
@@ -67,72 +76,105 @@ async function runAssetScores({ client, chainId, slots, compressor, logger }) {
67
76
  const freshRoots = stale.filter((root) => (freshened.get(root) ?? null) !== null);
68
77
  /** Every root under a lie, each read the way its base was. */
69
78
  const readLying = async (members) => {
70
- const [plain, refreshed] = await Promise.all([asIsRoots.length > 0 ? readUnder(client, asIsRoots, members, 5, updatedAt) : /* @__PURE__ */ new Map(), freshRoots.length > 0 ? readUnder(client, freshRoots, members, 5, updatedAt, fresh) : /* @__PURE__ */ new Map()]);
79
+ const [plain, refreshed] = await Promise.all([asIsRoots.length > 0 ? readUnder(client, asIsRoots, members, 10, updatedAt) : /* @__PURE__ */ new Map(), freshRoots.length > 0 ? readUnder(client, freshRoots, members, 10, updatedAt, fresh) : /* @__PURE__ */ new Map()]);
71
80
  return new Map([...plain, ...refreshed]);
72
81
  };
73
- for (const position of positions) if (position.reserveFeed && (base.get(position.reserveFeed) ?? null) === null) {
74
- position.reserveFeed = void 0;
75
- position.notes.push("reserve does not answer");
76
- }
77
- const reach = sourcesByFeed(roots, nodes, sources);
78
- const sourceByTarget = new Map(sources.map((source) => [source.target, source]));
82
+ const effective = pairs.map((pair) => pair.reserve && (base.get(pair.reserve) ?? null) === null ? {
83
+ ...pair,
84
+ reserve: void 0,
85
+ notes: [...pair.notes, "reserve does not answer"]
86
+ } : pair);
79
87
  const byGroup = /* @__PURE__ */ new Map();
80
88
  for (const source of sources.filter(drivable)) byGroup.set(source.group, [...byGroup.get(source.group) ?? [], source]);
81
89
  const reachedGroups = /* @__PURE__ */ new Set();
82
- for (const root of priced) for (const target of reach.get(root) ?? []) {
83
- const source = sourceByTarget.get(target);
84
- if (drivable(source)) reachedGroups.add(source.group);
85
- }
90
+ for (const root of roots) for (const group of groupsOf(root).keys()) reachedGroups.add(group);
86
91
  const stressed = /* @__PURE__ */ new Map();
87
92
  const queue = [...reachedGroups];
88
93
  for (let i = 0; i < queue.length; i += CONCURRENCY) await Promise.all(queue.slice(i, i + CONCURRENCY).map(async (group) => {
89
94
  stressed.set(group, await readLying(byGroup.get(group) ?? []));
90
95
  }));
91
- const groupsOf = (feed) => {
92
- const out = /* @__PURE__ */ new Map();
93
- for (const target of feed ? reach.get(feed) ?? [] : []) {
94
- const source = sourceByTarget.get(target);
95
- if (drivable(source)) out.set(source.group, [...out.get(source.group) ?? [], source]);
96
- }
97
- return out;
98
- };
99
- const byAsset = /* @__PURE__ */ new Map();
100
- for (const { main, reserve, reserveFeed, notes } of positions) {
101
- const mainFeed = feedOf(main);
102
- const before = finalPrice(base.get(mainFeed) ?? null, reserveFeed ? base.get(reserveFeed) ?? null : void 0);
103
- if (before === null || before <= 0n) {
104
- logger?.warn(`feed ${mainFeed} of chain ${chainId} does not answer; ${main.tokenSymbol ?? main.token} in ${main.marketName ?? main.market} is not scored`);
105
- continue;
106
- }
107
- const onMain = groupsOf(mainFeed);
108
- const onReserve = groupsOf(reserveFeed);
109
- const impacts = [.../* @__PURE__ */ new Set([...onMain.keys(), ...onReserve.keys()])].map((group) => {
110
- const under = stressed.get(group) ?? /* @__PURE__ */ new Map();
111
- const after = finalPrice(under.get(mainFeed) ?? null, reserveFeed ? under.get(reserveFeed) ?? null : void 0);
112
- const factor = factorOf(before, after);
113
- const underlying = underlyingOf(main);
114
- const debtBefore = underlying ? base.get(underlying) ?? null : null;
115
- const debtFactor = debtBefore === null || debtBefore <= 0n || !underlying ? null : factorOf(debtBefore, under.get(underlying) ?? null);
116
- const ratioFactor = factor === null || debtFactor === null || debtFactor === 0 ? null : factor / debtFactor;
117
- const members = [...onMain.get(group) ?? [], ...onReserve.get(group) ?? []];
96
+ return {
97
+ results: effective.map(({ main, reserve, notes }) => {
98
+ const before = finalPrice(base.get(main) ?? null, reserve ? base.get(reserve) ?? null : void 0);
99
+ if (before === null || before <= 0n) return null;
100
+ const onMain = groupsOf(main);
101
+ const onReserve = groupsOf(reserve);
102
+ const impacts = [.../* @__PURE__ */ new Set([...onMain.keys(), ...onReserve.keys()])].map((group) => {
103
+ const under = stressed.get(group) ?? /* @__PURE__ */ new Map();
104
+ const after = finalPrice(under.get(main) ?? null, reserve ? under.get(reserve) ?? null : void 0);
105
+ const factor = factorOf(before, after);
106
+ const members = [...onMain.get(group) ?? [], ...onReserve.get(group) ?? []];
107
+ return {
108
+ group,
109
+ identified: members.every((source) => source.identified),
110
+ feeds: [...new Set(members.map((source) => source.description))],
111
+ reaches: onMain.has(group) ? onReserve.has(group) ? "both" : "main" : "reserve",
112
+ factor,
113
+ score: scoreOfFactor(factor)
114
+ };
115
+ });
118
116
  return {
119
- group,
120
- identified: members.every((source) => source.identified),
121
- feeds: [...new Set(members.map((source) => source.description))],
122
- reaches: onMain.has(group) ? onReserve.has(group) ? "both" : "main" : "reserve",
123
- factor,
124
- ratioFactor,
125
- score: scoreOfFactor(factor)
117
+ hasReserve: reserve !== void 0,
118
+ price: Number(formatUnits(before, 8)),
119
+ unverified: reserve !== void 0 && impacts.some((impact) => !impact.identified && impact.reaches !== "both"),
120
+ notes,
121
+ ...worstOf(impacts)
126
122
  };
123
+ }),
124
+ sourcesOf
125
+ };
126
+ }
127
+ /**
128
+ * Every asset of one chain, scored by what each of its source groups can do
129
+ * to the price its running markets hold for it.
130
+ *
131
+ * One read of the oracles' feed trees, then the simulation of every market's
132
+ * main and reserve pair at once — see {@link simulatePairs}.
133
+ **/
134
+ async function runAssetScores({ client, chainId, slots, compressor, logger }) {
135
+ const running = slots.filter((slot) => !slot.isShutdown);
136
+ const oracles = [...new Set(running.map((slot) => lower(slot.priceOracle)))];
137
+ const { nodes, answers, unreadableOracles } = await readForest(client, compressor, oracles);
138
+ for (const oracle of unreadableOracles) logger?.warn(`price oracle ${oracle} of chain ${chainId} would not report its feed tree; the markets on it are not scored`);
139
+ const slotsByPosition = /* @__PURE__ */ new Map();
140
+ for (const slot of running) {
141
+ const key = `${lower(slot.market)}:${lower(slot.token)}`;
142
+ const entry = slotsByPosition.get(key) ?? {};
143
+ entry[slot.reserve ? "reserve" : "main"] = slot;
144
+ slotsByPosition.set(key, entry);
145
+ }
146
+ const positions = [];
147
+ for (const { main, reserve } of slotsByPosition.values()) {
148
+ if (!main) continue;
149
+ const mainNode = nodes.get(lower(main.priceFeed));
150
+ if (!mainNode || mainNode.contractType === ZERO_TYPE) continue;
151
+ positions.push({
152
+ main,
153
+ reserve
127
154
  });
128
- const unverified = reserveFeed !== void 0 && impacts.some((impact) => !impact.identified && impact.reaches !== "both");
129
- const side = (slot) => ({
130
- feed: feedOf(slot),
131
- name: slot.priceFeedName,
132
- contractType: nodes.get(feedOf(slot))?.contractType ?? "",
133
- sources: [...groupsOf(feedOf(slot)).keys()]
134
- });
135
- const asset = main.token.toLowerCase();
155
+ }
156
+ const { results, sourcesOf } = await simulatePairs({
157
+ client,
158
+ chainId,
159
+ nodes,
160
+ answers,
161
+ pairs: positions.map(({ main, reserve }) => pairOf(lower(main.priceFeed), reserve ? lower(reserve.priceFeed) : void 0, nodes)),
162
+ logger
163
+ });
164
+ const side = (slot) => ({
165
+ feed: lower(slot.priceFeed),
166
+ name: slot.priceFeedName,
167
+ contractType: nodes.get(lower(slot.priceFeed))?.contractType ?? "",
168
+ sources: sourcesOf(lower(slot.priceFeed))
169
+ });
170
+ const byAsset = /* @__PURE__ */ new Map();
171
+ positions.forEach(({ main, reserve }, i) => {
172
+ const result = results[i];
173
+ if (!result) {
174
+ logger?.warn(`feed ${lower(main.priceFeed)} of chain ${chainId} does not answer; ${main.tokenSymbol ?? main.token} in ${main.marketName ?? main.market} is not scored`);
175
+ return;
176
+ }
177
+ const asset = lower(main.token);
136
178
  const entry = byAsset.get(asset) ?? {
137
179
  symbol: main.tokenSymbol,
138
180
  markets: []
@@ -145,15 +187,11 @@ async function runAssetScores({ client, chainId, slots, compressor, logger }) {
145
187
  curatorName: main.curatorName,
146
188
  main: side(main),
147
189
  reserve: reserve ? side(reserve) : null,
148
- hasReserve: reserveFeed !== void 0,
149
- price: Number(formatUnits(before, 8)),
150
- unverified,
151
- notes,
152
- ...worstOf(impacts)
190
+ ...result
153
191
  });
154
- }
192
+ });
155
193
  return [...byAsset].map(([asset, { symbol, markets }]) => {
156
- const sorted = markets.sort((a, b) => a.score - b.score);
194
+ const sorted = markets.sort((a, b) => b.score - a.score);
157
195
  const worst = sorted[0] ?? null;
158
196
  return {
159
197
  chainId,
@@ -166,5 +204,47 @@ async function runAssetScores({ client, chainId, slots, compressor, logger }) {
166
204
  };
167
205
  });
168
206
  }
207
+ /**
208
+ * Any main feed and reserve on a chain, scored the way a market holding them
209
+ * would be — whether or not any market does yet.
210
+ *
211
+ * The two trees are read off the compressor by feed rather than by oracle,
212
+ * so a feed the curator has only just deployed scores as well as one in use.
213
+ *
214
+ * @throws Where there is nothing to score: the compressor cannot read a
215
+ * feed, the main feed is a `ZeroPriceFeed`, or the final price does not
216
+ * answer even with every pull feed under it freshened.
217
+ **/
218
+ async function runFeedPairScore({ client, chainId, compressor, main, reserve, logger }) {
219
+ const mainFeed = lower(main);
220
+ const reserveFeed = reserve ? lower(reserve) : void 0;
221
+ const feeds = reserveFeed && reserveFeed !== mainFeed ? [mainFeed, reserveFeed] : [mainFeed];
222
+ const { nodes, answers } = await readFeedTrees(client, compressor, feeds).catch((cause) => {
223
+ throw new Error(`the PriceFeedCompressor of chain ${chainId} would not read ${feeds.join(" and ")}: not a price feed it knows`, { cause });
224
+ });
225
+ for (const feed of [mainFeed, reserveFeed]) if (feed && !nodes.has(feed)) throw new Error(`feed ${feed} of chain ${chainId} is not one the PriceFeedCompressor can read`);
226
+ if (nodes.get(mainFeed)?.contractType === ZERO_TYPE) throw new Error(`the main feed ${mainFeed} is a ZeroPriceFeed: it prices nothing, so nothing can move`);
227
+ const { results, sourcesOf } = await simulatePairs({
228
+ client,
229
+ chainId,
230
+ nodes,
231
+ answers,
232
+ pairs: [pairOf(mainFeed, reserveFeed, nodes)],
233
+ logger
234
+ });
235
+ const result = results[0];
236
+ if (!result) throw new Error(`the main feed ${mainFeed} of chain ${chainId} does not answer, so there is no price to move`);
237
+ const side = (feed) => ({
238
+ feed,
239
+ name: null,
240
+ contractType: nodes.get(feed)?.contractType ?? "",
241
+ sources: sourcesOf(feed)
242
+ });
243
+ return {
244
+ main: side(mainFeed),
245
+ reserve: reserveFeed ? side(reserveFeed) : null,
246
+ ...result
247
+ };
248
+ }
169
249
  //#endregion
170
- export { runAssetScores };
250
+ export { runAssetScores, runFeedPairScore };
@@ -1,6 +1,6 @@
1
1
  import { classifySources, sourcesByFeed } from "./classify.js";
2
2
  import { UNBOUNDED_DOWN, UNBOUNDED_UP, groupOverride, perturb, rateStub, roundDataStub } from "./override.js";
3
3
  import { scoreGroup, slotHarm, verdictOf } from "./score.js";
4
- import { DEFAULT_SCENARIOS, RATE_FEED_TYPE, readForest, readRateTargets, readUnder, runStressTest } from "./run.js";
4
+ import { DEFAULT_SCENARIOS, RATE_FEED_TYPE, readFeedTrees, readForest, readRateTargets, readUnder, runStressTest } from "./run.js";
5
5
  import "./types.js";
6
- export { DEFAULT_SCENARIOS, RATE_FEED_TYPE, UNBOUNDED_DOWN, UNBOUNDED_UP, classifySources, groupOverride, perturb, rateStub, readForest, readRateTargets, readUnder, roundDataStub, runStressTest, scoreGroup, slotHarm, sourcesByFeed, verdictOf };
6
+ export { DEFAULT_SCENARIOS, RATE_FEED_TYPE, UNBOUNDED_DOWN, UNBOUNDED_UP, classifySources, groupOverride, perturb, rateStub, readFeedTrees, readForest, readRateTargets, readUnder, roundDataStub, runStressTest, scoreGroup, slotHarm, sourcesByFeed, verdictOf };
@@ -2,7 +2,7 @@ import { priceFeedCompressorAbi } from "../../../../abi/compressors/priceFeedCom
2
2
  import { classifySources, sourcesByFeed } from "./classify.js";
3
3
  import { UNBOUNDED_DOWN, UNBOUNDED_UP, groupOverride } from "./override.js";
4
4
  import { scoreGroup, verdictOf } from "./score.js";
5
- import { formatUnits, hexToString, parseAbi } from "viem";
5
+ import { BaseError, CallExecutionError, formatUnits, hexToString, parseAbi } from "viem";
6
6
  //#region src/permissionless/api/oracles/stress/run.ts
7
7
  /**
8
8
  * The two ends the question is asked at, which is what "infinitely large and
@@ -71,18 +71,7 @@ async function readForest(client, compressor, oracles) {
71
71
  return;
72
72
  }
73
73
  const { priceFeedTree } = state.result;
74
- for (const node of priceFeedTree) {
75
- const address = node.baseParams.addr.toLowerCase();
76
- if (nodes.has(address)) continue;
77
- nodes.set(address, {
78
- address,
79
- contractType: typeName(node.baseParams.contractType),
80
- version: Number(node.baseParams.version),
81
- description: node.description,
82
- underlyingFeeds: node.underlyingFeeds.map((feed) => feed.toLowerCase())
83
- });
84
- answers.set(address, node.answer.success ? node.answer.price : null);
85
- }
74
+ addTree(priceFeedTree, nodes, answers);
86
75
  });
87
76
  return {
88
77
  nodes,
@@ -91,6 +80,43 @@ async function readForest(client, compressor, oracles) {
91
80
  };
92
81
  }
93
82
  /**
83
+ * The trees under a handful of feeds, flattened, whether or not any oracle
84
+ * holds them.
85
+ *
86
+ * One call for all of them: the compressor walks the feeds it is given the
87
+ * way it walks an oracle's, and answers with the same nodes.
88
+ */
89
+ async function readFeedTrees(client, compressor, feeds) {
90
+ const tree = await client.readContract({
91
+ address: compressor,
92
+ abi: priceFeedCompressorAbi,
93
+ functionName: "loadPriceFeedTree",
94
+ args: [feeds]
95
+ });
96
+ const nodes = /* @__PURE__ */ new Map();
97
+ const answers = /* @__PURE__ */ new Map();
98
+ addTree(tree, nodes, answers);
99
+ return {
100
+ nodes,
101
+ answers
102
+ };
103
+ }
104
+ /** A tree's nodes into the flattened forest, the first copy of each kept. */
105
+ function addTree(tree, nodes, answers) {
106
+ for (const node of tree) {
107
+ const address = node.baseParams.addr.toLowerCase();
108
+ if (nodes.has(address)) continue;
109
+ nodes.set(address, {
110
+ address,
111
+ contractType: typeName(node.baseParams.contractType),
112
+ version: Number(node.baseParams.version),
113
+ description: node.description,
114
+ underlyingFeeds: node.underlyingFeeds.map((feed) => feed.toLowerCase())
115
+ });
116
+ answers.set(address, node.answer.success ? node.answer.price : null);
117
+ }
118
+ }
119
+ /**
94
120
  * Where each rate-bearing feed reads its rate, and what it will accept.
95
121
  *
96
122
  * The baseline a rate source is perturbed from is the feed's own `lowerBound`
@@ -152,6 +178,18 @@ async function readRateTargets(client, feeds) {
152
178
  bounds
153
179
  };
154
180
  }
181
+ /** Tries after the first before a refused read is given up on. */
182
+ const READ_RETRIES = 3;
183
+ /**
184
+ * A multicall failure that is the node's and not the feed's.
185
+ *
186
+ * A call that reverted inside the multicall is decoded from its own return
187
+ * data and carries no error of the `eth_call` around it; a chunk the node
188
+ * refused carries exactly that, for every call in the chunk.
189
+ */
190
+ function isRefusal(error) {
191
+ return error instanceof BaseError && error.walk((cause) => cause instanceof CallExecutionError) !== null;
192
+ }
155
193
  /**
156
194
  * What every feed answers with one group's sources overridden.
157
195
  *
@@ -164,7 +202,7 @@ async function readUnder(client, feeds, sources, multiplier, updatedAt, freshen
164
202
  const driven = groupOverride(sources, multiplier, updatedAt);
165
203
  const taken = new Set(driven.map((override) => override.address.toLowerCase()));
166
204
  const overrides = [...groupOverride(freshen, 1, updatedAt).filter((override) => !taken.has(override.address.toLowerCase())), ...driven];
167
- const results = await client.multicall({
205
+ const readOnce = () => client.multicall({
168
206
  contracts: feeds.map((address) => ({
169
207
  address,
170
208
  abi: aggregatorAbi,
@@ -173,6 +211,17 @@ async function readUnder(client, feeds, sources, multiplier, updatedAt, freshen
173
211
  allowFailure: true,
174
212
  stateOverride: overrides.length > 0 ? overrides : void 0
175
213
  });
214
+ let results = [];
215
+ for (let attempt = 0;; attempt++) {
216
+ results = await readOnce();
217
+ const refused = results.find((result) => result.status === "failure" && isRefusal(result.error));
218
+ if (!refused) break;
219
+ if (attempt >= READ_RETRIES) {
220
+ const reason = refused.error instanceof BaseError ? refused.error.walk().shortMessage : String(refused.error);
221
+ throw new Error(`the node would not answer a read of ${feeds.length} feeds after 4 tries: ${reason}`, { cause: refused.error });
222
+ }
223
+ await new Promise((resolve) => setTimeout(resolve, 500 * 2 ** attempt));
224
+ }
176
225
  return new Map(feeds.map((feed, i) => {
177
226
  const result = results[i];
178
227
  return [feed, result?.status === "success" ? result.result[1] ?? null : null];
@@ -426,4 +475,4 @@ async function runStressTest({ client, chainId, slots, compressor, scenarios = D
426
475
  };
427
476
  }
428
477
  //#endregion
429
- export { DEFAULT_SCENARIOS, RATE_FEED_TYPE, readForest, readRateTargets, readUnder, runStressTest };
478
+ export { DEFAULT_SCENARIOS, RATE_FEED_TYPE, readFeedTrees, readForest, readRateTargets, readUnder, runStressTest };
@@ -15,9 +15,9 @@ import { getFeedDecimals, getLatestPrices, getPrices } from "./utils/price-updat
15
15
  import { classifySources, sourcesByFeed } from "./api/oracles/stress/classify.js";
16
16
  import { UNBOUNDED_DOWN, UNBOUNDED_UP, groupOverride, perturb, rateStub, roundDataStub } from "./api/oracles/stress/override.js";
17
17
  import { scoreGroup, slotHarm, verdictOf } from "./api/oracles/stress/score.js";
18
- import { DEFAULT_SCENARIOS, RATE_FEED_TYPE, readForest, readRateTargets, readUnder, runStressTest } from "./api/oracles/stress/run.js";
19
- import { REVERT_SCORE, SCORE_MULTIPLIER, factorOf, finalPrice, scoreOfFactor, worstOf } from "./api/oracles/score/formula.js";
20
- import { runAssetScores } from "./api/oracles/score/run.js";
18
+ import { DEFAULT_SCENARIOS, RATE_FEED_TYPE, readFeedTrees, readForest, readRateTargets, readUnder, runStressTest } from "./api/oracles/stress/run.js";
19
+ import { SCORE_MULTIPLIER, factorOf, finalPrice, scoreOfFactor, worstOf } from "./api/oracles/score/formula.js";
20
+ import { runAssetScores, runFeedPairScore } from "./api/oracles/score/run.js";
21
21
  import { GET_PRICES_CHUNK_SIZE, GET_PRICES_GAS_LIMIT_BY_CHAIN_ID, PermissionlessOracles } from "./api/oracles/PermissionlessOracles.js";
22
22
  import { TokenCompressorContract } from "./bindings/compressors/token-compressor.js";
23
23
  import { WithdrawalCompressorContract } from "./bindings/compressors/withdrawal-compressor.js";
@@ -48,4 +48,4 @@ import { getBlockNumberByTimestamp } from "./utils/block-utils.js";
48
48
  import { convertQueueBatchToExecuteTx } from "./utils/governance/timelock-txs.js";
49
49
  import { getPriceUpdateTx } from "./utils/price-update/get-price-update-tx.js";
50
50
  import "./utils/index.js";
51
- export { AccessMode, AddressProviderContract, Addresses, ArchiveTransport, BYTECODE_REPOSITORY, BytecodeRepositoryContract, CROSS_CHAIN_GOVERNANCE, CROSS_CHAIN_MULTISIG, ChainsNamespace, CrossChainMultisigContract, DEFAULT_DEPLOY_SALT, DEFAULT_SCENARIOS, GET_PRICES_CHUNK_SIZE, GET_PRICES_GAS_LIMIT_BY_CHAIN_ID, GearboxPermissionless, GovernorContract, INSTANCE_MANAGER, InstanceManagerContract, InstanceOwnerTransactions, KILL_SWITCH_PROXY, KILL_SWITCH_PROXY_MINOR_VERSION, KillSwitchProxyContract, KillSwitchProxyNotAllowedError, MAINNET_CCM_CONSTRUCTOR_PARAMS, MAINNET_DEPLOYMENT_BLOCK, MAINNET_DEPLOYMENT_CCM_SALT, MAINNET_DEPLOYMENT_IM_SALT, MARKET_CONFIGURATOR_FACTORY, MarketConfiguratorContract, MarketConfiguratorFactoryContract, PRICE_FEED_STORE, PUBLIC_CREATE2_FACTORY, PermissionlessAnalytics, PermissionlessChainUnreachableError, PermissionlessDeploy, PermissionlessNotAttachedError, PermissionlessOracles, PriceFeedNotInStoreError, PriceFeedSaltMissingError, PriceFeedStalenessUnknownError, PriceFeedStoreContract, RATE_FEED_TYPE, REVERT_SCORE, RoutingManagerContract, SCORE_MULTIPLIER, TOKEN_COMPRESSOR, TimeLockContract, TokenCompressorContract, TreasurySplitterContract, UNBOUNDED_DOWN, UNBOUNDED_UP, WithdrawalCompressorContract, calculateChecksum, checkCreate2Deployment, checkCreate2DeploymentAddress, chunkedLogsTransport, classifySources, convertPercent, convertQueueBatchToExecuteTx, convertRawTxToSafeMultisigTx, createCallData, createCreate2DeployRawTx, deepJsonParse, deployUsingPublicCreate2, deriveEffectiveStalenessPeriod, encodeFunctionParams, factorOf, finalPrice, formatBytecodeSize, formatBytecodeVersion, getBlockNumberByTimestamp, getCreate2AddressPublicFactory, getCreate2DeploymentAddress, getFeedDecimals, getLatestPrices, getPriceUpdateTx, getPrices, getSafeBatch, getSafeBatches, getUpdatablePriceFeeds, groupOverride, handleSalt, killSwitchProxySalt, normalizeSignature, perturb, priceFeedSalt, priceFeedSetupParams, rateStub, readForest, readRateTargets, readUnder, roundDataStub, runAssetScores, runStressTest, scoreGroup, scoreOfFactor, shortenHash, significantTrunc, slotHarm, sourcesByFeed, verdictOf, worstOf };
51
+ export { AccessMode, AddressProviderContract, Addresses, ArchiveTransport, BYTECODE_REPOSITORY, BytecodeRepositoryContract, CROSS_CHAIN_GOVERNANCE, CROSS_CHAIN_MULTISIG, ChainsNamespace, CrossChainMultisigContract, DEFAULT_DEPLOY_SALT, DEFAULT_SCENARIOS, GET_PRICES_CHUNK_SIZE, GET_PRICES_GAS_LIMIT_BY_CHAIN_ID, GearboxPermissionless, GovernorContract, INSTANCE_MANAGER, InstanceManagerContract, InstanceOwnerTransactions, KILL_SWITCH_PROXY, KILL_SWITCH_PROXY_MINOR_VERSION, KillSwitchProxyContract, KillSwitchProxyNotAllowedError, MAINNET_CCM_CONSTRUCTOR_PARAMS, MAINNET_DEPLOYMENT_BLOCK, MAINNET_DEPLOYMENT_CCM_SALT, MAINNET_DEPLOYMENT_IM_SALT, MARKET_CONFIGURATOR_FACTORY, MarketConfiguratorContract, MarketConfiguratorFactoryContract, PRICE_FEED_STORE, PUBLIC_CREATE2_FACTORY, PermissionlessAnalytics, PermissionlessChainUnreachableError, PermissionlessDeploy, PermissionlessNotAttachedError, PermissionlessOracles, PriceFeedNotInStoreError, PriceFeedSaltMissingError, PriceFeedStalenessUnknownError, PriceFeedStoreContract, RATE_FEED_TYPE, RoutingManagerContract, SCORE_MULTIPLIER, TOKEN_COMPRESSOR, TimeLockContract, TokenCompressorContract, TreasurySplitterContract, UNBOUNDED_DOWN, UNBOUNDED_UP, WithdrawalCompressorContract, calculateChecksum, checkCreate2Deployment, checkCreate2DeploymentAddress, chunkedLogsTransport, classifySources, convertPercent, convertQueueBatchToExecuteTx, convertRawTxToSafeMultisigTx, createCallData, createCreate2DeployRawTx, deepJsonParse, deployUsingPublicCreate2, deriveEffectiveStalenessPeriod, encodeFunctionParams, factorOf, finalPrice, formatBytecodeSize, formatBytecodeVersion, getBlockNumberByTimestamp, getCreate2AddressPublicFactory, getCreate2DeploymentAddress, getFeedDecimals, getLatestPrices, getPriceUpdateTx, getPrices, getSafeBatch, getSafeBatches, getUpdatablePriceFeeds, groupOverride, handleSalt, killSwitchProxySalt, normalizeSignature, perturb, priceFeedSalt, priceFeedSetupParams, rateStub, readFeedTrees, readForest, readRateTargets, readUnder, roundDataStub, runAssetScores, runFeedPairScore, runStressTest, scoreGroup, scoreOfFactor, shortenHash, significantTrunc, slotHarm, sourcesByFeed, verdictOf, worstOf };
@@ -5,7 +5,7 @@ import { AttachChainsArgs, ChainScope, ChainSummaryArgs, PermissionlessChain, Pe
5
5
  import { ChainsNamespace } from "./chains/ChainsNamespace.js";
6
6
  import { DeployArgs, DeployContractArgs, DeployPriceFeedArgs, PreparedDeployment, PreparedPriceFeedDeployment } from "./deploy/types.js";
7
7
  import { DEFAULT_DEPLOY_SALT, PermissionlessDeploy, priceFeedSalt } from "./deploy/PermissionlessDeploy.js";
8
- import { AssetScore, AssetScoresArgs, GroupImpact, MarketScore, ScoredFeed } from "./oracles/score/types.js";
8
+ import { AssetScore, AssetScoresArgs, FeedPairScore, FeedPairScoreArgs, GroupImpact, MarketScore, ScoredFeed } from "./oracles/score/types.js";
9
9
  import { BreakingPoint, FeedOutcome, GroupReport, GroupScenarioOutcome, SlotOutcome, StressReport, StressScenario, StressSource, StressSourceKind, StressTestArgs } from "./oracles/stress/types.js";
10
10
  import { AssetArgs, AssetCap, AssetsArgs, FeedCap, MarketCap, PermissionlessAsset, PermissionlessAssetDetails, PermissionlessAssetMarket, PermissionlessAssetMarketFeed, PermissionlessAssetQuota, PermissionlessAssetSummary, PermissionlessIndexedAssetDetails, PermissionlessIndexedAssetSummary, PermissionlessIndexedPriceFeedDetails, PermissionlessIndexedPriceFeedMarket, PermissionlessMarket, PermissionlessMarketSlot, PermissionlessPriceFeedAsset, PermissionlessPriceFeedDetails, PermissionlessPriceFeedMarket, PermissionlessPriceFeedStore, PermissionlessQuotaTotals, PriceFeedArgs, PriceFeedDecimals, PriceFeedPrice, PriceFeedRef } from "./oracles/types.js";
11
11
  import { GET_PRICES_CHUNK_SIZE, GET_PRICES_GAS_LIMIT_BY_CHAIN_ID, PermissionlessOracles } from "./oracles/PermissionlessOracles.js";
@@ -17,12 +17,12 @@ import "./chains/index.js";
17
17
  import { deriveEffectiveStalenessPeriod } from "./deploy/effective-staleness.js";
18
18
  import "./deploy/index.js";
19
19
  import { PermissionlessChainUnreachableError, PermissionlessNotAttachedError, PriceFeedNotInStoreError, PriceFeedSaltMissingError, PriceFeedStalenessUnknownError } from "./errors.js";
20
- import { REVERT_SCORE, SCORE_MULTIPLIER, factorOf, finalPrice, scoreOfFactor, worstOf } from "./oracles/score/formula.js";
21
- import { runAssetScores } from "./oracles/score/run.js";
20
+ import { SCORE_MULTIPLIER, factorOf, finalPrice, scoreOfFactor, worstOf } from "./oracles/score/formula.js";
21
+ import { runAssetScores, runFeedPairScore } from "./oracles/score/run.js";
22
22
  import { TreeNode, classifySources, sourcesByFeed } from "./oracles/stress/classify.js";
23
23
  import { UNBOUNDED_DOWN, UNBOUNDED_UP, groupOverride, perturb, rateStub, roundDataStub } from "./oracles/stress/override.js";
24
- import { DEFAULT_SCENARIOS, RATE_FEED_TYPE, readForest, readRateTargets, readUnder, runStressTest } from "./oracles/stress/run.js";
24
+ import { DEFAULT_SCENARIOS, RATE_FEED_TYPE, readFeedTrees, readForest, readRateTargets, readUnder, runStressTest } from "./oracles/stress/run.js";
25
25
  import { scoreGroup, slotHarm, verdictOf } from "./oracles/stress/score.js";
26
26
  import "./oracles/index.js";
27
27
  import "./transactions/index.js";
28
- export { AddFeedsArgs, AssetArgs, AssetCap, AssetScore, AssetScoresArgs, AssetsArgs, AttachChainsArgs, BreakingPoint, ChainScope, ChainSummaryArgs, ChainsNamespace, ChangeNamesArgs, ChangeStalenessPeriodsArgs, DEFAULT_DEPLOY_SALT, DEFAULT_SCENARIOS, DeployArgs, DeployContractArgs, DeployPriceFeedArgs, FeedCap, FeedOutcome, ForbidFeedsArgs, GET_PRICES_CHUNK_SIZE, GET_PRICES_GAS_LIMIT_BY_CHAIN_ID, GearboxPermissionless, GearboxPermissionlessOptions, GroupImpact, GroupReport, GroupScenarioOutcome, InstanceOwnerTransactions, InstanceTxsArgs, MarketCap, MarketScore, PermissionlessAnalytics, PermissionlessAsset, PermissionlessAssetDetails, PermissionlessAssetMarket, PermissionlessAssetMarketFeed, PermissionlessAssetQuota, PermissionlessAssetSummary, PermissionlessChain, PermissionlessChainSummary, PermissionlessChainUnreachableError, PermissionlessDeploy, PermissionlessIndexedAssetDetails, PermissionlessIndexedAssetSummary, PermissionlessIndexedPriceFeedDetails, PermissionlessIndexedPriceFeedMarket, PermissionlessMarket, PermissionlessMarketSlot, PermissionlessNotAttachedError, PermissionlessOracles, PermissionlessPriceFeedAsset, PermissionlessPriceFeedDetails, PermissionlessPriceFeedMarket, PermissionlessPriceFeedStore, PermissionlessQuotaTotals, PreparedDeployment, PreparedPriceFeedDeployment, PriceChartArgs, PriceFeedArgs, PriceFeedChartPoint, PriceFeedDecimals, PriceFeedNotInStoreError, PriceFeedPair, PriceFeedPrice, PriceFeedPriceChart, PriceFeedRef, PriceFeedSaltMissingError, PriceFeedStalenessUnknownError, RATE_FEED_TYPE, REVERT_SCORE, SCORE_MULTIPLIER, ScoredFeed, SlotOutcome, StressReport, StressScenario, StressSource, StressSourceKind, StressTestArgs, TreeNode, UNBOUNDED_DOWN, UNBOUNDED_UP, classifySources, deriveEffectiveStalenessPeriod, factorOf, finalPrice, groupOverride, perturb, priceFeedSalt, rateStub, readForest, readRateTargets, readUnder, roundDataStub, runAssetScores, runStressTest, scoreGroup, scoreOfFactor, slotHarm, sourcesByFeed, verdictOf, worstOf };
28
+ export { AddFeedsArgs, AssetArgs, AssetCap, AssetScore, AssetScoresArgs, AssetsArgs, AttachChainsArgs, BreakingPoint, ChainScope, ChainSummaryArgs, ChainsNamespace, ChangeNamesArgs, ChangeStalenessPeriodsArgs, DEFAULT_DEPLOY_SALT, DEFAULT_SCENARIOS, DeployArgs, DeployContractArgs, DeployPriceFeedArgs, FeedCap, FeedOutcome, FeedPairScore, FeedPairScoreArgs, ForbidFeedsArgs, GET_PRICES_CHUNK_SIZE, GET_PRICES_GAS_LIMIT_BY_CHAIN_ID, GearboxPermissionless, GearboxPermissionlessOptions, GroupImpact, GroupReport, GroupScenarioOutcome, InstanceOwnerTransactions, InstanceTxsArgs, MarketCap, MarketScore, PermissionlessAnalytics, PermissionlessAsset, PermissionlessAssetDetails, PermissionlessAssetMarket, PermissionlessAssetMarketFeed, PermissionlessAssetQuota, PermissionlessAssetSummary, PermissionlessChain, PermissionlessChainSummary, PermissionlessChainUnreachableError, PermissionlessDeploy, PermissionlessIndexedAssetDetails, PermissionlessIndexedAssetSummary, PermissionlessIndexedPriceFeedDetails, PermissionlessIndexedPriceFeedMarket, PermissionlessMarket, PermissionlessMarketSlot, PermissionlessNotAttachedError, PermissionlessOracles, PermissionlessPriceFeedAsset, PermissionlessPriceFeedDetails, PermissionlessPriceFeedMarket, PermissionlessPriceFeedStore, PermissionlessQuotaTotals, PreparedDeployment, PreparedPriceFeedDeployment, PriceChartArgs, PriceFeedArgs, PriceFeedChartPoint, PriceFeedDecimals, PriceFeedNotInStoreError, PriceFeedPair, PriceFeedPrice, PriceFeedPriceChart, PriceFeedRef, PriceFeedSaltMissingError, PriceFeedStalenessUnknownError, RATE_FEED_TYPE, SCORE_MULTIPLIER, ScoredFeed, SlotOutcome, StressReport, StressScenario, StressSource, StressSourceKind, StressTestArgs, TreeNode, UNBOUNDED_DOWN, UNBOUNDED_UP, classifySources, deriveEffectiveStalenessPeriod, factorOf, finalPrice, groupOverride, perturb, priceFeedSalt, rateStub, readFeedTrees, readForest, readRateTargets, readUnder, roundDataStub, runAssetScores, runFeedPairScore, runStressTest, scoreGroup, scoreOfFactor, slotHarm, sourcesByFeed, verdictOf, worstOf };
@@ -2,7 +2,7 @@ import { PriceFeedData } from "../../../model/opportunities.js";
2
2
  import { AbstractOffchainNamespace } from "../../../offchain/AbstractOffchainNamespace.js";
3
3
  import { GearboxPermissionlessOptions } from "../types.js";
4
4
  import { ChainsNamespace } from "../chains/ChainsNamespace.js";
5
- import { AssetScore, AssetScoresArgs } from "./score/types.js";
5
+ import { AssetScore, AssetScoresArgs, FeedPairScore, FeedPairScoreArgs } from "./score/types.js";
6
6
  import { StressReport, StressTestArgs } from "./stress/types.js";
7
7
  import { AssetArgs, AssetCap, AssetsArgs, FeedCap, MarketCap, PermissionlessAssetDetails, PermissionlessAssetSummary, PermissionlessMarketSlot, PermissionlessPriceFeedDetails, PermissionlessPriceFeedStore, PriceFeedArgs, PriceFeedDecimals, PriceFeedPrice, PriceFeedRef } from "./types.js";
8
8
  //#region src/permissionless/api/oracles/PermissionlessOracles.d.ts
@@ -192,14 +192,16 @@ export declare class PermissionlessOracles extends AbstractOffchainNamespace {
192
192
  * three leaves anything to simulate.
193
193
  **/
194
194
  /**
195
- * Every asset's score out of 10 for how its markets guard its price.
195
+ * Every asset's manipulation resistance: how far, from 1 to 10 times, a
196
+ * lying source can push the price its markets hold for it.
196
197
  *
197
198
  * Each source group behind an asset's feeds — Chainlink, Redstone, Lido and
198
- * so on — is made in turn to answer five times what it answers now, and a
199
+ * so on — is made in turn to answer ten times what it answers now, and a
199
200
  * market scores by how far that moves its final price, `min(main,
200
- * reserve)`: 10 for not at all, 0 for the full five times, on a log scale.
201
- * A price that stops answering scores 5. An asset scores its worst running
202
- * market that takes it as collateral.
201
+ * reserve)`, as a multiple either way: 1 for not at all, 1.04 for 4%, 10
202
+ * for the whole lie. A group under which the price stops answering moves
203
+ * nothing and is listed rather than scored. An asset scores its worst
204
+ * running market, underlying or collateral.
203
205
  *
204
206
  * Simulated on the chain as it stands, through state overrides in
205
207
  * `eth_call`: one call per source group per chain, so the node behind the
@@ -215,6 +217,28 @@ export declare class PermissionlessOracles extends AbstractOffchainNamespace {
215
217
  * {@link GearboxPermissionless.attach} has resolved.
216
218
  **/
217
219
  assetScores({ chainId }?: AssetScoresArgs): Promise<AssetScore[]>;
220
+ /**
221
+ * The manipulation resistance of any main feed and reserve on a chain,
222
+ * scored the way {@link assetScores} scores a market holding them — for
223
+ * trying a pair before any market does.
224
+ *
225
+ * ```ts
226
+ * const pair = await permissionless.oracles.feedPairScore({
227
+ * chainId: 1,
228
+ * main: "0x…",
229
+ * reserve: "0x…",
230
+ * });
231
+ * ```
232
+ *
233
+ * Reads the chain directly, through state overrides in `eth_call`, so the
234
+ * client has to have attached and the node behind it has to accept them.
235
+ *
236
+ * @throws When the client has not attached, when the chain has no client or
237
+ * no PriceFeedCompressor, or when there is nothing to score: a feed the
238
+ * compressor cannot read, a `ZeroPriceFeed` main, or a final price that does
239
+ * not answer.
240
+ **/
241
+ feedPairScore({ chainId, main, reserve }: FeedPairScoreArgs): Promise<FeedPairScore>;
218
242
  stressTest({ chainId, ...options }: StressTestArgs): Promise<StressReport>;
219
243
  /**
220
244
  * The feeds one feed reads from, recursively.
@@ -1,13 +1,13 @@
1
- import { AssetScore, AssetScoresArgs, GroupImpact, MarketScore, ScoredFeed } from "./score/types.js";
1
+ import { AssetScore, AssetScoresArgs, FeedPairScore, FeedPairScoreArgs, GroupImpact, MarketScore, ScoredFeed } from "./score/types.js";
2
2
  import { BreakingPoint, FeedOutcome, GroupReport, GroupScenarioOutcome, SlotOutcome, StressReport, StressScenario, StressSource, StressSourceKind, StressTestArgs } from "./stress/types.js";
3
3
  import { AssetArgs, AssetCap, AssetsArgs, FeedCap, MarketCap, PermissionlessAsset, PermissionlessAssetDetails, PermissionlessAssetMarket, PermissionlessAssetMarketFeed, PermissionlessAssetQuota, PermissionlessAssetSummary, PermissionlessIndexedAssetDetails, PermissionlessIndexedAssetSummary, PermissionlessIndexedPriceFeedDetails, PermissionlessIndexedPriceFeedMarket, PermissionlessMarket, PermissionlessMarketSlot, PermissionlessPriceFeedAsset, PermissionlessPriceFeedDetails, PermissionlessPriceFeedMarket, PermissionlessPriceFeedStore, PermissionlessQuotaTotals, PriceFeedArgs, PriceFeedDecimals, PriceFeedPrice, PriceFeedRef } from "./types.js";
4
4
  import { GET_PRICES_CHUNK_SIZE, GET_PRICES_GAS_LIMIT_BY_CHAIN_ID, PermissionlessOracles } from "./PermissionlessOracles.js";
5
- import { REVERT_SCORE, SCORE_MULTIPLIER, factorOf, finalPrice, scoreOfFactor, worstOf } from "./score/formula.js";
6
- import { runAssetScores } from "./score/run.js";
5
+ import { SCORE_MULTIPLIER, factorOf, finalPrice, scoreOfFactor, worstOf } from "./score/formula.js";
6
+ import { runAssetScores, runFeedPairScore } from "./score/run.js";
7
7
  import "./score/index.js";
8
8
  import { TreeNode, classifySources, sourcesByFeed } from "./stress/classify.js";
9
9
  import { UNBOUNDED_DOWN, UNBOUNDED_UP, groupOverride, perturb, rateStub, roundDataStub } from "./stress/override.js";
10
- import { DEFAULT_SCENARIOS, RATE_FEED_TYPE, readForest, readRateTargets, readUnder, runStressTest } from "./stress/run.js";
10
+ import { DEFAULT_SCENARIOS, RATE_FEED_TYPE, readFeedTrees, readForest, readRateTargets, readUnder, runStressTest } from "./stress/run.js";
11
11
  import { scoreGroup, slotHarm, verdictOf } from "./stress/score.js";
12
12
  import "./stress/index.js";
13
- export { AssetArgs, AssetCap, AssetScore, AssetScoresArgs, AssetsArgs, BreakingPoint, DEFAULT_SCENARIOS, FeedCap, FeedOutcome, GET_PRICES_CHUNK_SIZE, GET_PRICES_GAS_LIMIT_BY_CHAIN_ID, GroupImpact, GroupReport, GroupScenarioOutcome, MarketCap, MarketScore, PermissionlessAsset, PermissionlessAssetDetails, PermissionlessAssetMarket, PermissionlessAssetMarketFeed, PermissionlessAssetQuota, PermissionlessAssetSummary, PermissionlessIndexedAssetDetails, PermissionlessIndexedAssetSummary, PermissionlessIndexedPriceFeedDetails, PermissionlessIndexedPriceFeedMarket, PermissionlessMarket, PermissionlessMarketSlot, PermissionlessOracles, PermissionlessPriceFeedAsset, PermissionlessPriceFeedDetails, PermissionlessPriceFeedMarket, PermissionlessPriceFeedStore, PermissionlessQuotaTotals, PriceFeedArgs, PriceFeedDecimals, PriceFeedPrice, PriceFeedRef, RATE_FEED_TYPE, REVERT_SCORE, SCORE_MULTIPLIER, ScoredFeed, SlotOutcome, StressReport, StressScenario, StressSource, StressSourceKind, StressTestArgs, TreeNode, UNBOUNDED_DOWN, UNBOUNDED_UP, classifySources, factorOf, finalPrice, groupOverride, perturb, rateStub, readForest, readRateTargets, readUnder, roundDataStub, runAssetScores, runStressTest, scoreGroup, scoreOfFactor, slotHarm, sourcesByFeed, verdictOf, worstOf };
13
+ export { AssetArgs, AssetCap, AssetScore, AssetScoresArgs, AssetsArgs, BreakingPoint, DEFAULT_SCENARIOS, FeedCap, FeedOutcome, FeedPairScore, FeedPairScoreArgs, GET_PRICES_CHUNK_SIZE, GET_PRICES_GAS_LIMIT_BY_CHAIN_ID, GroupImpact, GroupReport, GroupScenarioOutcome, MarketCap, MarketScore, PermissionlessAsset, PermissionlessAssetDetails, PermissionlessAssetMarket, PermissionlessAssetMarketFeed, PermissionlessAssetQuota, PermissionlessAssetSummary, PermissionlessIndexedAssetDetails, PermissionlessIndexedAssetSummary, PermissionlessIndexedPriceFeedDetails, PermissionlessIndexedPriceFeedMarket, PermissionlessMarket, PermissionlessMarketSlot, PermissionlessOracles, PermissionlessPriceFeedAsset, PermissionlessPriceFeedDetails, PermissionlessPriceFeedMarket, PermissionlessPriceFeedStore, PermissionlessQuotaTotals, PriceFeedArgs, PriceFeedDecimals, PriceFeedPrice, PriceFeedRef, RATE_FEED_TYPE, SCORE_MULTIPLIER, ScoredFeed, SlotOutcome, StressReport, StressScenario, StressSource, StressSourceKind, StressTestArgs, TreeNode, UNBOUNDED_DOWN, UNBOUNDED_UP, classifySources, factorOf, finalPrice, groupOverride, perturb, rateStub, readFeedTrees, readForest, readRateTargets, readUnder, roundDataStub, runAssetScores, runFeedPairScore, runStressTest, scoreGroup, scoreOfFactor, slotHarm, sourcesByFeed, verdictOf, worstOf };